-
- 前置知识
- 正文
-
- 一、IDA静态分析
-
- [1、密码学类(来自 ADVAPI32.dll)](#1、密码学类(来自 ADVAPI32.dll))
- [2、图形界面类(来自 USER32.dll)](#2、图形界面类(来自 USER32.dll))
- 3、关键字符串
- 初步判断
- [4、找到程序的主函数 WinMain](#4、找到程序的主函数 WinMain)
-
- [(1) 什么是 WinMain?](#(1) 什么是 WinMain?)
- [(2) 怎么找 WinMain?](#(2) 怎么找 WinMain?)
- [5、找到窗口过程函数(WndProc)------ 程序的核心](#5、找到窗口过程函数(WndProc)—— 程序的核心)
-
- [(1) 什么是窗口过程?](#(1) 什么是窗口过程?)
- (2)怎么找到它?
- 6、找到真正的验证逻辑
-
- [(1) 验证发生在哪里?](#(1) 验证发生在哪里?)
- [(2) 看懂验证逻辑(汇编级别)](#(2) 看懂验证逻辑(汇编级别))
- [7、找到 MD5 算法(确定是 MD5 不是其他哈希)](#7、找到 MD5 算法(确定是 MD5 不是其他哈希))
-
- [(1) 怎么确定是 MD5?](#(1) 怎么确定是 MD5?)
- (2)怎么验证?
- [8、找到 XOR 加密函数](#8、找到 XOR 加密函数)
- 得到md5、xor核心公式
- 二、编写python脚本
-
- [1、XOR 解密得到预期 MD5](#1、XOR 解密得到预期 MD5)
- [2、找点击序列(与预期 MD5 值碰撞)](#2、找点击序列(与预期 MD5 值碰撞))
- 3、用算出的序列与加密的flag字符串进行XOR运算,解密flag
前置知识
正文
一、IDA静态分析
把exe文件放入IDA中, Shift+F12 会看到程序调用了很多 Windows API 函数。重点圈出以下几类:
1、密码学类(来自 ADVAPI32.dll)
| 函数名 | 中文含义 |
|---|---|
CryptAcquireContextA |
初始化加密服务 |
CryptCreateHash |
创建哈希对象 |
CryptHashData |
往哈希对象里喂数据 |
CryptGetHashParam |
取出哈希结果 |
CryptDestroyHash |
销毁哈希对象 |
看到这些说明程序算了某种哈希值,具体是什么哈希后面再确定。

2、图形界面类(来自 USER32.dll)
| 函数名 | 中文含义 |
|---|---|
CreateWindowExA |
创建窗口 |
MessageBoxA |
弹窗 |
SetWindowTextA |
修改窗口标题文字 |
![]() |
3、关键字符串
除此之外,还可以在Strings窗口里看到以下字符:
| 地址 | 字符串内容 | 猜测 |
|---|---|---|
0x423024 |
^_^ |
可能是弹窗标题 |
0x423028 |
flag{} |
这就是我们想要的东西! |
0x423030 |
W^RTI_.miF.n_.lW[TL |
乱码,很可能是加密后的数据 |
0x42304C |
0kk\d1a`55k222k2a776jbfgd`06cjjb` |
乱码,很可能是加密后的数据 |
0x423FDC |
Are you kidding me? |
错误提示信息 |

初步判断
这是一个 Windows GUI 程序,用了哈希 + 窗口消息来处理逻辑。
4、找到程序的主函数 WinMain
(1) 什么是 WinMain?
Windows 的 GUI 程序不像控制台程序有 main()。GUI 程序入口叫 WinMain。
(2) 怎么找 WinMain?
方法 1 :在 IDA 左侧的 Functions Window (函数列表窗口,如果没显示就菜单 View → Open Subviews → Functions)里按 Ctrl+F 搜索 WinMain,双击即可。

方法 2 :按 G 键输入 WinMain 回车(如果反编译过了,也可以用符号名跳转)。
找到 WinMain 之后,你可以按 F5 (Hex-Rays 反编译)查看 C 语言伪代码。


WinMain 函数的逻辑可以通俗地理解为:
1. 创建主窗口
2. 进入死循环,不断接收用户的鼠标/键盘消息
3. 每条消息交给"窗口过程函数"处理
关键代码在地址 0x4010C8 附近调用了 sub_401023(注册窗口类),函数里设定了 窗口过程函数 (WndProc)的地址。

5、找到窗口过程函数(WndProc)------ 程序的核心
(1) 什么是窗口过程?
当你在窗口上点鼠标、按键,Windows 会给这个窗口发"消息"。窗口过程就是一个巨大无比的 switch-case,对每种消息分别做出反应。
(2)怎么找到它?
在 WinMain 中有一个函数叫 sub_4011D0(地址 0x4011D0),它会注册窗口类。在这个函数里,有一行:
c
v2.lpfnWndProc = (WNDPROC)JUMPOUT_w;

双击进去JUMPOUT_w,就是地址0x401640:

在 0x401640 处,从汇编代码中找到跳转表 (jump table),它在 0x401B20 附近有一个 switch 语句。

最直接的方式:在反汇编窗口中搜索 jpt_401B62 或者直接跳到 0x401B20。
按 G → 输入 0x401B20 → 回车 → 再按 F5 (这个函数能反编译)。

伪代码长这样:
c
switch (a2) {
case 272: // 窗口初始化,不做任何事
return 1;
case 273: // 按钮点击,关闭窗口
EndDialog(hDlg, n2);
return 1;
case 514: // 某类鼠标操作 → 记录字符 '1'
byte_428C54[index++] = '1';
return 0;
case 517: // 某类鼠标操作 → 记录字符 '3'
byte_428C54[index++] = '3';
return 0;
case 520: // 某类鼠标操作 → 记录字符 '2'
byte_428C54[index++] = '2';
return 0;
}
怎么读懂这段代码?
switch(a2)里的a2是 Windows 发给窗口的"消息编号"。每个数字代表一种事件:
- 272 = 窗口刚创建
- 273 = 点了按钮
- 514 / 517 / 520 = 3 种不同的鼠标操作(你不需要知道具体是按下还是松开,知道有 3 种就够了)
核心结论:
- 程序在全局数组
byte_428C54中按顺序记录你的鼠标操作- 操作 A → 记
'1',操作 B → 记'3',操作 C → 记'2'- 所以你的点击序列只由
'1'、'2'、'3'这三个字符组成
所以用户的操作序列(比如"左键、双击、右键、右键、双击、左键")会被记录为字符串 "123321",
存在全局变量 byte_428C54 中。
6、找到真正的验证逻辑
(1) 验证发生在哪里?
在 WndProc(0x401640)的后面部分(约 0x401900 开始),有一段代码是在每次收到某个消息后执行验证。
具体触发的消息编号不那么重要,你只要知道:用户点击一定次数后,程序会自动开始验证。
(2) 看懂验证逻辑(汇编级别)
跳到地址 0x401900(G → 0x401900),这段汇编代码的通俗翻译如下:

阶段 A --- 解密预期 MD5 值:
asm
1. 从内存中取出加密后的字符串 "0kk`d1a...06cjjb",复制到栈上
0x40194E: mov esi, offset "0kk`d1a`55k222k2a776jbfgd`06cjjb"
0x401959: rep movsd ; 复制 32 字节

2. 取出 XOR 密钥:两个字符 "SS"(0x53 0x53)
0x40196E: mov cx, word_423048 ; cx = 0x5353,即 "SS"


可以用编码转换工具理解一下:

3. 调用 XOR 解密(sub_401005 就是 sub_401590 的跳板)
0x4019BB: call sub_401005 ; XOR(加密字符串, "SS", 长度)
现在栈上的字符串变成了预期的 MD5 哈希值!

阶段 B --- 对用户的点击序列做 MD5 并比较:
asm
4. 把用户的点击序列做 MD5(sub_40101E 就是 sub_4013A0 的跳板)
0x401930: call sub_40101E ; MD5(点击序列)

; 5. 比较 MD5(点击序列) 是否等于解密出来的预期 MD5
0x4019CF: call __strcmpi
`
;6. 如果不相等 → 跳到"失败处理"
0x4019D9: jnz 失败位置

可以双击进loc_401A37:

如果失败 :程序把窗口标题设置为
"flag{}",弹出"Are you kidding me?",然后程序退出。
阶段 C --- 如果匹配,用点击序列 XOR 解密 flag:
asm
; 7. 用原始点击序列 XOR 解密存储的 flag
0x401A0F: call sub_401005 ; XOR(加密flag, 点击序列, 长度)
; 8. MessageBox 弹出解密后的 flag!
0x401A28: call MessageBoxA

7、找到 MD5 算法(确定是 MD5 不是其他哈希)
(1) 怎么确定是 MD5?
前面说了 sub_4013A0 是计算哈希的函数。跳过去(G → 0x4013A0 → F5)。

你会看到这行:
c
CryptCreateHash(phProv, 0x8003u, 0, 0, &phHash);
这里的 0x8003 是什么?它是 Windows 定义的常量 CALG_MD5。

可以在网上搜 "CALG_MD5 value" 验证:0x8003 = 32771 = CALG_MD5。
还有以下口诀:
| 宏名 | 十六进制 | 十进制 |
|---|---|---|
| CALG_MD2 | 0x8001 | 32769 |
| CALG_MD4 | 0x8002 | 32770 |
| CALG_MD5 | 0x8003 | 32771 |
| CALG_SHA1 | 0x8004 | 32772 |
所以这个函数就是 MD5 哈希。
(2)怎么验证?
在反汇编窗口里跳到 0x4013EB,你会看到:

asm
push 8003h ; Algid ← 这里明确推入 0x8003
8、找到 XOR 加密函数
sub_401590(地址 0x401590)是 XOR 加密/解密函数。按 G → 0x401590 → F5:

c
for (i = 0; i < len; i++)
buf[i] ^= key[i % strlen(key)];
就是把数据逐字节和密钥(循环使用)做 XOR。
XOR 的特性:加密和解密是同一个操作。
A XOR B XOR B = A。

得到md5、xor核心公式
① 预期 MD5 = "0kk`d1a`55k222k2a776jbfgd`06cjjb" XOR "SS"
② 需要找到 S,使得 MD5(S) = 预期 MD5
③ 正确 flag = "W^RTI_.miF.n_.lW[TL" XOR S
二、编写python脚本
1、XOR 解密得到预期 MD5
python
# 从 IDA 中提取的加密字符串(密文)
encrypted = "0kk`d1a`55k222k2a776jbfgd`06cjjb"
# 密钥字符串 "SS" 对应上文 0x5353
key = "SS"
# 用于存放解密完成后的明文结果
result = ""
# enumerate:同时拿到索引i 和 当前字符c,遍历整个密文字符串
for i, c in enumerate(encrypted):
# ord(c):字符转ASCII十进制数值
# ord(key[i % 2]):取密钥字符ASCII
# i % 2 密钥长度=2,循环取用 key[0]='S'、key[1]='S'
# ^ 异或运算;chr() 将异或后的数字转回字符
result += chr(ord(c) ^ ord(key[i % 2]))
# 转换成小写,题目要求标准md5小写格式
expected_md5 = result.lower()
# 输出最终解密得到的MD5哈希字符串
print("预期 MD5:", expected_md5)
# 输出: c8837b23ff8aaa8a2dde915473ce0991
2、找点击序列(与预期 MD5 值碰撞)
python
# 导入内置哈希库,用来计算MD5
import hashlib
# 导入排列组合工具,用于暴力穷举生成所有组合
import itertools
# 目标MD5哈希值,我们需要找到原始字符串,使其MD5等于该值
target = "c8837b23ff8aaa8a2dde915473ce0991"
# 第一步:优先尝试常见弱口令,避免直接暴力节省时间
commons = ["123", "321", "123321", "123456", "111111", "123123"]
# 遍历每一个预设常见字符串
for s in commons:
# 1. s.encode():字符串转为bytes,hashlib只能处理字节流
# 2. hashlib.md5():创建md5对象
# 3. hexdigest():获取32位小写十六进制md5字符串
if hashlib.md5(s.encode()).hexdigest() == target:
print("找到序列:", s)
break
# for-else语法:只有上面的for循环没有触发break时,才会进入else分支
else:
# 常见口令没命中,开始暴力枚举;尝试密码长度:4位、5位、6位
for length in range(4, 7):
# itertools.product('123', repeat=length)
# 作用:笛卡尔积,生成全部由字符'1','2','3'组成的length长度字符串,允许重复
# 例如length=2:('1','1'),('1','2'),('1','3'),('2','1')...
for combo in itertools.product('123', repeat=length):
# 将元组拼接为普通字符串,例如('1','3','2') → "132"
s = ''.join(combo)
# 计算MD5比对目标哈希
if hashlib.md5(s.encode()).hexdigest() == target:
print("找到序列:", s)
break
3、用算出的序列与加密的flag字符串进行XOR运算,解密flag
从 IDA .rdata 段 0x423030 提取的加密 flag 数据:

编写脚本:
python
# 定义加密后的二进制密文,bytes数组存储十六进制字节
# enc_flag 是加密后的flag原始字节流(逆向程序中提取出来的密文)
enc_flag = bytes([
0x57, 0x5E, 0x52, 0x54, 0x49, 0x5F, 0x01, 0x6D,
0x69, 0x46, 0x02, 0x6E, 0x5F, 0x02, 0x6C, 0x57,
0x5B, 0x54, 0x4C
])
# 滚动异或使用的密钥字符串:点击序列 "123321"
click_seq = "123321"
# 用来拼接解密出来的明文flag
flag = ""
# 遍历密文的每一个字节,i为字节下标
for i in range(len(enc_flag)):
"""
enc_flag[i]:取出当前加密字节(十进制数值)
click_seq[i % len(click_seq)]:循环选取密钥字符
len(click_seq)=6,下标取模6:0,1,2,3,4,5,0,1,2...循环复用密钥
ord():密钥字符转为ASCII数字
^ :异或运算,加密和解密操作完全相同
chr():将异或计算后的数字转回可见字符
"""
flag += chr(enc_flag[i] ^ ord(click_seq[i % len(click_seq)]))
# 输出最终解密得到的flag
print("Flag:", flag)
