第3章:汇编语言基础(x86/x64)——逆向工程的“母语”
说实话,很多刚接触Pwn的朋友一看到汇编就头大。我完全理解——满屏的mov、push、pop,看着像天书。但你要知道,汇编是逆向工程的母语。你读不懂汇编,就相当于拿着英文词典去读中文古诗——每个字都认识,连起来不知道啥意思。
这一章,咱们就把x86/x64汇编的底裤扒干净。不扯虚的,直接上干货。
3.1 寄存器体系:CPU的“手边记事本”
寄存器是什么?说白了就是CPU内部的一小块存储空间。它离运算单元最近,读写速度比内存快几个数量级。我习惯把寄存器想象成程序员桌面上的便签纸——随手记、随手用,但数量有限。
3.1.1 x86(32位)通用寄存器
| 寄存器 | 名称 | 我的理解 |
|---|---|---|
| EAX | 累加器 | 算数运算的主力,函数返回值也放这 |
| EBX | 基址寄存器 | 常用来存内存地址的基址 |
| ECX | 计数器 | 循环、移位操作默认用它计数 |
| EDX | 数据寄存器 | 配合EAX做乘除法,存余数 |
| ESI | 源索引 | 字符串操作时指向源地址 |
| EDI | 目标索引 | 字符串操作时指向目标地址 |
| ESP | 栈指针 | 指向当前栈顶,千万别乱改 |
| EBP | 基址指针 | 指向当前栈帧底部,方便访问局部变量 |
3.1.2 标志寄存器(EFLAGS/RFLAGS)
这个寄存器不存数据,存的是状态。比如上一条指令结果是不是0(ZF标志)、有没有进位(CF标志)、有没有溢出(OF标志)。逆向时经常看到jz、jnz这类跳转指令,它们就是根据标志位来决定跳不跳的。
我记得有一次逆向一个CTF题目,函数里有个cmp eax, 0x1337后面跟了个jz。我当时没注意标志位变化,硬是看了半天才反应过来——哦,这是在检查返回值是不是等于0x1337。
3.2 栈与函数调用约定
栈是什么?一个后进先出的数据结构。在汇编层面,栈就是内存里一块连续区域,由ESP/RSP指向栈顶。push让ESP减小,pop让ESP增大。嗯,这里要注意:栈是向下增长的——你往栈里压东西,栈顶地址反而变小。
3.2.1 函数调用过程
一个函数调用,CPU干了三件事:
- 传参:按约定把参数放到寄存器或压栈
- 保存返回地址:
call指令会把下一条指令地址压栈 - 跳转:CPU跳到函数入口执行
函数返回时,ret指令从栈顶弹出返回地址,跳回去继续执行。
3.2.2 三种调用约定对比
| 约定 | 参数传递 | 谁清理栈 | 适用场景 |
|---|---|---|---|
| cdecl | 全部压栈(从右到左) | 调用者 | C语言默认,CTF最常见 |
| stdcall | 全部压栈(从右到左) | 被调用者 | Win32 API |
| fastcall | 前两个参数用ECX/EDX,其余压栈 | 被调用者 | 追求速度的场景 |
我个人习惯在逆向时先看函数开头有没有ret 0x10这种带参数的返回——如果有,八成是stdcall或fastcall。cdecl的话,ret后面什么都不跟,调用者自己加add esp, xxx来清理。
3.3 常用指令详解
下面这些指令,你在逆向里每天都会见到。我按使用频率排了个序。
3.3.1 数据传送类
mov dst, src:把src的值复制到dst。注意,两个操作数不能都是内存地址。push src:把src压栈,ESP减4(32位)或8(64位)。pop dst:从栈顶弹出数据到dst,ESP增加。lea dst, [addr]:加载有效地址。不访问内存,只计算地址。比如lea eax, [ebx+ecx*4]——这比mov加乘法快多了。
lea和mov的区别。你想想看,mov eax, [ebx]是把ebx指向的内存里的值取出来;而lea eax, [ebx]是把ebx的地址本身给eax。我见过新手把这两个搞混,调试半天找不到bug。
3.3.2 控制流类
call addr:先把返回地址压栈,然后跳转到addr。ret:从栈顶弹出返回地址,跳转过去。jmp addr:无条件跳转到addr。jz / je addr:如果ZF=1(结果为0/相等),跳转。jnz / jne addr:如果ZF=0(结果不为0/不相等),跳转。jg / jge / jl / jle:有符号数比较跳转。ja / jae / jb / jbe:无符号数比较跳转。
这里有个小规律:j开头的指令都是条件跳转,后面两个字母代表条件。比如jz就是jump if zero。逆向时看到cmp后面跟jz,基本就是个if判断。
3.3.3 算术与逻辑类
add dst, src:dst += srcsub dst, src:dst -= srcinc dst:dst++dec dst:dst--xor dst, src:dst ^= src。注意,xor eax, eax是清零的惯用写法,比mov eax, 0更高效。and / or / not:按位与、或、非。shl / shr:左移/右移。左移1位相当于乘2,右移1位相当于除2。
3.4 知识体系总览
下面这张图把本章的核心知识点串起来了。你把它存脑子里,以后逆向时随时调取。
3.5 实战小练习
光看不练假把式。我给你一段简单的汇编代码,你试着分析它在干什么:
push ebp
mov ebp, esp
sub esp, 0x10
mov dword [ebp-0x4], 0xa
mov dword [ebp-0x8], 0x14
mov eax, [ebp-0x4]
add eax, [ebp-0x8]
mov esp, ebp
pop ebp
ret
这段代码做了什么事?我提示你:它先保存了旧的EBP,然后开辟了16字节的局部空间,存了两个数,加起来,最后返回。嗯,说白了就是一个加法函数。