从二进制中解读程序逻辑
逆向工程(Reverse Engineering)是通过分析二进制可执行文件来理解程序逻辑的技术。在安全领域,逆向用于分析恶意软件、发现漏洞、破解保护机制。
| 架构 | 指令集 | 寄存器 | 调用约定 |
|---|---|---|---|
| x86 (32-bit) | CISC | EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP | cdecl/stdcall |
| x64 | CISC | RAX,RBX,...,R8-R15,RIP,RSP | System V AMD64 |
| ARM32 | RISC | R0-R12,SP,LR,PC | AAPCS |
| AArch64 | RISC | X0-X30,SP,PC | AAPCS64 |
# x64核心指令集
# 数据移动
mov rax, rbx # rbx → rax
mov rax, [rbx] # 内存[rbx] → rax
mov [rax], rbx # rbx → 内存[rax]
lea rax, [rbx+8] # rbx+8的地址 → rax (不访问内存)
# 算术运算
add rax, rbx # rax = rax + rbx
sub rax, rbx # rax = rax - rbx
imul rax, rbx # rax = rax * rbx
xor rax, rax # rax = 0 (清零的经典方式)
# 比较与跳转
cmp rax, rbx # 比较 rax 和 rbx
test rax, rax # 测试 rax 是否为0
je label # 相等跳转 (ZF=1)
jne label # 不等跳转 (ZF=0)
jl label # 小于跳转
jg label # 大于跳转
jmp label # 无条件跳转
# 函数调用
call func # push RIP; jmp func
ret # pop RIP; jmp RIP
# 栈操作
push rax # RSP -= 8; [RSP] = rax
pop rax # rax = [RSP]; RSP += 8
# 常见模式识别
# 字符串比较:
lea rdi, [str1] ; 第一个字符串
lea rsi, [str2] ; 第二个字符串
call strcmp ; 比较
test eax, eax ; 检查返回值
je success ; 相等=密码正确
// crackme.c - 简单的密码验证程序
#include <stdio.h>
#include <string.h>
int check_password(const char *input) {
// 方法1: 直接字符串比较
if (strcmp(input, "s3cr3t_p4ss") == 0) {
return 1;
}
return 0;
}
int check_password_v2(const char *input) {
// 方法2: 逐字符比较
const char *key = "\x73\x33\x63\x72\x33\x74"; // "s3cr3t"
for (int i = 0; i < 6; i++) {
if (input[i] != key[i]) return 0;
}
return 1;
}
int check_password_v3(const char *input) {
// 方法3: 算法验证
int sum = 0;
for (int i = 0; input[i]; i++) {
sum += (unsigned char)input[i] ^ 0x55;
}
return sum == 0x2A3; // 特定校验和
}
int main() {
char input[64];
printf("Enter password: ");
scanf("%63s", input);
if (check_password(input)) {
printf("✅ Access granted!\n");
} else {
printf("❌ Wrong password!\n");
}
return 0;
}
// 编译:
// gcc -o crackme crackme.c -fno-pie -no-pie -g
// -fno-pie: 禁用位置无关执行(方便调试)
# 启动GDB
gdb ./crackme
# 基本命令
(gdb) info functions # 列出所有函数
(gdb) disas check_password # 反汇编check_password函数
# 输出:
Dump of assembler code for function check_password:
0x00401176: push rbp
0x00401177: mov rbp,rsp
0x0040117a: sub rsp,0x10
0x0040117e: mov QWORD PTR [rbp-0x8],rdi
0x00401182: lea rsi,[0x402004] ← "s3cr3t_p4ss"地址
0x00401189: mov rdi,QWORD PTR [rbp-0x8]
0x0040118d: call 0x401030
0x00401192: test eax,eax ← 检查strcmp返回值
0x00401194: jne 0x40119c ← 不等则跳转(失败)
0x00401196: mov eax,0x1 ← 返回1(成功)
0x0040119b: jmp 0x4011a1
0x0040119c: mov eax,0x0 ← 返回0(失败)
# 设置断点
(gdb) break *0x00401192 # strcmp返回后
(gdb) break check_password # 函数入口
# 运行程序
(gdb) run
Enter password: test123
# 断点命中后检查
(gdb) x/s $rdi # 查看第一个参数
(gdb) x/s $rsi # 查看第二个参数(密码!)
# 方法1: 直接读取密码
(gdb) x/s 0x402004
0x402004: "s3cr3t_p4ss" ← 密码就在这里!
# 方法2: 修改跳转指令(绕过验证)
(gdb) set $eflags = $eflags | 0x40 # 设置ZF=1(模拟相等)
(gdb) continue
✅ Access granted!
# 方法3: 修改返回值
(gdb) break *0x00401194
(gdb) continue
(gdb) set $eax = 1 # 强制返回1
(gdb) continue
# Ghidra (NSA开源逆向框架)
# 1. 创建项目: File → New Project
# 2. 导入二进制: File → Import File → crackme
# 3. 分析: 自动分析(Analyzers全选)
# 4. 找到check_password函数(Symbol Tree → Functions)
# 5. 查看反编译的C伪代码:
// Ghidra反编译输出:
undefined8 check_password(char *param_1)
{
int iVar1;
iVar1 = strcmp(param_1, "s3cr3t_p4ss");
return (long)(iVar1 == 0);
}
// 密码直接暴露在字符串中!
# 6. 搜索字符串: Search → For Strings
# 7. 交叉引用: 右键 → References → Find References
# 逆向check_password_v3 (算法验证)
# Ghidra反编译:
int check_password_v3(char *input)
{
int sum = 0;
for (int i = 0; input[i] != '\0'; i++) {
sum += (byte)input[i] ^ 0x55;
}
return sum == 673; // 0x2A3
}
# 还原算法: 每个字符XOR 0x55后求和 = 673
# Python破解脚本:
target_sum = 673
xor_key = 0x55
# 方法1: 暴力搜索短密码
import itertools
import string
def check(password):
s = sum(ord(c) ^ xor_key for c in password)
return s == target_sum
for length in range(4, 12):
for combo in itertools.product(string.printable[:62], repeat=length):
pwd = ''.join(combo)
if check(pwd):
print(f"Found: {pwd}")
break
# 方法2: 构造满足条件的密码
# 需要字符XOR 0x55后求和 = 673
# 选择特定字符构造
chars = []
remaining = target_sum
while remaining > 0:
c = min(remaining, 0x7e ^ xor_key) # 可打印字符
chars.append(chr(c ^ xor_key))
remaining -= c
password = ''.join(chars)
print(f"Constructed password: {password}")
# 方法1: 使用GDB patch
# 将jne改为jmp (无条件跳到成功分支)
(gdb) set *(char*)0x00401194 = 0xEB # JMP指令操作码
(gdb) set *(char*)0x00401195 = 0x06 # 偏移量
# 方法2: 使用xxd修改
cp crackme crackme_patched
xxd crackme_patched | grep "75 0c" # 找到jne指令
# 0001194: 75 0c → 74 0c (jne→je, 翻转逻辑)
printf '\x74' | dd of=crackme_patched bs=1 seek=$((0x1194)) conv=notrunc
# 方法3: 使用Ghidra patch
# 右键指令 → Patch Instruction
# jne → je (翻转条件)
# 方法4: NOP掉验证调用
# 将call check_password替换为NOP
# 90 90 90 90 90 (5字节NOP)
printf '\x90\x90\x90\x90\x90' | dd of=crackme_patched bs=1 seek=$((0x1168)) conv=notrunc
# 然后确保eax=1:
printf '\xb8\x01\x00\x00\x00' | dd of=crackme_patched bs=1 seek=$((0x1168)) conv=notrunc
| 工具 | 类型 | 平台 | 特点 |
|---|---|---|---|
| Ghidra | 静态+反编译 | 跨平台 | 免费开源,反编译强 |
| IDA Pro | 静态+反编译 | 跨平台 | 行业标准,F5反编译 |
| GDB | 动态调试 | Linux | 命令行调试器 |
| x64dbg | 动态调试 | Windows | GUI调试器,插件丰富 |
| radare2 | 全功能 | 跨平台 | 命令行,脚本化强 |
| Cutter | GUI(r2) | 跨平台 | radare2的GUI前端 |
| Binary Ninja | 静态+反编译 | 跨平台 | API强大,商业工具 |
| Chef | 二进制分析 | Web | 在线反汇编器 |
# radare2 快速入门
r2 ./crackme
# 分析
[0x00401000]> aaa # 全自动分析
[0x00401000]> afl # 列出函数
[0x00401000]> pdf @sym.check_password # 反汇编函数
# 调试模式
r2 -d ./crackme
[0x7f...]> db 0x00401192 # 设置断点
[0x7f...]> dc # 继续执行
[0x7f...]> dr rax # 查看寄存器
[0x7f...]> ps @rsi # 查看字符串
# 常见反调试技术
# 1. IsDebuggerPresent
if (IsDebuggerPresent()) ExitProcess(0);
# 2. 检测硬件断点
CONTEXT ctx;
GetThreadContext(GetCurrentThread(), &ctx);
if (ctx.Dr0 != 0) ExitProcess(0); # 硬件断点检测
# 3. 时间检测
DWORD t1 = GetTickCount();
// ... 关键代码 ...
DWORD t2 = GetTickCount();
if (t2 - t1 > 100) ExitProcess(0); # 调试导致延迟
# 4. INT 3检测 (软件断点插入0xCC)
if (*(BYTE*)func_addr == 0xCC) ExitProcess(0);
# 绕过方法:
# GDB: set *(int*)IsDebuggerPresent = 1 # 修改返回值
# 或: 在检测前设置断点,修改跳转
# 或: 使用ScyllaHide插件(x64dbg)自动绕过
xor eax, eax比mov eax, 0更常用?有什么优势?# 1. 简单比较型
# 程序直接与硬编码字符串比较
# 解法: strings + grep 或 GDB断点读取
# 2. 算法还原型
# 程序对输入做复杂变换后比较
# 解法: Ghidra反编译 → 还原算法 → 反向求解
# 3. 加壳脱壳型
# UPX/Themida/VMProtect加壳
# 解法: 检测壳类型 → 脱壳 → 正常分析
# UPX脱壳
upx -d packed_crackme.exe -o unpacked.exe
# 4. 反调试型
# 包含IsDebuggerPresent等反调试
# 解法: Patch掉检测或使用ScyllaHide
# 5. 简单加密型
# 输入经过XOR/ROT/Base64后比较
# 解法: 提取密钥和算法 → 解密
# XOR解密示例
python3 << 'EOF'
# 已知: 输入 XOR 0x42 = 比较值
encrypted = [0x2a, 0x1c, 0x0e, 0x2e, 0x1a, 0x0c]
key = 0x42
plaintext = ''.join(chr(b ^ key) for b in encrypted)
print(f"Password: {plaintext}")
EOF
# Ghidra Python脚本 (在Ghidra脚本管理器中运行)
# 自动查找所有字符串比较
# @category CTF
from ghidra.program.model.symbol import SymbolType
listing = currentProgram.getListing()
fm = currentProgram.getFunctionManager()
for func in fm.getFunctions(True):
for instr in listing.getInstructions(func.getBody(), True):
mnemonic = instr.getMnemonicString()
if mnemonic == "CALL":
for ref in instr.getReferencesFrom():
if "strcmp" in str(ref) or "strncmp" in str(ref):
print(f"String compare at {instr.getAddress()} in {func.getName()}")
# 自动标记XOR操作
for func in fm.getFunctions(True):
for instr in listing.getInstructions(func.getBody(), True):
mnemonic = instr.getMnemonicString()
if mnemonic == "XOR":
ops = instr.getOpObjects(1)
if ops and str(ops[0]) != str(instr.getOpObjects(0)[0]):
print(f"XOR with key at {instr.getAddress()}: {instr}")