逆向工程实战
逆向的本质只有一件事:把编译器做的事逆回去。编译器把人的逻辑变成机器指令,逆向就是从机器指令还原人的逻辑。本文不讲工具操作手册,只讲逆向的思维方法和比赛中的实战套路。
一、逆向的本质
为什么能逆向
源代码 → 编译器 → 机器码
编译是确定性的映射:同一段源码,同一编译器,同一优化级别,产出相同的机器码。
逆向利用的就是这个确定性——机器码中保留了源码的逻辑结构,只是表达形式变了。
逆向做不到完美还原源码(变量名、注释丢失),但能还原程序的行为逻辑——这对CTF足够了。
逆向的核心循环
观察现象 → 定位关键代码 → 理解逻辑 → 构造输入
1. 观察现象:程序做了什么?输入什么?输出什么?
2. 定位关键代码:找到判断flag的逻辑在哪
3. 理解逻辑:这个判断条件是什么?算法是什么?
4. 构造输入:根据逻辑算出正确的输入(flag)
二、前置知识
2.1 汇编——逆向的"英语"
不需要会写汇编,但必须能读。逆向中99%的时间在读汇编,只有1%在写。
x86-64 核心指令(掌握这些就能读大部分代码):
| 类别 | 指令 | 含义 |
|---|---|---|
| 数据移动 | mov dst, src |
dst = src |
| 算术运算 | add/sub/imul/idiv |
加/减/乘/除 |
| 逻辑运算 | and/or/xor/not/shl/shr |
与/或/异或/非/左移/右移 |
| 比较 | cmp a, b |
计算a-b设置标志位 |
| 测试 | test a, b |
计算a&b设置标志位 |
| 跳转 | je/jne/jg/jl/jge/jle |
等于/不等于/大于/小于/大于等于/小于等于时跳转 |
| 函数调用 | call/ret |
调用函数/返回 |
| 栈操作 | push/pop |
入栈/出栈 |
关键寄存器:
通用寄存器(x86-64):
rax — 函数返回值
rdi, rsi, rdx, rcx, r8, r9 — 函数参数(按顺序)
rbx, r12-r15 — 被调用者保存(函数内用前必须保存)
rsp — 栈顶指针
rbp — 栈底指针(帧指针)
标志寄存器关键位:
ZF — 零标志(结果为0时置1)
SF — 符号标志(结果为负时置1)
CF — 进位标志
OF — 溢出标志
2.2 调用约定
Linux (System V AMD64 ABI):
参数传递:rdi → rsi → rdx → rcx → r8 → r9 → 栈
返回值:rax
栈对齐:call前RSP必须是16的倍数
Windows (x64):
参数传递:rcx → rdx → r8 → r9 → 栈
返回值:rax
影子空间:调用者预留32字节
识别调用约定的作用:
看到mov rdi, xxx → call → Linux约定
看到mov rcx, xxx → call → Windows约定
由此判断参数个数和类型
2.3 可执行文件格式
ELF (Linux):
ELF Header → 文件类型、入口点、架构
Program Headers → 内存段布局(加载器用)
Section Headers → 代码段(.text)、数据段(.data/.bss)、符号表(.symtab)
PE (Windows):
DOS Header → PE签名
Optional Header → 入口点、镜像基址
Section Table → .text/.data/.rdata/.rsrc
逆向关注点:
入口点(Entry Point)→ 程序从哪开始执行
.text段大小 → 代码量,判断是否有壳
.symtab是否存在 → 有符号表则函数名可读,逆向难度骤降
三、静态分析
3.1 思路
静态分析的核心是不运行程序,通过阅读反编译代码理解逻辑。
IDA Pro / Ghidra 工作流:
1. 找入口点 → F5反编译main
2. 识别关键函数 → 看字符串引用(Shift+F12)→ 交叉引用(X键)定位
3. 读伪代码 → 理解算法逻辑
4. 标注 → 重命名变量/函数,加注释,让逻辑越来越清晰
3.2 定位关键代码
最快的方法:字符串引用。
程序总要输出"correct"/"wrong"/"flag{"之类的字符串
→ IDA中 Shift+F12 打开字符串窗口
→ 找到关键字符串
→ 双击 → 交叉引用(X) → 直接跳到判断逻辑
没有明显字符串?
→ 看输入函数:scanf/gets/read/fgets → 交叉引用找调用点
→ 看比较函数:strcmp/memcmp/strncmp → 交叉引用找比较逻辑
→ 看加密函数:AES/DES/RC4/TEA/XTEA 的特征常量
3.3 识别常见算法
加密算法的特征常量——逆向中的"指纹":
| 算法 | 特征常量/特征操作 |
|---|---|
| AES | S盒 0x63,0x7c,0x77,0x7b...,轮常量 0x01,0x02,0x04... |
| DES | 初始置换表,S盒,P盒 |
| RC4 | 初始化循环 for(i=0;i<256;i++) S[i]=i,swap操作 |
| TEA/XTEA | 魔数 0x9E3779B9(delta),循环32次,+=/^=交替 |
| MD5 | 初始值 0x67452301,0xEFCDAB89...,64轮循环 |
| SHA1 | 初始值 0x67452301,0xEFCDAB89,0x98BADCFE...,80轮 |
| SHA256 | 初始值 0x6a09e667...,64轮,K常量表 |
| Base64 | 字符表 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ |
看到0x9E3779B9就是TEA/XTEA,看到0x67452301就是MD5/SHA——这是逆向的速判技巧。
3.4 读反编译代码的方法
核心原则:不要逐行读,先抓结构再填细节
1. 识别控制流
if-else → 看cmp/test后的分支
循环 → 看跳回指令,判断是for/while
switch → 跳表结构
2. 识别数据流
输入在哪?→ 追踪输入变量的使用
比较在哪?→ 找到flag判断点
中间做了什么?→ 加密/编码/变换
3. 识别函数功能
看参数和返回值 → 推断函数用途
看内部操作 → 确认推断
重命名函数 → 让代码可读
四、动态调试
4.1 什么时候需要动态调试
静态分析够用时不需要动态调试。以下情况必须动态:
1. 反编译代码看不懂 → 运行时观察变量值
2. 有反调试 → 需要绕过
3. 有自解密/加壳 → 需要脱壳后dump
4. 算法复杂 → 需要单步跟踪确认理解
5. 需要修改跳转 → 直接patch掉判断
4.2 GDB(Linux)
# 启动
gdb ./program
gdb -q ./program # 安静模式
# 断点
b main # 在main下断
b *0x401234 # 在地址下断
b func_name # 在函数下断
b file.c:42 # 在源码行下断
delete # 删所有断点
# 执行
r # 运行
r arg1 arg2 # 带参数运行
c # 继续执行
si # 单步进入(进函数)
ni # 单步越过(不进函数)
finish # 执行到当前函数返回
# 观察
info registers # 查看所有寄存器
p $rax # 打印寄存器
p/x $rax # 十六进制打印
p/s (char*)$rdi


2046

被折叠的 条评论
为什么被折叠?



