用 REA 深入研究一个 Windows CrackMe

授权算法 字符解析 文件边界与线程行为

研究与验证日期 2026 年 10 月 12 日

这个 CrackMe 的标准卡密能够还原,但更值得研究的是程序实际接受哪些输入。深入验证后可以看到:一个字符就能满足卡密比较;某些标点可以替代数字;对于余数为 −1 的机器字节,多达 186 种非零字节都能作为匹配字符。与此同时,机器名比较、文件字节和、代码指纹和线程状态仍然是独立的通过条件。

本文围绕用户提供的练习样本,使用 REA 调用 Ghidra,检查原始 x86 指令,再通过 Unicorn 的受控实验和 Wine 中的原始进程运行交叉验证。文章将解释完整的授权流程、被忽略的输入边界,以及“算法正确”与“程序成功运行”之间的距离。

一 研究来源与验证范围

先说明研究来源:最初的分析已经阅读了吾爱破解的解答帖,核心函数定位和标准卡密思路受到该帖启发。因此,这不是一次未知答案条件下的独立破解。本文在这一基础上重新检查样本,新增字符解析穷举、授权文件边界、安装输入溢出、线程先后顺序,以及原始 EXE 在 Wine 中的运行实验。解答帖提供线索,本文自己的脚本和日志承担验证结论的依据。

样本介绍 52pojie thread 2121853

参考解答 52pojie thread 2124009

压缩包以 52pojie 为解压密码,得到 CrackMe.exe。整个研究使用同一份未修改的 EXE。下面的摘要可以用于检查读者手里的文件是否属于本文样本。

项目 本次实际使用的对象
文件 CrackMe.exe 19,470 字节
格式 PE32 i386 控制台程序
装载与入口 ImageBase 0x400000 EntryPoint 0x4014A0
静态分析 REA npm 包 6.4.0 Ghidra 12.1.4 JDK 21
指令执行 Unicorn 2.1.4 pefile 2024.8.26
进程运行 Ubuntu 24.04 容器 Wine 9.0 Xvfb
Wine 测试身份 GetUserNameA → lab GetComputerNameA → CRACKME-LAB
SHA-256
195c955c7e21a7222f92bfdc3e5224606
c9a8309bb222a942c6eb3fd29820248

上一框为了排版把摘要拆为两行,校验时应拼接成一个 64 位十六进制字符串。PE 入口先经过运行库启动代码,业务主函数位于 0x4015C0;两者不能混为同一个地址。

证据来源 它直接证明什么 它没有覆盖什么
REA 与 Ghidra 样本中的代码、引用、反编译结果 实际线程调度与运行成功率
Unicorn 给定导入模型与初始状态下的原始指令行为 完整 Windows API、CRT 与调度器
Wine 原进程 兼容层中真实文件读写、线程和成功或错误弹窗 原生 Windows 上的全部行为

Unicorn 装载原 PE 的代码,重定向导入地址到显式的 CRT 和 Win32 模型。卡密、签名、指纹和计时函数本体仍执行原始指令。大多数授权测试记录计时线程的创建,却不调度它;计时函数另用受控 tick 值单独执行。因此,字符穷举的通过结果只适用于所声明的初始化状态,不能冒充整机运行结果。

二 REA 在这次研究中承担什么工作

REA 为智能体提供 MCP 和 CLI 接口,把本地分析引擎的结果组织成可追踪的证据。对于这个 PE,真正执行反汇编和反编译的是 Ghidra。此次研究实际打开样本,并通过 analyze_function 获取了 14 个业务相关函数的分析记录。Unicorn 验证器和 Wine 运行器由本次研究另行编写。

REA 项目及官方说明

这里值得检查的是结果的来路。REA 源码中的 executeFunctionAnalysisEvidence 先校验 analyze_function 参数,再调用 provider,解析函数 dossier,最后保留 provider、rawResult、limitations、locations 和 analysisProfile,形成 Evidence。它让一段伪代码能追溯到特定样本、分析引擎和分析配置。

REA 函数分析证据实现 固定研究版本

本次返回记录包含样本 SHA-256,provider 为 Ghidra 12.1.4,authority 为 shipped-artifact,confidence 为 observed。这里的 observed 描述分析工具获得了该静态观察,不能解读为“程序运行已经成功”。environment 为 null,也不包含一份 Windows 运行环境证明。

open_binary({
  path: "/absolute/path/CrackMe.exe",
  provider_id: "ghidra"
})
analyze_function({ procedure: "0x401d0c" })
analyze_function({ procedure: "0x402190" })
close_binary({})

上面是本次 MCP 请求的关键参数。复现时需要配置 GHIDRA_INSTALL_DIR 和 JAVA_HOME,并启动 REA 的 stdio MCP 服务。证据包保留了调用脚本;文章中的描述性函数名由分析者添加,Ghidra 原始名称通常仍是 FUN_0040xxxx。

三 从程序启动追到最终成功条件

地址 本文使用的名称 职责
0x4015C0 main 设置目录、创建线程、处理参数
0x40174F remove_data 删除授权文件与 data 目录
0x401781 decode_message 将提示字符串逐字节减一
0x4017BC init_check 查询 data 并设置就绪标志
0x401841 copy_signature 复制指定长度的字节
0x401874 install 输入卡密并写授权文件
0x401B69 fetch_machine 取得用户名与计算机名
0x401BD4 unlock 串联授权校验并显示成功
0x401CB0 check_signatures 比较文件首尾标记
0x401D0C verify_key 机器绑定与卡密字符匹配
0x401F18 check_authdata 读取记录、指纹与最终字节检查
0x402190 decode_key_char 把卡密字符转为比较数值
0x4021C9 fingerprint_or_sum 代码指纹或文件字节和校验
0x40239F timing 记录和比较 GetTickCount

main 将当前目录切换到 %USERPROFILE%,所以授权文件的位置是用户配置文件目录下的 data,而不一定是 EXE 所在目录。程序创建一个计时线程和一个解锁线程,然后计算主函数指纹。-i 进入安装流程,-u 删除文件,无参数调用 init_check 并等待解锁线程。目录展开、目录切换和线程创建的返回结果并未完整检查。

CrackMe 主线程、计时线程与解锁线程的执行流程

图 1 主线程与两个线程的功能关系 三列不表示实际执行先后

解锁线程先计算 init_check 和 install 的指纹,再获取机器信息。它只有在 data 就绪、未进入参数分支且调试标志仍为 2 时,才继续检查文件字节和与授权记录。最终还要求第二组代码指纹通过、首尾标记有效、机器绑定匹配,以及卡密失败标志为零。

if (dir_ready && !args_present && debugger_flag == 2) {
    fingerprint_or_sum(NULL);
    check_authdata();
    if (fp_group2 == 0 && signature_ok == 1 &&
        machine_ok == 1 && key_failure == 0) {
        show_success();
        exit(0);
    }
}

这是便于阅读的逻辑重写,不是从原作者取得的源码。成功判据采用原程序的 Success 弹窗文本;exit(0) 本身不能作为解锁证据,因为错误输入、计时失败和若干提前退出路径也使用退出码 0。

四 授权文件的实际结构

安装函数写出 220 字节 authdata.dat,并把这 220 个无符号字节的和写入另一个文件 checksum。checksum 是一个四字节小端整数。记录的字段布局可以从两个签名写入点、strcpy 目标和 scanf 目标反推出。

十进制偏移 长度 字段 含义与实际检查
0 6 首标记 cv2pr 加 NUL 使用 strcmp
6 2 填充 没有独立语义检查
8 4 时间值 time 返回值 没有有效期比较
12 50 用户名 ANSI C 字符串 与当前用户名 strcmp
62 50 计算机名 ANSI C 字符串 与当前计算机名 strcmp
112 100 卡密 C 字符串 验证时会被改写
212 6 尾标记 cv2pr 加 NUL 使用 strcmp
218 2 填充 没有独立语义检查
checksum = sum(authdata[0:220])
checksum_file = little_endian_uint32(checksum)

这里没有时间签名、口令哈希或加密校验。时间值和填充字节只参与加法和。我将时间四字节改为另一组值,再重算 checksum,原进程在 Wine 中三次都成功。又在受控指令测试中把一个未检查字节加一、另一个减一,保留原 checksum,同样通过。这个文件和只能发现部分修改,不能证明记录的真实性。

字段长度是存储空间大小,不是已经执行的输入长度限制。程序用 strcpy 写两个 50 字节字段,不能自动阻止超长名字。本文生成器把每个名字限制为 1 至 49 个 ANSI 字节,并把记录预先清零;这是为了稳定构造符合正常布局的文件,不代表原程序拥有同样的边界检查。

五 代码指纹与反调试状态

0x4021C9 有两种任务。参数是函数地址时,它逐字节读取代码,遇到连续七个 0x90 才停止;单个 0x90 不计入和,其余字节相加。五个预设值分别对应 main、init_check、install、verify_key 和 check_authdata。匹配后清零相应槽位,再据前三个和后两个槽位更新两组全局状态。

被检查函数 预设加法和 所属条件
main 0x89D9 第一组
init_check 0x302C 第一组
install 0x11B35 第一组
verify_key 0xD21B 第二组
check_authdata 0xE5B5 第二组

这解释了为什么随意改写关键函数可能连带破坏授权流程。它也不是密码学指纹:排除 0x90 的加法和会产生碰撞,函数扫描的停止位置又依赖一段字节模式。本文没有改动这些函数,也没有把补丁绕过作为成功路径。

参数为 NULL 时,同一个函数改为读取 authdata.dat 的前 220 字节,计算字节和,再读 checksum 的前四字节。两者不同会先删除两个文件并尝试删除 data,之后授权读取才产生 Key not found。因而“Key not found”不一定说明文件最初不存在。

IsDebuggerPresent 为真会把一个初始值为 2 的全局标志改为 1,解锁线程要求该值仍为 2。另一个全局计数器累计非 NULL 的指纹调用,正常序列得到 5;卡密函数只有在计数恰为 5 时才反转输入卡密。计数和指纹槽位是两个不同条件,下一节的算法以它们处于正常状态为前提。

六 标准卡密算法与 ANSI 字节语义

机器绑定比较的是 GetUserNameA 与 GetComputerNameA 返回的字节字符串。卡密则逐字节把这些数据当作有符号 char,再计算除以 16 的余数。对 ASCII,结果与取低四位相同;对大于等于 0x80 的字节,这个简化常常是错的。

signed_byte = b if b < 128 else b - 256
# 在 Python 中实现 C 的向零截断余数
r = signed_byte % 16 if signed_byte >= 0 else -(
    (-signed_byte) % 16
)

标准生成方式先处理用户名,再处理计算机名。余数非负时使用 0123456789ABCDEF;余数为负时使用 ASCII 值 0x30 加余数的字符。生成出的完整字符串保持用户名和计算机名原有的字节顺序。正常校验会先反转卡密,然后依次比较倒序计算机名、倒序用户名,两个反转正好对应起来。

def canonical_key(username, computername):
    out = []
    for b in username + computername:
        v = b if b < 128 else b - 256
        r = v % 16 if v >= 0 else -((-v) % 16)
        out.append("0123456789ABCDEF"[r] if r >= 0
                   else chr(0x30 + r))
    return "".join(out)
例子 机器信息 标准完整卡密
参考帖例子 13120 / ANDY_DESKTOP 131201E49F453B4F0
ASCII 实验 alice / PC01 1C9350301
Wine 实验 lab / CRACKME-LAB C123213BD5DC12
cp936 字节实验 测试 / ANDY_DESKTOP ""*$1E49F453B4F0

“测试”在 cp936 中是 B2 E2 CA D4。作为有符号字节,它们的余数是 −14、−14、−6、−12,对应两个双引号、星号和美元符号。直接对 Unicode 码点计算,或者把中文编码为 UTF-8,都会得到另一种输入。这个例子在原指令模型中通过,本文没有声称在 Wine 中构造了相同的中文系统账号。

早期验证还对除 NUL 和保留值 x 以外的 254 种单字节用户名做过测试。该结果支持有符号字节规则,但这些 API 返回值是合成输入,并不等于 Windows 允许创建 254 种这样的账号。用户名或计算机名恰为字符串 x 时,会触发程序自身的无效机器标记判断。

七 实际接受的卡密比标准生成规则更宽

1 为什么一个字符就能通过

verify_key 在匹配成功后将卡密对应字节改为 0xFF,失败则改为 0x00。check_authdata 保存修改前的卡密长度,随后仅检查这段长度内是否全部为 0xFF。程序没有要求卡密长度等于用户名与计算机名的总长度。

正常状态下,任意非空后缀都会被反转为比较序列的前缀。对于 alice / PC01,完整卡密 1C9350301 的九种非空后缀全部通过;其中最短的 1 只需要匹配计算机名最后一个字节 1。Wine 身份的完整卡密以 2 结束,单字符 2 也三次显示成功,错误单字符 3 则三次显示 Illegal copy。

这个长度缺口削弱了卡密比较,却没有删除机器绑定:文件里的完整用户名和计算机名仍要与当前系统信息相等,首尾标记与文件和也仍须有效。复制另一台机器的文件后,仅把卡密缩短,不能自动解决名字比较失败。

2 解码器不是严格的十六进制解析器

int decode_key_char(unsigned char b) {
    if (b > 0x46) return -1;
    int n = b - 0x30;
    if (n > 9) n -= 7;
    return n;
}

我直接执行了 0x402190 的原指令,穷举全部 256 个输入字节。结果与上面的重写一致。它没有先检查字符是否落在 0–9 或 A–F 两个区间,而是允许两个区间之间的标点进入第二段算式。

字符 返回值 与标准字符的关系
: ; < 3 4 5 分别可替代 3 4 5
= > ? @ 6 7 8 9 分别可替代 6 7 8 9
/ −1 可匹配有符号余数 −1
G 至 0xFF −1 全部被转为 −1 并非独立拒绝
A 至 F 10 至 15 标准大写十六进制字母

例如 alice / PC01 的 1C9350301 可写成 1C9:50:01;两个 3 都用冒号替代。Wine 例子中的 C12:21:BD5DC12 也三次成功。按完整卡密的每个字符解析值列出所有等价输入,再对全部非空后缀做笛卡尔积,alice / PC01 一共得到 66 个不同的有效卡密,全部通过原指令验证。

后缀长度       1  2  3  4  5  6   7   8   9
等价卡密数量   1  1  2  2  4  8  16  16  16
总计          66

进一步穷举这组机器信息的 255 个非零单字节卡密,仅 ASCII 1 通过。随后把计算机名 API 的最后一个字节设为合成值 0xFF,其有符号余数是 −1;此时 / 以及 0x47 至 0xFF 的 185 种字节都能匹配,总计 186 种。这是在合成机器字节条件下的原指令结果,不能推导出普通 ASCII 机器上的任意小写字母都有效。

3 用一个条件描述整个接受集合

令 U、C 为机器名的原始 ANSI 字节,T 为 U 连接 C 后逐字节计算的有符号余数序列,N 为 T 的长度,L 为输入卡密的长度。若指纹计数为 5、机器绑定与其他门槛均有效,那么字符比较的条件是:

1 <= L <= N
对每个 j = 0 ... L-1
    decode_key_char(key[j]) == T[N-L+j]

标准生成器只选择每个数值的一种常规表示,并取 L 等于 N。程序实际接受的是每个非空余数后缀上,各字符解码前像的组合。这比“算法是用户名和计算机名的低四位十六进制”更准确,也解释了短卡密和标点替代为何同时存在。

八 文件长度与读取失败不能靠格式图推断

程序两次读取授权记录都使用 fread(buffer, 220, 1, file),却不检查返回值;checksum 也只读取一个四字节元素。因此,220 字节是正常写出的长度,不是读取端强制要求的文件总长度。

实验输入 原指令受控结果 Wine 原进程结果
有效 220 字节记录 成功 3 次成功
220 字节后追加数据 成功 3 次成功
checksum 四字节后追加数据 成功 3 次成功
改变时间并重算字节和 成功 3 次成功
截断为 217、218 或 219 字节 零栈可成功 A5 栈失败 每组 3 次 Key not found
错误 checksum 删除文件后失败 3 次 Key not found

截断实验特意使用同一记录、同一 checksum,改变读取前的栈填充值。模型只复制可读到的文件前缀,未写入部分保留原值:初始为零时,缺少的末尾零字节恰好得到补全;初始为 0xA5 时,额外字节参与文件和,引发删除。这证明该模型下的结果依赖未初始化状态。它不能证明截断文件在任意 CRT 上可稳定通过。

本次 Wine 的九次截断实验全部失败,与零栈模型的结果不同。这种差异是研究结果的一部分:模拟器提供了一个受控条件,兼容层进程使用另一份真实栈和真实 CRT。微软 fread 文档也说明,部分读取元素的值无法确定,读取返回的是完整元素数量;程序应检查返回值再使用数据。

Microsoft fread 返回值与部分元素说明

checksum 文件不存在时还有一个独立问题:函数跳过 fread,却仍调用 fclose(NULL)。早期验证器把所有 fclose 简化为成功,掩盖了这条路径。本次扩展模型会明确记录 CRT 契约错误并停止,不把它归为正常的授权拒绝。现代微软 CRT 文档规定空指针触发无效参数处理;样本导入的是 msvcrt.dll,本文没有据此断言所有历史 CRT 都会显示同一种错误。

Microsoft fclose 参数校验说明

九 安装流程的三个边界问题

1 输入宽度 100 会覆盖尾签名

卡密字段只有 100 字节,但 scanf 使用 %100[^\n]。宽度限制最多读入 100 个字符,随后还会写入一个 NUL。读满时,NUL 正好写到偏移 112 + 100 = 212,覆盖尾标记的首字符 c。

正常尾标记     63 76 32 70 72 00   cv2pr\0
100 字符之后   00 76 32 70 72 00   \0v2pr\0

受控安装函数和 Wine 中实际执行 -i 都复现了这个覆盖。99 字符没有覆盖尾签名,但实验所用的 99 个 A 本来就不满足机器卡密算法,仍显示 Illegal copy。这里检查的是内存边界,不能把“99 不覆盖”误写为“99 字符卡密有效”。

2 授权记录包含未初始化栈字节

安装函数没有在正常路径预先清零整条记录,而是只写签名、时间、名字和输入字符串等字段。以 alice / PC01 和九字符完整卡密为例,先将栈填充 0xA5,再执行原安装指令,输出的 220 字节中仍有 183 个 0xA5;这些剩余字节同样进入 checksum。重新读取这个记录仍可通过。

183 是这组指定名字、指定输入和指定栈模型的观察值,不是所有机器的固定数量。原进程中的剩余数据会随栈布局和运行历史变化。这也解释了为什么自行构造时应固定未使用字节,而不能照着字段表假定原安装文件天然全零。

3 名字复制与空输入检查都缺少可靠边界

把合成用户名设为 50 个字节时,strcpy 写出的 NUL 落入计算机名字段起点,随后被计算机名覆盖。后续 strcmp 看到的用户名跨过字段边界,机器绑定失败。这个实验验证了复制函数的边界缺口,没有把合成名字当作一个实际创建的系统账号。

空行输入也值得注意。扫描集没有匹配到字符时,scanf 可返回 0,不写目标缓冲区;程序却只检查 key[0] 是否为零,没有检查 scanf 返回值。0xA5 栈实验中,空输入仍写出了文件。这是对某种初始内存状态的复现,不能预测每一次控制台空输入都会产生相同文件。

十 计时判断实际比较的是 256 毫秒区间

0x40239F 的参数为 1 时记录 GetTickCount,否则对当前值与记录值异或,然后用 mov al, 0 清掉最低八位。剩余位非零就 exit(0)。它等价于要求两个 tick 的高 24 位相同。

passed = (baseline & 0xFFFFFF00) == (current & 0xFFFFFF00)
基准 tick 当前 tick 差值 原指令结果
256 511 255 通过
511 512 1 退出 0
0 4096 4096 退出 0
0x123400 0x1234FF 255 通过
0xFFFFFFFF 0 回绕 退出 0

因此,“运行耗时小于 256 毫秒就通过”并不成立。跨过区间边界,即使 tick 只差 1,也会退出。基准记录线程若尚未运行,零初值也可能导致比较失败。这里给定的 tick 是为了精确检查分支的模型输入;GetTickCount 在真实 Windows 上的分辨率通常为 10 至 16 毫秒,不能把差 1 的案例描述为本次实际测得的墙钟时间。

Microsoft GetTickCount 分辨率与回绕说明

十一 线程先后顺序会改变授权流程

1 解锁线程错过初始化后不会重试

main 在调用 init_check 之前就创建了解锁线程。解锁线程只判断一次 dir_ready,没有等待循环。受控实验先执行 unlock:它计算两项指纹、取得名字,由于第一组指纹尚未齐全又将名字替换为 x,随后发现 dir_ready 为零直接返回,整个过程中没有读取授权文件。之后再执行 main 的指纹和 init_check,状态虽已就绪,解锁线程却已结束。

反过来,在主函数指纹完成且目录就绪后执行解锁线程,同一份有效记录会成功。这是函数级原指令的两种受控执行顺序,说明源码中存在漏过就绪状态的路径;本文没有模拟完整的 Windows 调度器,也没有给出这个路径在原生 Windows 上的发生概率。

线程先后顺序对授权流程的影响

图 2 相同授权数据在两种受控执行顺序下的路径差异

2 非原子的指纹计数会丢失更新

0x4022D3  mov eax, [0x407040]
0x4022D8  add eax, 1
0x4022DB  mov [0x407040], eax

这三条指令不是原子自增。本次在同一个 Unicorn 内存中使用两份 CPU 上下文和两段栈,安排 A、B 都先读取计数 0,再让 A 完成加一和存回,最后让 B 完成加一和存回。两个指纹调用都执行完成,全局计数却只有 1。这里复现的是原始机器指令的丢失更新,不只是纸面推测。

计数的后果可单独检查:固定其他条件有效,计数 5 时标准完整卡密通过,计数 4 时它失败;反转完整卡密则恰好相反。单字符 1 在两个计数状态下都匹配,因为反转单字符没有变化。这些是卡密函数级实验;本文没有宣称已经在 Wine 中触发了计数竞争。

3 线程句柄等待也值得谨慎解读

程序保存 _beginthread 的返回值并传给 WaitForSingleObject,却不检查等待结果。微软文档提醒,_beginthread 的线程结束会自动关闭句柄,过早结束的线程可能使返回句柄失效。这个 API 契约支持对等待逻辑的质疑,但具体 msvcrt.dll 和 Wine 的实现仍须独立观察。本文没有将此项列为已经测得的原生 Windows 故障。

Microsoft _beginthread 与 _beginthreadex 句柄语义

十二 两处容易误判的辅助逻辑

初始化检查忽略搜索句柄

init_check 调用 FindFirstFileA(".\data", …),丢弃返回值,随后只判断 GetLastError 是否等于 2。受控导入模型给出成功句柄和遗留错误码 2 时,原指令报告 Not Initialized;给出 INVALID_HANDLE_VALUE 和错误码 5 时,它反而设置 dir_ready 为 1。

正确的判断入口应是 FindFirstFileA 返回的搜索句柄,失败后才读取相关错误码。GetLastError 是每线程保存的状态,成功 API 未必清零;它不是其他线程共同写入的全局错误值。此外,此路径没有关闭成功的搜索句柄。上述两种导入返回组合用于验证分支缺陷,本文未声称在 Wine 实测中碰到了它们。

Microsoft FindFirstFileA 返回值与 FindClose

Microsoft GetLastError 的线程局部状态

提示字符串解码不补 NUL

提示文本用每个字节减一的方式隐藏。0x401781 写入共享缓冲区,却没有在新文本末尾写 NUL。缓冲区初始为零时往往看不出问题;较长文本之后写较短文本,旧后缀就可能残留。

第一次输入密文 BCDEFG\0  → 缓冲区 ABCDEF
第二次输入密文 YZ\0      → 缓冲区 XYCDEF
期望的独立短文本应为 XY

这组结果由同一模拟实例连续执行原解码函数获得,说明它依赖旧缓冲区内容。多线程调用又会共享这个缓冲区,但本文没有测定真实运行中的文本竞争概率。描述弹窗时应优先记录实际观察文本,不宜只根据解码后的常量推断显示效果。

十三 原始 EXE 的运行结果

为检验模拟结果,我在独立的 Ubuntu 24.04 容器中安装 Wine 9.0,使用一个专用 32 位前缀和 Xvfb。容器运行阶段关闭网络,以普通用户执行,样本只读挂载。辅助 PE 通过与样本相同的 ANSI API 获取名字,复制测试记录到 C:\users\lab\data,启动原始 CrackMe.exe,读取属于该子进程的弹窗文本,然后关闭弹窗并记录退出状态。

这种方法没有替换原程序的导入函数,也没有跳过计时或改写代码。每个测试重新写入各自的授权记录,测试顺序串行,防止错误 checksum 删除文件后污染下一组。辅助程序只控制实验目录和自己启动的子进程。

输入组 重复次数 捕获到的原始弹窗
标准完整卡密 3 Success
单字符后缀 2 3 Success
错误单字符 3 3 Illegal copy
两个 3 改写为 : 3 Success
授权记录追加尾部数据 3 Success
时间字段修改并重算和 3 Success
截断 217 字节 3 Key not found
截断 218 字节 3 Key not found
截断 219 字节 3 Key not found
错误 checksum 3 Key not found
checksum 追加尾部数据 3 Success

共 33 次无参数原进程校验,其中 18 次捕获 Success,15 次捕获上述预期错误文本。随后另执行三次 -i 安装,并分别重新启动验证:标准完整卡密成功,99 个 A 显示 Illegal copy,100 个 A 无成功弹窗而退出;读取安装产物确认,100 字符案例的尾标记变成 00 76 32 70 72 00。这些数量描述这批固定用例,不是对随机线程调度成功率的统计。

Wine 中原始 CrackMe 的成功弹窗

图 3 在 Wine 中捕获的原始 CrackMe 成功弹窗内容

Wine 是兼容层。这一实验把结论推进到了真实 PE 进程执行层,但仍不能代替原生 Windows 的环境验证。当前没有完成原生 Windows 测试;线程竞态、旧 CRT 的异常路径和不同 ANSI 代码页,仍应在对应环境中另行检查。

十四 复现方式与随文材料

配套实验包保留 14 份 REA Evidence 及伪代码、原始反汇编、标准生成器、原指令验证器和扩展实验、Wine 辅助程序源码、容器构建文件、测试记录与日志。样本文件由读者使用原附件准备,包内不重复放入 EXE,也不打包 Wine 镜像或运行前缀。

python -m venv verify-env
# 按当前系统激活 verify-env
pip install -r requirements.txt
python crackme_verify.py /path/to/CrackMe.exe
python deep_verify.py /path/to/CrackMe.exe

deep_verify.py 会校验样本摘要,并在脚本所在目录保存 deep-verification.json。其扩展模型明确记录计时线程未调度、部分读取保留旧内存、以及 fclose(NULL) 的处理方式。763 项字符相关输入检查由 256 项解码器输入、66 项等价后缀、255 项单字节卡密和 186 项负一余数匹配组成;它们之外还有文件、安装、计时、初始化和上下文交错实验。

python crackme_keygen.py alice PC01
python crackme_keygen.py 测试 ANDY_DESKTOP --encoding cp936
python crackme_keygen.py alice PC01 --out demo-data

生成器使用标准完整卡密,输出目录中的两个文件可用于所指定的机器字节。Windows 上省略两个名字参数时,它调用 GetUserNameA 和 GetComputerNameA 取得同类数据。–out 显式控制写文件,现有文件不会被覆盖。完整输入检查与文件布局实现均保留在配套脚本中。

REA 部分可以独立重跑。将证据包中的 rea-reproduce.mjs按 README 所述配置为本地 MCP 客户端,指定 REA 6.4.0、Ghidra 安装目录和 JDK,即可重新取得函数 dossier。Wine 部分使用 Dockerfile 构建测试镜像,再按 README 创建独立前缀、编译 probe.c、准备与实际 API 名字一致的测试文件。文章列出的 Wine 名字是本次实验身份,换环境后应重新生成文件。

读者可以从标准完整卡密开始,再按表中用例改变长度、字符和文件结构。每一步保留原始弹窗与状态记录,就能检查自己的环境是否重现了本文的接受条件。

上一篇
下一篇