BUUCTF findKey-学习笔记

    • 前置知识
    • 正文
      • 一、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)—— 程序的核心)
        • 6、找到真正的验证逻辑
          • [(1) 验证发生在哪里?](#(1) 验证发生在哪里?)
          • [(2) 看懂验证逻辑(汇编级别)](#(2) 看懂验证逻辑(汇编级别))
        • [7、找到 MD5 算法(确定是 MD5 不是其他哈希)](#7、找到 MD5 算法(确定是 MD5 不是其他哈希))
        • [8、找到 XOR 加密函数](#8、找到 XOR 加密函数)
        • 得到md5、xor核心公式
      • 二、编写python脚本

前置知识

BUUCTF xor-学习笔记

正文

一、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)
相关推荐
0566461 小时前
agent学习Day15——SQLAlchemy 查询过滤、分页与历史列表接口
python·学习·fastapi
炎武丶航3 小时前
汽车功能测试学习(1):FCW前方碰撞预警
功能测试·学习·汽车
x17388062734 小时前
XXE外部实体注入
xml·笔记
库玛西8 小时前
C++ 运行时多态 :核心总结与原理图解
c语言·c++·笔记
魔力女仆9 小时前
语言开发笔记3
笔记
x17388062739 小时前
文件包含漏洞介绍
笔记
qq_185198699 小时前
SpingBoot3入门学习一
学习
噗噗夹的TA之旅9 小时前
Shader 学习 21:自定义 Render Feature
学习·游戏·unity·c#·游戏引擎·图形渲染
码农小韩10 小时前
AIAgent应用开发——大模型理论基础与应用(七)
python·学习·ai·大模型·agent