第一次接触CTF Pwn的新手常有个误区:觉得只要会写Python脚本、会用现成工具就能打比赛。直到遇到第一道需要手写shellcode的题目时,看着满屏的mov eax, ebx和0x08048000才会意识到——不懂汇编和内存模型,连题目在干什么都看不懂。
我在带新人训练时发现,90%的Pwn题漏洞利用都需要以下基础能力:
这些全都建立在扎实的汇编和内存知识上。去年DEF CON CTF中一道看似简单的栈溢出题,就因为选手没注意x86和x64调用约定差异,导致精心构造的ROP链全部失效。
x86架构有8个通用寄存器,就像厨师手边的调料盒:
数据寄存器:EAX(累加器)、EBX(基址)、ECX(计数器)、EDX(数据)
mov eax, 1表示系统调用exitloop指令依赖它指针寄存器:ESP(栈指针)、EBP(基址指针)
push eax等效于sub esp,4; mov [esp],eaxmov ebp, esp建立变址寄存器:ESI(源索引)、EDI(目的索引)
rep movsb指令会按ESI→EDI方向连续复制实际解题时遇到32位和64位寄存器命名差异:
- 32位:EAX, EBX...
- 64位:RAX, RBX...
高位和低位关系:RAX包含EAX,EAX包含AX,AX又分AH和AL
assembly复制mov eax, ebx ; ebx值复制到eax
lea ecx, [eax+4] ; 计算eax+4的地址存入ecx(不访问内存)
xchg edx, esi ; 交换两个寄存器值
assembly复制add esp, 0x10 ; 栈指针下移16字节
sub ecx, 1 ; 计数器减1
imul eax, ebx ; 有符号乘法
assembly复制cmp eax, 5 ; 比较eax与5
jz label1 ; 相等则跳转(ZF=1时)
call 0x08048400 ; 调用函数(压入返回地址)
ret ; 从栈弹出返回地址
leave = mov esp, ebp; pop ebp (销毁当前栈帧)nop = 0x90 (滑板指令,用于对齐payload)int 0x80 = 触发系统调用(32位Linux)用cat /proc/[pid]/maps查看进程内存映射,典型布局如下:
code复制08048000-08049000 r-xp /target ; 代码段(.text)
08049000-0804a000 r--p /target ; 只读数据(.rodata)
0804a000-0804b000 rw-p /target ; 可写数据(.data/.bss)
f7de0000-f7fa0000 r-xp /lib32/libc ; libc代码段
ffb90000-ffbb0000 rw-p [stack] ; 主线程栈
r-x(不可写)rw-brk/sbrk扩展以func(1,2,3)调用为例:
c复制push 3 ; 参数从右向左压栈
push 2
push 1
call func ; 压入返回地址
; 进入func后:
push ebp ; 保存旧基址
mov ebp, esp ; 建立新栈帧
sub esp, 0x10 ; 分配局部变量空间
此时栈布局:
code复制+----------------+
| 局部变量 | ← ebp-0x10
+----------------+
| 旧ebp值 | ← ebp
+----------------+
| 返回地址 | ← ebp+4
+----------------+
| 参数1 | ← ebp+8
+----------------+
| 参数2 | ← ebp+12
+----------------+
| 参数3 | ← ebp+16
+----------------+
64位系统参数传递规则不同:前6个参数通过RDI,RSI,RDX,RCX,R8,R9传递
观察存在漏洞的代码:
c复制void vuln() {
char buf[16];
gets(buf); // 无长度检查
}
编译后对应的汇编:
assembly复制vuln:
push ebp
mov ebp, esp
sub esp, 0x18 ; 分配24字节空间(16字节buf+对齐)
lea eax, [ebp-0x10]
push eax
call gets
add esp, 4
leave
ret
当输入超过16字节时,数据会覆盖:
计算偏移量的实用方法:
python复制from pwn import *
# 方法1:cyclic模式字符串
payload = cyclic(100)
send(payload)
# 崩溃时eip=0x6161616c → 计算得偏移为cyclic_find(0x6161616c)
# 方法2:手动计算
offset = 16(buf) + 4(旧ebp) = 20
payload = b'A'*20 + p32(0xdeadbeef)
现代系统常见防护:
绕过示例(部分):
python复制# 泄露canary值
send("%15$p") # 通过格式化字符串漏洞
canary = int(recv(),16)
# 构造含canary的payload
payload = b'A'*16 + p32(canary) + b'B'*12 + p32(target)
bash复制gdb -q ./target # 安静模式启动
b *0x08048400 # 下断点
r < input # 重定向输入
x/10wx $esp # 查看栈内存
info registers # 显示寄存器状态
p system # 打印函数地址
汇编入门:
系统底层:
man proc查看内存映射说明实战训练:
最后给个忠告:不要满足于跑通exp脚本,一定要用gdb单步跟踪每条指令对寄存器和内存的影响。我当年就是通过反复调试一道题,才真正理解了leave指令对栈帧的破坏作用。