bugs bunny ctf 2017-pwn150
1.常规checksec,可以发现NX enabled,并且没有RAX字段。打开IDA后可以看到在hello函数中存在栈溢出:
1 | #注释头 |
然后分析程序,汇编代码什么的,没找到有call eax之类的操作,这里就选择ROP来getshell。
2.由于是64位程序,传参方式不同,依次为:rdi, rsi, rdx, rcx, r8, r9, 栈,而我们的目标是跳转system函数,让system函数读取binsh字符串,system函数又只有一个参数,所以这个参数必然需要在rdi中读取。我们的输入是位于栈上,所以需要一个pop rdi;和ret的操作命令,让我们的输入赋值给rdi寄存器。
3.在哪找pop rdi; ret;也是个问题,这里有个工具可以实现ROPgadget ,在linux下可以输入:以下代码来获取代码地址。
1 | #注释头 |
4.然后需要system函数的地址,这里today函数直接call了该函数,所以可以直接用IDA在汇编中看到该地址(行的前缀)。或者先ctrl + s,在got.plt中搜索一下,发现也能找到system函数。所以这里获取system地址我们可以有两种方法:
①pop rdi之后,让ret指向today函数中的call_system_地址:0x40075F
②pop rdi之后,让ret指向从elf = ELF(‘./pwn150’)和system_addr = p64(elf.symbols[‘system’])中找到的地址system_addr,也就是plt表中的地址(这里其实可以直接在IDA中找到)
(但是需要注意的是,这是64位程序,system函数从rdi上取值,与栈无关系,所以call和直接跳转plt差不多,但是如果是32位程序,那么布置栈的时候就需要考虑到plt表和直接call system函数的不同了。如果是直接跳转plt表中的地址,那么栈的布置顺序应该是:
system函数-system函数的返回地址-sytem函数的参数。
但如果是跳转call system,那么由于call指令会自动push进eip,则栈布置应该为:
call system函数地址-system函数参数。
两者不太一样,需要加以区分。后面会有got表和plt的详细讲解)
4.接下来寻找binsh字符串,但是没找到,只有sh,也可以开shell。shift+F12进入字符串后右键在十六进制中同步,之后可以对应看到sh的字符地址,由于sh之后直接就是结束字符00,不会往后多读,而只会读取sh,所以可以直接将该字符串的地址写在pop rdi地址后面,直接赋值给rdi,写进去。
5.编写payload,顺序为:payload = padding + pop_rdi_addr + bin_sh_addr + system_addr(或者是call_system_addr)。
▲由于64位程序中通常参数从左到右依次放在rdi, rsi, rdx, rcx, r8, r9,多出来的参数才会入栈(根据调用约定的方式可能有不同,通常是这样),因此,我们就需要一个给RDI赋值的办法。也就是ROPgadget –pwn150 | grep “pop rdi”这段代码获取。所以进入system中用call和return直接进都行,参数是从rdi中获取的,进去之后栈上的返回地址是啥都没关系,因为已经getshell,用不到。
▲执行call func_addr指令相当于push eip ;jmp func_addr,而执行plt表中地址则相当于只有jmp func_addr,没有前面的push eip,所以需要手动设置这个eip,而call则不用。注意这是32位程序下,64位程序下则按照本篇所说,直接pop rdi即可。
参考资料:
https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157