🔧 逆向工程 — 破解简单CrackMe

从二进制中解读程序逻辑

📖 逆向工程基础

逆向工程(Reverse Engineering)是通过分析二进制可执行文件来理解程序逻辑的技术。在安全领域,逆向用于分析恶意软件、发现漏洞、破解保护机制。

逆向工程分析层次: 高层: 源代码级 (C/C++/Rust) ↓ 编译 中层: 中间表示 (IR/SSA) ↓ 优化 低层: 汇编代码 (x86/ARM) ↓ 汇编 底层: 机器码 (0x90 0x90 0x90...) ↓ 执行 硬件: CPU执行 逆向 = 从底层往上推导 目标: 恢复出程序的逻辑和算法
架构指令集寄存器调用约定
x86 (32-bit)CISCEAX,EBX,ECX,EDX,ESI,EDI,EBP,ESPcdecl/stdcall
x64CISCRAX,RBX,...,R8-R15,RIP,RSPSystem V AMD64
ARM32RISCR0-R12,SP,LR,PCAAPCS
AArch64RISCX0-X30,SP,PCAAPCS64

🔬 x86/x64汇编速成

# 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实战

创建测试CrackMe

// 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
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静态分析

# 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}")

Patch二进制(修改程序行为)

# 方法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动态调试WindowsGUI调试器,插件丰富
radare2全功能跨平台命令行,脚本化强
CutterGUI(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)自动绕过
破解简单CrackMe — 掌握逆向工程的基本技能!你能使用GDB动态调试、Ghidra静态分析、还原算法逻辑,并Patch二进制修改程序行为。
命令已验证:gcc / gdb / r2 / xxd / objdump — 所有命令在Linux x64环境测试通过
思考题:
  1. 为什么xor eax, eaxmov eax, 0更常用?有什么优势?
  2. 如何通过修改一个字节将jne变为je?这翻转了什么逻辑?
  3. 如果密码不是明文存储而是算法验证,逆向的思路有什么不同?
  4. 反调试技术的时间检测如何绕过?原理是什么?

📚 延伸阅读

🔧 CTF逆向实战技巧

常见CTF逆向题型

# 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脚本自动化

# 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}")
参考资料:Practical Reverse Analysis(Dennis Yurichev) | Ghidra Documentation | GDB Manual | x86-64 ABI | RPISEC Malware Analysis Course