初次接触这道题时,我注意到题目描述极其简洁,仅提示"请找出答案",这种开放性反而增加了挑战性。作为逆向工程领域的常见题型,这类题目往往考察选手对程序逻辑的深度理解能力。我们先从基础分析入手:
使用IDA Pro加载程序后,我习惯先查看字符串窗口(Shift+F12),但这次发现没有明显提示字符串,这提示我们可能需要更深入的分析。通过查看函数列表(Ctrl+F12),我锁定了main和fun两个关键函数。
在main函数中,程序结构非常清晰 - 一个简单的无限循环不断调用fun函数。这种设计在CTF题目中很常见,目的是让选手专注于核心验证逻辑的分析。按下F5生成伪代码后,我立即注意到几个关键特征:
通过分析fun函数的伪代码,我将加密过程归纳为四个主要阶段:
数据采集阶段:
预处理阶段:
加密变换阶段:
c复制for (i = 0; i < v9; ++i) {
v7[i] = v4[i];
}
for (j = 0; j < v9 - 1; ++j) {
v5 = v7[j] ^ v7[j + 1];
v7[j] = v5 ^ 0x53;
}
这个双重变换是加密的核心,通过相邻字符异或后再与固定值0x53异或
动态密钥阶段:
c复制for (k = 1; k < v9; ++k) {
v3[k] ^= v4[(k - 1) % 9] ^ v4[(k - 1) % 7] ^ v3[k - 1];
}
这种密钥生成方式使得每个加密步骤都依赖于之前的状态
通过伪代码分析,我们可以建立以下数学模型:
设:
加密过程可表示为:
要逆向这个加密过程,我们需要解决几个关键问题:
确定比对目标值:
通过IDA的Hex View功能,我在.data段找到了疑似比对数据的字节数组:
python复制target = [0x47, 0x64, 0x18, 0x33, 0x10, 0x61, 0x51, 0x3B, 0x35, 0x5E, 0x63, 0x64, 0x1D, 0x74, 0x19, 0x4D, 0x60, 0x1B, 0x69]
逆向推导公式:
利用异或运算的自反性(A⊕B=C ⇒ C⊕B=A),我们可以从最终比对值反推原始输入:
code复制v7_new[j] = target[j] ⊕ v3[j]
v4[j+1] = v7_new[j] ⊕ 0x53
密钥流生成:
由于密钥流生成是确定性的,我们可以完全复现加密时的密钥生成过程
基于上述分析,我编写了以下解密脚本:
python复制def solve_flag():
# 初始化参数
v9 = 19
v4 = [0] * v9
v4[0] = ord('H') # 固定起始字符
# 初始密钥流与目标数据
v3 = list(b"a7shw9o10e63nfi19dk")
target = [71, 100, 24, 51, 16, 97, 81, 59, 53, 94, 99, 100, 29, 116, 25, 77, 96, 27, 105]
# 核心解密循环
for i in range(18):
# 计算当前字符
v4[i + 1] = target[i] ^ v3[i] ^ 0x53
# 更新下一轮密钥
k = i + 1
if k <= 18:
v3[k] = v3[k] ^ v4[(k - 1) % 9] ^ v4[(k - 1) % 7] ^ v3[k - 1]
# 大小写转换处理
for ii in range(0, v9, 2):
# 偶数位大写转小写
if 64 < v4[ii] <= 90:
v4[ii] += 32
# 奇数位小写转大写
if ii + 1 < v9:
if 96 < v4[ii + 1] <= 122:
v4[ii + 1] -= 32
# 输出最终flag
flag_content = "".join(chr(c) for c in v4)
print(f"flag{{{flag_content}}}")
if __name__ == "__main__":
solve_flag()
在脚本实现过程中,有几个容易出错的细节需要特别注意:
密钥流更新时机:
必须在计算完当前字符后立即更新密钥,因为下一个字符的解密依赖于更新后的密钥状态
模运算的边界条件:
(k-1)%9和(k-1)%7的计算要确保不会越界,这是保证密钥正确生成的关键
大小写转换逻辑:
程序最后的大小写转换是基于字符位置的奇偶性,而不是字符本身的属性
在实际解题过程中,我遇到了几个典型的错误,值得分享:
密钥流生成顺序错误:
最初我尝试先生成完整的密钥流再解密,结果失败。正确的做法应该是交替进行字符解密和密钥更新。
大小写转换理解偏差:
误以为程序是根据字符本身的大小写状态进行转换,实际上它是基于字符位置的奇偶性。
异或运算优先级:
在Python中,异或运算的优先级低于比较运算符,必要时应使用括号明确运算顺序。
对于这类复杂的加密逻辑,我总结了几点有效的调试方法:
分阶段验证:
将解密过程拆分为多个阶段,每个阶段输出中间结果进行验证
与原始程序对比:
编写正向加密函数,验证逆向逻辑的正确性
使用断点调试:
在IDA中设置断点,观察程序实际执行时的内存状态
单元测试:
对解密脚本中的关键函数编写测试用例,确保每个组件正确工作
这道题目虽然规模不大,但包含了许多经典的逆向工程元素:
多层加密变换:
反逆向技巧:
隐蔽的flag设计:
最终flag"hUaN-Y1N5-cAn5-5a1!"是中文"欢迎参赛"的Leetspeak变体,这种设计增加了题目的趣味性
对于想要进一步提高逆向能力的同学,我建议可以从以下几个方面入手:
在实际的逆向工程工作中,类似的加密逻辑经常出现在软件保护、授权验证等场景。理解这些基础模式,能够帮助我们更好地分析复杂的商业软件保护机制。