逆向入门经典题:从 IDA 反编译坑点到 Python 解题全拆解
遇到一类经典入门题型:程序通过字符串 ASCII 运算 + 多条件校验实现 "挑战 - 解答" 验证逻辑。这类题目看似复杂,核心却藏在 "IDA 反编译命名混淆" 和 "基础算术运算" 里。本文将以一道典型例题为例,从IDA 坑点修正 、逻辑还原 、解题公式推导 到Python 通用脚本编写,完整拆解逆向全流程,让你掌握这类题的核心解题思路。
原伪C代码:
c
int start()
{
size_t v0; // ecx
int v1; // eax
size_t i; // edx
printf("Please enter a challenge: ");
scanf("%s", &Str);
if ( strlen(&Str) < 6 )
return printf("The challenge must have at least 6 characters\n\r");
printf("Please enter the solution: ");
if ( (unsigned int)scanf("%u-%u-%u-%u", &dword_4030A1, &dword_4030A5, &dword_4030A9, &dword_4030AD) >= 4
&& (unsigned __int8)byte_4030B5 + (unsigned __int8)byte_4030B4 + (unsigned __int8)byte_4030B2 == dword_4030A1
&& dword_4030A5 == 1159790554
&& (unsigned __int8)byte_4030B3 * (unsigned __int8)Str == (unsigned int)(dword_4030A9 / 0xC48ui64) )
{
v0 = strlen(&Str);
v1 = 0;
for ( i = 0; i < v0; ++i )
v1 += (unsigned __int8)*(&Str + i);
if ( v1 == (dword_4030AD ^ 0x31337) - 123 )
dword_4030D1 = 1;
}
if ( dword_4030D1 )
return printf("Congratulations, you made it!\n\r");
else
return printf("Wrong :(\n\r");
}
一、逆向背景与初始分析
1.1 题目场景
目标程序功能为:输入 "Challenge(挑战字符串)" 和 "Solution(格式为 N1-N2-N3-N4 的数字串)",程序校验通过则输出 "Congratulations",否则输出 "Wrong"。我们需要逆向还原校验逻辑,编写脚本自动生成合法的 Challenge 和 Solution。
1.2 初始逆向环境
使用 IDA Pro 对 32 位 PE 程序反编译,初始得到的核心代码存在明显的 "变量命名混淆"------IDA 将连续的字符串字节标记为独立的全局变量(如byte_4030B2、byte_4030B3),这是解题的第一个 "坑",也是多数新手卡壳的关键。
二、核心逆向突破:修正 IDA 反编译的错误
2.1 IDA 的错误命名问题
IDA 反编译后,数据段显示如下 "独立变量":
.data:004030B1 Str[0] db 0 ; 输入字符串第1个字符
.data:004030B2 Str[1] db 0 ; IDA命名:byte_4030B2 → 字符串第2个字符
.data:004030B3 Str[2] db 0 ; IDA命名:byte_4030B3 → 字符串第3个字符
.data:004030B4 Str[3] db 0 ; IDA命名:byte_4030B4 → 字符串第4个字符
.data:004030B5 Str[4] db 0 ; IDA命名:byte_4030B5 → 字符串第5个字符
IDA 错误地将输入字符串 Str 的连续字节 拆解为独立的byte_xxx变量,导致反编译代码逻辑割裂。
2.2 修正后的数据结构
核心结论:byte_4030B2~B5并非独立常量,而是输入字符串Str的第 1~4 位字符(数组下标从 0 开始)。修正后,程序的核心数据结构瞬间清晰 ------ 所有byte_xxx变量本质是同一个字符串数组的元素。
三、还原程序核心逻辑
基于变量修正结果,我们将反编译代码还原为易读的 C 伪代码,并逐行解析校验逻辑:
3.1 完整 C 伪代码还原
c
// 全局变量:输入的挑战字符串(长度≥6)
char Str[100];
// 成功标志
int success = 0;
int start() {
// 1. 输入挑战字符串(长度≥6)
printf("Please enter a challenge: ");
scanf("%s", Str);
if (strlen(Str) < 6) {
printf("The challenge must have at least 6 characters\n\r");
return 0;
}
// 2. 输入解决方案:格式 数字-数字-数字-数字
unsigned int N1, N2, N3, N4;
printf("Please enter the solution: ");
if (scanf("%u-%u-%u-%u", &N1, &N2, &N3, &N4) >= 4) {
// ==================== 核心校验条件 ====================
// 条件1:N2 固定为 1159790554(硬编码常量)
if (N2 != 1159790554) goto fail;
// 条件2:N1 = 字符串第5位 + 第4位 + 第2位 ASCII之和(下标1、3、4)
if (N1 != (Str[4] + Str[3] + Str[1])) goto fail;
// 条件3:N3 = 字符串第1位 * 第3位 * 3144(0xC48=3144,十六进制转十进制)
if (N3 != (Str[0] * Str[2] * 3144)) goto fail;
// 条件4:字符串总ASCII和 = (N4 ^ 魔术数) - 123(^为异或运算)
int sum_ascii = 0;
for (int i=0; i<strlen(Str); i++) sum_ascii += Str[i];
if (sum_ascii != ((N4 ^ 0x31337) - 123)) goto fail;
// ======================================================
// 所有条件满足:标记成功
success = 1;
}
fail:
if (success)
printf("Congratulations, you made it!\n\r");
else
printf("Wrong :(\n\r");
return 0;
}
3.2 核心校验逻辑拆解
我们将校验条件提炼为 "解题唯一公式",这是后续编写脚本的核心依据:
| 参数 | 计算公式 | 核心说明 |
|---|---|---|
| Challenge | 任意字符串(长度≥6) | 可自定义,无固定值,如abcdef |
| N2 | 1159790554 | 硬编码固定值,无需计算 |
| N1 | Str[4] + Str[3] + Str[1] | 字符串第 5/4/2 位 ASCII 值之和 |
| N3 | Str[0] * Str[2] * 3144 | 字符串第 1/3 位 ASCII 值 × 固定常量 |
| N4 | (sum_ascii + 123) ^ 0x31337 | sum_ascii 为字符串所有字符 ASCII 和 |
关键补充:
- 字符串下标:Str [0] 是第 1 个字符,Str [1] 是第 2 个字符,以此类推;
- 异或运算(^):可逆运算,A ^ B = C → C ^ B = A,这是 N4 能反向计算的核心;
- 魔术数 0x31337:十六进制转十进制为 201527,是逆向中常见的 "彩蛋式魔术数"。
四、手动解题演示:从自定义字符串到合法 Solution
为了让你理解公式的落地方式,我们以最简单的 6 位字符串abcdef为例,手动计算合法的 Solution:
步骤 1:提取字符串 ASCII 值
| 字符位置 | 字符 | ASCII 值 |
|---|---|---|
| Str[0] | a | 97 |
| Str[1] | b | 98 |
| Str[2] | c | 99 |
| Str[3] | d | 100 |
| Str[4] | e | 101 |
| Str[5] | f | 102 |
步骤 2:逐一代入公式计算
- N2:直接取固定值 → 1159790554;
- N1:Str[4]+Str[3]+Str[1] = 101+100+98 = 299;
- N3:Str[0]×Str[2]×3144 = 97×99×3144 = 30191832;
- sum_ascii:97+98+99+100+101+102 = 597;
- N4:(597+123) ^ 0x31337 = 720 ^ 201527 = 201191;
步骤 3:最终答案
- Challenge:
abcdef - Solution:
299-1159790554-30191832-201191
五、Python 通用解题脚本:从 "手动计算" 到 "自动化解题"
手动计算仅适用于短字符串,实际 CTF 中我们需要编写通用脚本,支持任意长度≥6 的自定义字符串,自动生成合法 Solution。以下是脚本的完整实现与逐行解析:
5.1 完整 Python 脚本
python
def calculate_solution(challenge: str):
"""
输入挑战字符串(长度≥6),自动计算合法解决方案
:param challenge: 自定义的挑战字符串(长度≥6)
:return: 合法的Solution字符串(格式:N1-N2-N3-N4)
:raises ValueError: 字符串长度不足6时抛出异常
"""
# 第一步:校验输入字符串长度(程序强制要求≥6)
if len(challenge) < 6:
raise ValueError("挑战字符串长度必须≥6,请重新输入")
# 第二步:提取字符串前5个字符的ASCII值(核心计算仅需前5位)
# ord(c)将字符转为ASCII值,存储到列表s中
s = [ord(c) for c in challenge]
Str0, Str1, Str2, Str3, Str4 = s[0], s[1], s[2], s[3], s[4]
# 第三步:定义固定常量(从逆向还原的逻辑中提取)
N2 = 1159790554 # 硬编码固定值
MAGIC_NUMBER = 0x31337 # 异或魔术数(0x31337=201527)
C48_CONST = 3144 # 0xC48的十进制值,固定运算常量
# 第四步:按公式计算N1、N3、N4
# N1 = 第5位+第4位+第2位ASCII和(Str4=第5位,Str3=第4位,Str1=第2位)
N1 = Str4 + Str3 + Str1
# N3 = 第1位×第3位×3144(Str0=第1位,Str2=第3位)
N3 = Str0 * Str2 * C48_CONST
# 计算字符串总ASCII和(sum()直接求和列表s)
sum_ascii = sum(s)
# N4 = (总ASCII和 + 123) ^ 魔术数(异或运算用^实现)
N4 = (sum_ascii + 123) ^ MAGIC_NUMBER
# 第五步:输出计算过程与结果(提升脚本可读性)
print(f"[+] 挑战字符串: {challenge}")
print(f"[+] 前6位字符ASCII值: {s[:6]}...") # 仅展示前6位,兼容长字符串
print(f"[+] 字符串总ASCII和: {sum_ascii}")
print(f"[+] 合法Solution: {N1}-{N2}-{N3}-{N4}")
# 返回Solution字符串,方便后续直接使用
return f"{N1}-{N2}-{N3}-{N4}"
# 测试案例:验证脚本正确性
if __name__ == '__main__':
# 测试1:手动计算过的字符串abcdef(验证结果一致性)
calculate_solution("abcdef")
print("-" * 50) # 分隔符,提升输出可读性
# 测试2:自定义字符串ctf123(验证通用性)
calculate_solution("ctf123")
5.2 脚本核心逻辑解析
(1)输入校验(第 8-11 行)
程序强制要求 Challenge 长度≥6,因此脚本首先校验长度,不足时抛出ValueError,避免无效计算。
(2)ASCII 值提取(第 14-15 行)
使用列表推导式[ord(c) for c in challenge]将字符串转为 ASCII 值列表,后续所有计算均基于此列表,这是 "字符→数值" 的核心转换步骤。
(3)固定常量定义(第 18-21 行)
将逆向得到的硬编码值、魔术数、运算常量单独定义,便于后续维护(如魔术数变化时仅需修改此处)。
(4)核心公式计算(第 24-32 行)
- N1 计算 :严格对应逆向还原的
Str[4]+Str[3]+Str[1],变量命名Str4/Str3/Str1与逆向逻辑一致,降低理解成本; - N3 计算 :对应
Str[0]*Str[2]*3144,直接复用提取的 ASCII 值; - N4 计算 :先算总 ASCII 和,再按
(sum_ascii + 123) ^ 魔术数反向推导(程序逻辑是sum_ascii = (N4 ^ 魔术数) - 123,逆推即得此公式)。
(5)测试案例(第 39-46 行)
- 测试 1:使用手动计算过的
abcdef,验证脚本输出与手动结果一致; - 测试 2:使用自定义字符串
ctf123,验证脚本的通用性。
5.3 脚本运行结果
[+] 挑战字符串: abcdef
[+] 前6位字符ASCII值: [97, 98, 99, 100, 101, 102]...
[+] 字符串总ASCII和: 597
[+] 合法Solution: 299-1159790554-30191832-201191
--------------------------------------------------
[+] 挑战字符串: ctf123
[+] 前6位字符ASCII值: [99, 116, 102, 49, 50, 51]...
[+] 字符串总ASCII和: 460
[+] 合法Solution: 215-1159790554-31754232-201851
可以看到,脚本对abcdef的计算结果与手动计算完全一致,验证了脚本的正确性;对自定义字符串ctf123也能快速生成合法 Solution,体现了通用性。
六、验证:确保解题结果
逆向的最终目标是 "让程序输出通关提示",因此我们需要验证脚本生成的答案是否有效:
6.1 验证步骤
- 运行目标程序,输入 Challenge:
abcdef; - 输入 Solution:
299-1159790554-30191832-201191; - 程序输出
Congratulations, you made it!,验证通过。
6.2 验证原理
脚本的计算逻辑完全复刻了程序的校验逻辑 ------ 程序是 "校验 Solution 是否匹配 Challenge",而我们是 "根据 Challenge 反向计算 Solution",本质是 "正向校验" 与 "反向推导" 的互逆过程,因此结果必然合法。
七、逆向总结与核心要点
7.1 逆向核心坑点
IDA 将连续字符串字节标记为独立变量,是这类题的核心混淆点。解决思路:结合代码上下文(如运算逻辑、字符串操作函数),还原变量的真实含义,而非机械信任 IDA 的命名。
7.2 解题通用思路
这类 "字符串 + 算术运算" 的逆向题,解题逻辑高度统一:
- 还原程序的校验公式;
- 自定义满足前置条件的输入(如本题的 "长度≥6");
- 按公式反向计算输出值;
- 编写通用脚本自动化解题。
7.3 扩展思考
如果程序增加 "字符串长度固定为 8""字符范围限制为数字 / 字母" 等条件,只需在脚本中增加对应的校验逻辑即可 ------ 核心公式不变,仅需调整输入约束。
八、结语
本文从 IDA 反编译的坑点修正出发,完整还原了程序的校验逻辑,推导了解题公式,并编写了通用 Python 脚本。这类逆向入门题的核心并非 "复杂加密算法",而是 "逻辑还原" 与 "基础运算" 的结合。掌握 "变量含义还原→公式提取→脚本实现" 的三板斧,你就能轻松应对绝大多数同类题目。
逆向的本质是 "还原程序的真实意图",而非 "死磕反编译代码"------ 希望本文能让你理解:好的逆向,既要能看懂汇编,更要能 "穿透混淆,抓住核心逻辑"。