【PWN】04.Linux-User Mode-栈溢出-x86-中级ROP
中级 ROP 主要是使用了一些比较巧妙的 Gadgets。
1 ret2csu
原理
在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。
我们先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)
.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
.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+54j
.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+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
这里我们可以利用以下几点:
(1)从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。
(2)从 0x0000000000400600 到 0x0000000000400609,
loc_400600:mov rdx, r13 ; 将 r13 的值复制到 rdxmov rsi, r14 ; 将 r14 的值复制到 rsimov edi, r15d ; 将 r15d 的值复制到 edi (rdi 的低 32 位)
这三条指令在准备调用一个函数所需的参数。
x64 调用约定 (System V AMD64 ABI): 在 Linux/macOS 等系统的 x64 架构上,函数的前三个参数分别通过寄存器 rdi
(第一个)、rsi
(第二个)、rdx
(第三个) 传递。
指令
mov edi, r15d
只操作 32 位寄存器 (edi
是rdi
的低 32 位)。在 x64 架构下,当向一个 32 位寄存器 (如
edi
) 写入值时,对应的 64 位寄存器 (rdi
) 的高 32 位会自动清零。因此,
mov edi, r15d
的效果等同于mov rdi, r15
(前提是r15
的高 32 位本身是 0,或者你只关心r15
的低 32 位值)。结论是:通过控制
r15d
,我们实际上可以完全控制rdi
寄存器的值(使其低 32 位为r15d
,高 32 位为 0)。
call qword ptr [r12+rbx*8] ; 调用地址位于 [r12 + rbx*8] 处的函数
如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。
(3)从 0x000000000040060D 到 0x0000000000400614,
add rbx, 1 ; rbx = rbx + 1 (索引加 1)cmp rbx, rbp ; 比较 rbx 和 rbpjnz short loc_400600 ; 如果不相等 (rbx != rbp),跳回 loc_400600 继续循环
-
作用: 这部分控制循环是否继续执行下一次迭代。
-
原始循环逻辑: 在初始化函数时,
rbp
被设置成.init_array
中函数指针的数量((rbp - r12) / 8
)。循环从rbx = 0
开始,每次调用[r12 + rbx*8]
指向的函数后,rbx
加 1,然后和rbp
(函数数量) 比较。只要rbx < rbp
(jnz
条件满足),就跳回去继续调用下一个函数。 -
打破循环的关键: 如果我们想利用这段代码只调用 一次 我们指定的函数,然后正常退出循环继续执行后面的代码(
pop
指令和retn
),我们需要确保循环在执行一次后就停止。 -
这可以通过设置
rbx
和rbp
的值,使得在第一次执行完add rbx, 1
后,rbx
的值等于rbp
的值。 -
最直接的方法:设置
rbx = 0
和rbp = 1(
控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp)
。-
进入循环:
rbx = 0
,rbp = 1
(0 != 1
, 条件满足,进入循环体) -
调用函数:
call [r12 + 0*8]
->call [r12]
-
增加索引:
rbx = 0 + 1 = 1
-
比较:
cmp rbx(1), rbp(1)
-> 相等 (Zero Flag set) -
条件跳转:
jnz
(Jump if Not Zero) 条件不满足,不跳转。 -
继续执行: 执行
add rsp, 8
及后面的pop
和retn
指令,正常退出函数/循环。
-
-
目的: 这样设置后,这段代码片段就变成了一个“调用一个特定函数(地址在
[r12]
)并使用特定参数(r15d/r15
->rdi
,r14
->rsi
,r13
->rdx
),然后正常返回”的自包含 gadget。
例1
这里我们以蒸米的一步一步学 ROP 之 linux_x64 篇中 level5 为例。
首先检查程序的安全保护
程序为 64 位,开启了堆栈不可执行保护。
其次,寻找程序的漏洞,可以看出程序中有一个简单的栈溢出
read()
是一个系统函数,用于 从输入源读取数据。
-
参数解释:
-
0
:表示从 标准输入(stdin) 读取数据(比如键盘输入或文件重定向)。 -
0x200uLL
:表示要读取 512 字节 的数据(0x200
是十六进制,等于十进制的 512;uLL
表示“无符号长长整型”)
-
read()
会向 buf
写入 512 字节,但 buf
只能容纳 128 字节。
简单浏览下程序,发现程序中既没有 system 函数地址,也没有 /bin/sh 字符串,所以两者都需要我们自己去构造了。
教程中,作者尝试在本机使用 system 函数来获取 shell 失败了,应该是环境变量的问题,所以这里使用的是 execve 来获取 shell。
基本利用思路如下
- 利用栈溢出执行 libc_csu_gadgets 获取 write 函数地址,并使得程序重新执行 main 函数
- 根据 libcsearcher 获取对应 libc 版本以及 execve 函数地址
- 再次利用栈溢出执行 libc_csu_gadgets 向 bss 段写入 execve 地址以及 '/bin/sh’ 地址,并使得程序重新执行 main 函数。
- 再次利用栈溢出执行 libc_csu_gadgets 执行 execve('/bin/sh') 获取 shell。