目录
- 阶段6:动态验证
题目信息
| 项 | 值 |
| 文件名 | challenge.exe |
| 文件大小 | 79,289 字节 |
| MD5 | 7595baf148c7add9afc13492f80aa500 |
| SHA256 | 4bcaaa91b232baaaa987b62ee87a7a8c760558953cfd86c2940588542d7f83cb |
| 架构 | x86_64 (64位) |
| 编译器 | MinGW GCC |
| 难度 | 入门级 |
| 考察点 | 栈分析、XOR加密、小端序、内存重叠 |
环境准备
必备工具
- IDA Pro 7.x+ - 反汇编与反编译
- x64dbg - 动态调试(可选)
- Python 3.x - 编写解密脚本
- Windows环境 - 运行验证
IDA初始配置(新手必看)
1. 打开IDA Pro,选择"New"
2. 选择 challenge.exe
3. 弹出对话框选择"PE Executable"
4. 勾选"Load resources"、"Create FLIRT signatures"
5. 点击OK,等待自动分析完成(约10-30秒)
6. 按空格键切换到图形视图/文本视图
详细分析流程
阶段1:文件指纹与初步分析
1.1 文件类型识别
# 用file命令查看(或使用DIE/PEiD)
file challenge.exe
challenge.exe: PE32+ executable (console) x86-64, for MS Windows
1.2 字符串快速筛查
# 用strings命令提取可打印字符串
strings challenge.exe | findstr -i flag
关键字符串定位:
| 地址 | 字符串 | 含义 |
| 0x14000a000 | "Enter flag: " | 输入提示 |
| 0x14000a00d | "Wrong length.\n" | 长度错误提示 |
| 0x14000a01b | "Incorrect.\n" | 验证失败提示 |
| 0x14000a028 | "Correct! Here is your flag: %s\n" | 成功提示 |
💡 逆向技巧: 字符串是逆向分析的"路标",永远从字符串开始找交叉引用!
阶段2:IDA静态分析入门
2.1 定位主函数(3种方法)
方法1: 通过字符串交叉引用
1. Shift+F12 打开字符串窗口
2. 找到 "Enter flag: "
3. Ctrl+X 查看交叉引用(Xrefs to)
4. 双击进入引用位置 → 直接到达main函数!
方法2: 查看函数列表
1. Shift+F4 打开函数窗口
2. 按Ctrl+F搜索 "main"
3. 找到地址 0x1400014a4 的 main 函数
方法3: 从入口点跟踪
1. 按Ctrl+E跳转到入口点
2. 入口点会调用 __tmainCRTStartup
3. 最终调用 main 函数
阶段3:主函数深度解析
3.1 栈帧结构分析(关键!)
进入main函数(0x1400014a4),按F5反编译,得到伪代码。
栈内存布局:
地址 变量名 大小 用途
[rbp-B0h] Buffer 128字节 用户输入缓冲区
[rbp-30h] v6 30字节 加密的目标数组 ✨
[rbp-11h] v7 1字节 临时计算变量
[rbp-10h] v8 4字节 flag长度 = 30
[rbp-Ch] i 4字节 循环计数器
[rbp-8h] v10 8字节 输入字符串实际长度
3.2 逐行分析伪代码
/* 0x1400014a4 */ int __fastcall main(int argc, const char **argv, const char **envp)
{
/* 0x1400014af */ _main(argc, argv, envp); // GCC初始化
// ⚠️ 数据初始化:这里是第一个坑!
// 第一行:复制24字节到 v6[0] - v6[23]
/* 0x1400014be */ qmemcpy(v6, "35>0$/2#2/,6+08*>=282>,&", 24);
// ⚠️ 超级大坑在这里!!
// 第二行:从 v6[22] 开始写入8字节!!
// 这意味着 v6[22], v6[23] 会被覆盖!!
/* 0x1400014e8 */ *(_QWORD *)&v6[22] = 0xDA626F696F38262CuLL;
/* 0x1400014ec */ v8 = 30; // flag长度固定为30!
/* 0x1400014f6 */ printf("Enter flag: ");
// 读取用户输入
/* 0x14000150e */ v3 = __acrt_iob_func(0); // stdin
/* 0x140001525 */ if (!fgets_0(Buffer, 128, v3))
/* 0x14000152f */ return 0;
// 处理换行符
/* 0x140001548 */ v10 = strlen(Buffer);
/* 0x140001551 */ if (v10)
{
// 去掉fgets读入的换行符
/* 0x140001565 */ if (Buffer[v10 - 1] == '\n')
/* 0x14000156f */ Buffer[--v10] = 0;
}
// 第一重验证:长度检查
/* 0x140001580 */ if (v8 == v10) // 必须等于30
{
// 第二重验证:逐字节加密比较
/* 0x14000159b */ for (i = 0; i < v8; ++i)
{
/* 0x1400015b1 */ v7 = Buffer[i]; // 取输入字符
/* 0x1400015b4 */ v7 += 3; // 第一步:字符 + 3
/* 0x1400015b8 */ v7 ^= 0x5Au; // 第二步:异或 0x5A
/* 0x1400015c9 */ if (v7 != v6[i]) // 比较
{
/* 0x1400015d5 */ printf("Incorrect.\n");
/* 0x1400015df */ return 0;
}
}
/* 0x1400015ed */ printf("Correct! Here is your flag: %s\n", Buffer);
/* 0x140001606 */ return 0;
}
else
{
/* 0x14000158f */ printf("Wrong length.\n");
/* 0x140001594 */ return 0;
}
}
3.3 内存覆盖可视化
🎯 最关键的发现! 90%的新手会卡在这里!
初始化前的v6数组(30字节):
索引: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
└──────────────────────────────┘
qmemcpy复制的24字节 (0-23)
执行第二条赋值后(注意!):
索引: 0 1 2 3 ... 20 21 22 23 24 25 26 27 28 29
└───────────────────────────┘
这里写入了8字节的QWORD数据!
覆盖了原来的22,23位置!!
❌ 错误做法: 直接把24字节字符串解密
✅ 正确做法: 解密前必须先处理覆盖!
阶段4:加密算法逆向推导
4.1 正向加密流程
输入字符 Buffer[i]
│
▼
+ 3 (0x1400015b4)
│
▼
XOR 0x5A (0x1400015b8)
│
▼
与 v6[i] 比较
数学表达式:
v6[i] = (Buffer[i] + 3) XOR 0x5A
4.2 逆向推导(代数法)
已知:C = (P + 3) ^ K,其中 K = 0x5A
求P(明文):
步骤1: 两边同时 XOR K
C ^ K = [(P + 3) ^ K] ^ K
C ^ K = P + 3 (因为 X ^ K ^ K = X)
步骤2: 两边减3
(C ^ K) - 3 = P
最终解密公式:
P = (C ^ 0x5A) - 3
💡 数学技巧: XOR是自逆运算,异或两次回到原值!
阶段5:解密脚本编写与调试
5.1 小端序处理(第二个坑!)
还记得这个赋值吗?
*(_QWORD *)&v6[22] = 0xDA626F696F38262CuLL;
x86/x64是小端序架构! 数据在内存中是反过来存的!
数值: 0x DA 62 6F 69 6F 38 26 2C
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ └── 最低字节 → 内存最低地址 v6[22]
│ │ │ │ │ │ └───── v6[23]
│ │ │ │ │ └──────── v6[24]
│ │ │ │ └─────────── v6[25]
│ │ │ └────────────── v6[26]
│ │ └───────────────── v6[27]
│ └──────────────────── v6[28]
└─────────────────────── 最高字节 → 内存最高地址 v6[29]
内存中实际字节顺序:
地址: v6[22] v6[23] v6[24] v6[25] v6[26] v6[27] v6[28] v6[29]
值: 0x2C 0x26 0x38 0x6F 0x69 0x6F 0x62 0xDA
5.2 完整解密脚本
def decrypt_flag():
# 初始化30字节数组
encrypted = [0] * 30
# 第一步:先复制前24字节
# 来源: qmemcpy(v6, "35>0$/2#2/,6+08*>=282>,&", 24);
first_part = b"35>0$/2#2/,6+08*>=282>,&"
for i in range(24):
encrypted[i] = first_part[i]
print("第一次初始化后的22-23字节:")
print(f" [22] = 0x{encrypted[22]:02X} = {chr(encrypted[22])}")
print(f" [23] = 0x{encrypted[23]:02X} = {chr(encrypted[23])}")
print()
# 第二步:处理QWORD覆盖!(从索引22开始,共8字节)
# 0xDA626F696F38262C 小端序 → 字节顺序反过来
overwrite = [0x2C, 0x26, 0x38, 0x6F, 0x69, 0x6F, 0x62, 0xDA]
for i in range(8):
encrypted[22 + i] = overwrite[i]
print("QWORD覆盖后的22-29字节:")
for i in range(8):
print(f" [{22+i}] = 0x{encrypted[22+i]:02X}")
print()
# 解密每个字符
flag = []
for i, c in enumerate(encrypted):
plain = (c ^ 0x5A) - 3
flag.append(chr(plain))
print(f"[{i:2d}] cipher=0x{c:02X} → (0x{c:02X} ^ 0x5A) - 3 = 0x{plain:02X} = '{chr(plain)}'")
print()
result = ''.join(flag)
print(f"FLAG: {result}")
print(f"LENGTH: {len(result)}")
return result
if __name__ == "__main__":
decrypt_flag()
5.3 运行脚本并验证输出
第一次初始化后的22-23字节:
[22] = 0x26 = &
[23] = 0x00 =
QWORD覆盖后的22-29字节:
[22] = 0x2C
[23] = 0x26
[24] = 0x38
[25] = 0x6F
[26] = 0x69
[27] = 0x6F
[28] = 0x62
[29] = 0xDA
[ 0] cipher=0x33 → (0x33 ^ 0x5A) - 3 = 0x66 = 'f'
[ 1] cipher=0x35 → (0x35 ^ 0x5A) - 3 = 0x6C = 'l'
[ 2] cipher=0x3E → (0x3E ^ 0x5A) - 3 = 0x61 = 'a'
[ 3] cipher=0x30 → (0x30 ^ 0x5A) - 3 = 0x67 = 'g'
...
[29] cipher=0xDA → (0xDA ^ 0x5A) - 3 = 0x7D = '}'
FLAG: flag{reversing_made_easy_2025}
LENGTH: 30
阶段6:动态验证
6.1 命令行验证
Write-Output "flag{reversing_made_easy_2025}" | .\challenge.exe
预期输出:
Enter flag: Correct! Here is your flag: flag{reversing_made_easy_2025}
6.2 x64dbg动态调试验证(进阶)
1. 用x64dbg打开challenge.exe
2. 按Ctrl+G,输入 0x1400015C9 (比较指令处)
3. F9运行
4. 输入flag,程序断在比较处
5. 查看AL(v7)和v6[i]是否相等
6. 确认每个字符都通过验证
常见坑点与避坑指南
| 坑点 | 中招表现 | 如何避免 |
| 内存重叠覆盖 | 解密到最后4个字符乱码 | 仔细看反编译,注意指针偏移 |
| 小端序搞反 | 后半段完全乱码 | 牢记x86/x64都是小端! |
| 顺序搞反:先减3再XOR | 全乱码 | 代数推导,不要猜顺序! |
| 忘记去掉换行符 | 提示Wrong length | fgets会读入换行符! |
❌ 典型错误解密:
flag{reversing_made_easysy_202(未处理覆盖)✅ 正确解密结果:
flag{reversing_made_easy_2025}
进阶技巧总结
1. 栈分析技巧
IDA快捷键:
Ctrl+K → 查看栈帧
Alt+M → 内存映射
D → 切换数据显示格式
2. 快速解密思路
遇到逐字符加密比较的程序:
1. 下断点在比较处 (0x1400015C9)
2. 随便输入30个字符
3. 运行,每次断下时看AL寄存器的值
4. AL就是正确的加密后的值
5. 收集30字节后统一解密
→ 这叫"爆破断点法",不用分析算法也能解!
3. 内存特征识别
凡是看到:
for (i=0; i<len; i++) {
buf[i] ^= CONSTANT;
buf[i] += / -= x;
}
→ 100% 是简单替换加密,直接逆运算即可
最终FLAG
flag{reversing_made_easy_2025}
后记
这是一道非常经典的CTF逆向入门题,麻雀虽小五脏俱全,涵盖了:
- ✓ IDA基本操作
- ✓ 栈帧分析
- ✓ 简单密码学
- ✓ 内存模型理解
- ✓ 大小端序
强烈建议新手反复做3遍,把每个细节吃透!