首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
1
28
[原创]第四题:未时·车流困城 WP
发表于: 2026-8-16 00:30
1160
[原创]第四题:未时·车流困城 WP
mxym_
2
2026-8-16 00:30
1160
# KCTF CrackMe Writeup ## 总体结构 题面公开样本使用 `Name = KCTF` 和一个 9226 字符的 Serial;程序本身并未硬编码 `KCTF`,而是从 Name 动态派生 100 个 Final state。完整校验链可以整理成四层: 1. 自定义 64 字符编码把 Serial 还原为 6912 字节; 2. 异常驱动的调度层把执行流送入白盒变换; 3. 432 个独立 16 字节块经过原生 Windows Stage3 置换; 4. 6912 字节结果被解析成 1000 个十进制整数,Final 按 Name 派生的 100 个状态逐组验证十次多项式。 原始附件 SHA256 为: `338f493766cfc94b6b075138f37cead0a2516d61fc983960c169b7ddf62be760` ## 分析入口与证据顺序 附件是 PE32 控制台程序,但主体会从 x86 入口切换到 x64 代码,并通过 VEH 处理大量故意触发的异常。几个关键入口为: | 阶段 | 地址 | 作用 | | ------- | --------------: | ----------------------------------------------------------- | | Stage1 | `0x8386B8` | 检查 Serial 外壳并解码 6912 字节 | | Stage2 | `0x7A6920` | 异常驱动的控制流调度 | | Stage3 | `0x7BE8D0` | 432 个 16 字节白盒块变换 | | Stage4 | `0x8386B4` | 解析十进制字段并检查组内次序 | | Final | `0x7FA6C0` 附近 | 64 字节大整数 VM 与多项式约束;该 VA 不作为稳定动态计时锚点 | | success | `0x4013C0` | 输出 `Successful!` | 公开的 Name/Serial 是整条链最重要的回归样本。先把公开 Serial 解到 Stage1 payload,再在原生 Windows 进程中观察 Stage3 后的明文,便得到已知输入、已知输出的真实 oracle。后续每个候选模型都先过公开样本,最后才用于 `KCTF`。 分析过程中曾得到一个在 Wine、Unicorn 和自建模型内正反向自洽的候选,Serial SHA256 为 `9b00d7506b8f1cc3c87f229bfadbfe799bf0a88ea47a853ccd8aff428cfa9473`。它在公开回归中有 6871 个字节不同,432 个块没有任何一块完全匹配,因此被直接废弃。后续确认旧模型恰好复现了未允许父进程分支的 Stage3 输出,而不是比赛公开样本所处的允许分支。这一步确定了证据优先级:原版 Windows 的 `Successful!` 最高,其次是原生边界捕获,然后才是离线模型内部自洽。 完整求解方向是从两端向中间推进: ```text 公开 Serial -> Stage1 payload -> 原生 Stage3 oracle | Name -> hash31 / candidate generator -> 100 states -> 1000 roots -> Stage4 目标明文 | Stage3 逆变换 <- 目标明文 | Stage1 逆编码 -> 最终 Serial ``` ## Stage1:Serial 编码 Serial 的结构固定为: - 前缀 `lI|0O`; - 正文 9216 字符; - 后缀 `Il1|!`; - 总长度 9226。 64 字符表为: ``Il1|!ijJL`oO0QDSs5$Zz2B8gq96nNmMWwUuVvRrPpCc({tT+7xXKkYyAa4Ee3FH`` 正文第 `i` 个字符先映射为字符表下标 `a_i`,解码用: `v_i = (a_i + 51 + 27*i) mod 64` 随后按高位优先把 9216 个 6-bit 值打包为 6912 字节。逆向生成 Serial 时先把目标 payload 拆成 6-bit 值,再计算: `a_i = (v_i - 51 - 27*i) mod 64` 这层对 6912 字节 payload 是一一映射,任意 payload 都对应唯一的 9226 字符 Serial。生成结果只使用 64 种可见字符,ASCII 最小值 33、最大值 124,满足题目的 `[33,126]` 限制。 恢复过程从公开 Serial 的固定首尾开始。去掉各 5 字符后,正文长度恰为 9216,而且所有字符只落入同一张 64 字符表。反汇编中的循环先查表得到下标,再对位置 `i` 加上线性项。对多个位置记录循环前后的下标,差值依次增加 27,常数项为 51,于是得到上面的仿射式。 每四个 6-bit 值生成三个字节: ```text b0 = (v0 << 2) | (v1 >> 4) b1 = ((v1 & 0x0f) << 4) | (v2 >> 2) b2 = ((v2 & 0x03) << 6) | v3 ``` 这里 `9216*6 = 6912*8`,没有 Base64 尾部填充,也没有残余位。用公开 Serial 正向解码得到的 payload 长度为 6912,SHA256 为: `c0cbf123af4af396ba69f36148c284b4117f84a5d9aa05d18be076d086233616` 它的第一个 16 字节块为 `1f658f979b46e33a2ed346f8c6ee799a`。这个块后来用于验证 Stage3:真实输出必须是 ASCII `40243-231740-361`,即十六进制 `34303234332d3233313734302d333631`。 ## Stage2:异常调度 程序混用了 x86、x64 Heaven's Gate、VEH 和 `IN/OUT` 异常门。Stage2 的主体可以概括为 `IN EAX,DX; RET` 与 `OUT DX,AL; RET` 触发异常,再由 VEH 修改上下文继续执行。它主要负责控制流混淆,不改变后续数学约束。 x86 到 x64 的桥位于 `0x4010E0`,x64 VEH 位于 `0x8113A0`。异常发生后,handler 根据 fault site、寄存器和私有上下文决定下一段代码地址。真实运行中还观察到 `R10 XOR R11 = 0x8113A0` 一类与 VEH 地址相关的稳定关系,因此把异常入口的易失寄存器简单清零会改变调度。旧模拟器正是在这里与原版发生偏差。 这也解释了常规调试很难稳定复现:断点和线程上下文修改会扰动一个本来就依赖异常与上下文的状态机。动态阶段最初采用两个低扰动方法: 1. 子进程继承真实 Windows console,不重定向 stdin、stdout 或 stderr; 2. 观察器只调用 `OpenProcess`、`VirtualQueryEx` 和 `ReadProcessMemory`,不附加调试器,也不暂停或修改目标线程。 后续控制实验确认标准输入管道本身不会改变结果。同一个 Python 解释器副本命名为 `cmd.exe`,直接用 PIPE 向子进程写入公开 Name/Serial,仍然输出 `Successful!`。因此 console 保留方案只是早期降低变量数量的观测手段,不能解释 `cmd` 与 `pwsh` 的分歧。 ### cmd 成功而 pwsh 失败的根因 最初的现象是:双击、`cmd.exe` 和 Windows PowerShell 5.1 可以通过公开样本,而 PowerShell 7 的 `pwsh.exe` 会失败。先做输入侧控制变量后可以排除粘贴截断、代码页、console/PIPE、工作目录和环境变量:允许父进程下使用普通 PIPE 也能稳定 `Successful!`,而失败实例的 Stage1 仍解出同一份 6912 字节 payload。因此首次真实分歧位于 Stage3 的隐藏 schedule 输入。 允许环境中的 176 字节 schedule SHA256 为: `dd7f46c38963ee96ae140070eb10d3e4b8ee8a79cf7aefe151af6529476c975f` 未允许环境中的 schedule SHA256 为: `ae02ce0b9325225b86908ca5343ed73b1acd7f0b2d571725550544aa1171e3e9` 第 0 条 16 字节记录相同,第 1 条立即出现整块 `0x40` 差异: ```text allowed : 541596f41b96eea558ae489b68e46f5d rejected: 1455d6b45bd6aee518ee08db28a42f1d xor : 40404040404040404040404040404040 ``` 父子进程置换确认只看 `CrackMe.exe` 的**直接创建者**。例如 `pwsh -> cmd -> CrackMe` 成功,`cmd -> pwsh -> CrackMe` 失败。随后使用完全相同字节的父进程副本,只改变文件名,得到: | 同字节父进程 basename | 结果 | | ---------------------- | :--------: | | `cmd.exe` | Successful | | `powershell.exe` | Successful | | `explorer.exe` | Successful | | `pwsh.exe` / `bad.exe` | Failed | 这只能说明三个预期名字会命中,**不能推出字符串精确比较**。真正实现是大小写不敏感的 32-bit hash 白名单。 ### 父进程 ImageName、hash 白名单与真实映像验证 当前 Windows 环境中,目标通过私有 syscall stub 调用 `NtQuerySystemInformation(SystemExtendedProcessInformation)`;信息类为 `0x39`。第一次 64 KiB 查询用于得到所需长度,随后扩容重试。最终缓冲区中可以直接解析出当前 `CrackMe.exe` 记录、`InheritedFromUniqueProcessId` 和对应的直接父记录。 白名单输入究竟来自哪里,不能靠“晚改内存没变化”判断。最终采用的因果实验是在第二次进程表查询**已经从内核返回、但 caller 尚未执行返回后的第一条指令**时冻结线程,然后只改父记录的 `ImageName` backing buffer: - 实际父进程仍为同字节的 `bad.exe`,把父记录 `ImageName` 从 `bad.exe` 改为 `cmd.exe`:4/4 写入 hidden gate,完整运行一次直接 `Successful!`; - 实际父进程仍为 `cmd.exe`,把父记录 `ImageName` 从 `cmd.exe` 改为 `bad.exe`:4/4 不再写 gate,完整运行一次直接 `Failed!`。 所以 basename 的数据源就是最终 `SYSTEM_EXTENDED_PROCESS_INFORMATION` 中**直接父记录的 `ImageName`**。此前在程序已经消费名字之后再修改原记录,会得到“改了也没用”的假阴性。 对这个 basename 的实际变换为: ```python h = 0xF13093F0 for ch in parent_basename.lower(): h = (h * 33 + ord(ch)) & 0xFFFFFFFF ``` 实机与静态立即数完全闭合: ```text powershell.exe -> 0x8EED4105 cmd.exe -> 0xD0591A74 explorer.exe -> 0x9D1337F1 bad.exe -> 0x6760F627 ``` 原 PE 中三个允许值各只作为一条真实 `cmp edx, imm32` 出现: ```asm 0x8267A1 cmp edx, 8EED4105h ; powershell.exe 0x828FB4 cmp edx, D0591A74h ; cmd.exe 0x81369F cmp edx, 9D1337F1h ; explorer.exe ``` 动态 reachability 给出的顺序正是 `powershell -> cmd -> explorer -> reject`。hash 的 live 实现也逐步闭合:`0x81F1DD` 装入 seed `0xF13093F0`;字符入口能直接看到父 `ImageName` 当前 UTF-16 字符;`CMD.EXE` 运行时在字符入口读到 `'C'`,到后续算术块前已经归一化成 `'c'`。首轮更新的关键指令为: ```asm 0x81A60B shl r8d, 5 ; h * 32 0x831FC3 add r8d, edx ; h * 33 0x821177 add edx, r8d ; h * 33 + ch ``` 寄存器现场与公式逐项一致。 因此它不是“大小写不敏感的精确 basename 白名单”。最直接的反例是构造 hash 碰撞: ```text hash("cmd.exe") = 0xD0591A74 hash("ljfuxk.exe") = 0xD0591A74 ``` 把同一份父启动器字节复制为 `ljfuxk.exe` 后,真实文件名完全不同,原附件公开样本仍然 `Successful!`。这证明 gate 的语义是**32-bit hash 白名单**,理论上允许碰撞 basename。 hash 命中之后,程序才进入另一层“真实父进程映像”检查: ```text ImageName hash 命中 -> NtOpenProcess(parent, 0x410) -> NtQueryInformationProcess(ProcessBasicInformation) -> 得到父 native PEB -> NtReadVirtualMemory(parent, PEB+0x10, 8) -> 得到父 ImageBaseAddress -> 读取 ImageBase 开头并验证 MZ ``` 真分支入口静态 VA `0x83132F` 的现场 `RAX=0x5A4D`,正是刚验证的 `MZ`。随后代码执行: ```asm 0x83132F mov rax, qword ptr gs:[60h] ; 当前进程 native PEB 0x83133A jmp 0x8249EB 0x8249EB add byte ptr [rax+2], 40h ``` 也就是说父 `ImageName` hash 决定是否进入真实性验证,父映像验证通过后才把当前进程 native PEB `+2`(通常称 `BeingDebugged` 字节)加上 `0x40`,把它当作题目自己的隐藏状态。`CREATE_SUSPENDED` 时两条路径该字节都为 `0x00`,32 位 PEB 的同名字段也始终为零,因此这不是 Windows 自动设置的兼容标志。 这个字节随后进入 Stage3 schedule:强制把拒绝路径的 native PEB `+2` 从 `0x00` 改为 `0x40`,公开样本会翻成 `Successful!`;反向把允许路径清零则变为 `Failed!`。因此稳定后台验证只需要保证直接父进程的 basename hash 命中,例如使用 `cmd.exe` 直接创建目标。Windows PowerShell 5.1 成功而 PowerShell 7 失败,是因为前者 basename 为 `powershell.exe`,后者为 `pwsh.exe`。 ## Stage3:原生 Windows 白盒置换 Stage3 以 ECB 形式处理 432 个 16 字节块。对十组 chosen-input 共 4320 个块做原生边界捕获后,模型与程序输出逐字节完全一致。 ### 建立原生 chosen-input oracle 只用一组公开输入输出不足以恢复白盒映射,因此构造了完整的单字节真值表。对每个输入位置 `p=0..15` 和每个字节值 `v=0..255`,建立仅有该位置为 `v`、其余位置为零的 16 字节块,共 4096 个。每次程序能处理 432 个块,于是分成 10 次运行:每次放 428 个测试块,末尾再放 4 个由 SHA256 派生的 marker 块,用于确认捕获的 work buffer、块编号和运行批次没有串位。剩余 184 个槽填入固定随机种子的 dense block,专门做独立回归。 观察器先用公开 Stage1 payload 的 32~64 字节片段扫描可读内存,锁定 6912 字节工作区;随后在 Stage3 的 `NtGetContextThread` 边界高频读取当前块和上下文区。重复输入在不同进程中产生相同输出,任意块的变化也不会影响相邻块,从而确认它是固定的 16 字节置换,432 个块之间没有链式状态。 chosen-input 的作用不只是黑盒查表。设全零输入在某一轮边界的状态为 `B`,单字节输入表为 `T_p(v)`。实际捕获满足: `F(x_0,...,x_15) = B XOR xor(p=0..15)(T_p(x_p) XOR B)` 所有 dense block 都精确命中这个叠加式。这说明边界之前的非线性按字节独立,之后接一层 GF(2) 线性扩散,可以继续把 64 KiB 真值表压缩为 S-box、密钥和线性层。 ### 恢复线性层和首层 把 16 字节状态看作 4×4 的行优先矩阵。定义: `L(x) = InvMixColumns(InvShiftRows(x))` 其中 MixColumns 使用 AES 的 GF(2^8) 多项式 `0x11B`。自定义 S-box 在 Exp 中完整给出。 对单字节差分统计输出支撑集,每个输入字节恰好扩散到一列的四个字节,四个系数落在 AES `InvMixColumns` 的 `(0e,09,0d,0b)` 循环排列中。再对捕获差分应用候选 `L^-1`,每个差分都收缩为一个字节。16 个位置的落点给出严格的 4×4 转置,而不是近似的 AES 布局猜测。 首层存在一个外部输入转置。输入位置 `p` 对应内部位置: `q = (p mod 4)*4 + floor(p/4)` 令外部等效密钥为: `k_in = 0902ceb5593c04834174cab1171b4ee9` 首层基准常量为: `B = 0c90f0b16813bc8864c5cebe2dcfbc2f` 则首层可以直接写成: `u[q] = S(input[p] XOR k_in[p]) XOR S(k_in[p])` `state_0 = B XOR L(u)` 这个式子来自首轮 16×256 chosen-input oracle。对每个 oracle 差分应用 `L^-1` 后,结果只剩转置位置上的一个字节,并且精确符合 `S(x XOR k) XOR S(k)`。因此不需要在 Exp 中携带 64 KiB oracle。 全零输入的首层边界状态就是 `B=0c90f0b16813bc8864c5cebe2dcfbc2f`。逐位置枚举 `k`,要求 256 个输入值同时满足 `S(v XOR k) XOR S(k)`,每个位置都只有一个解,拼出 `0902ceb5593c04834174cab1171b4ee9`。首层的 forward 与 inverse 因而都可以直接写成 16 次 S-box 和一次线性层。 未允许父进程分支中还能观察到下面这套 canonical round-key 序列: ```text R0 = c01bf3ba6ab54a4832bdb6052492151d R1 = 9eeff678c2ea5c0ed6628bd21e28c026 R2 = bb71198e5228b652a4b4e9596936e8e6 R3 = 31ca6897bf7a9ee4d0105db0415fde0e R4 = 92fba2ff79c5e47aecc04dedce1e81d0 R5 = a469595dd8bc219ea42c8da02ed09f51 R6 = 6bcd300441649dbf4888a12d18fe4fce R7 = 59a6fd347025f922f2c0298c62e6b181 R8 = ecff5bc95e55dcdb8532e9a5ed845730 R9 = f913a492ce0b89075eb7db4c4a69d367 ``` 这组数值来自 `pwsh.exe`、Python 等未允许父进程选择的 Stage3 实例。其 176 字节 schedule 的末条记录按 dword 反转后正是 `R0=c01bf3ba...`。允许父进程实例的末条记录按同样方式处理后得到 `k_in=0902ceb5...`。两者首先是不同父进程 gate 选择的两套运行实例,随后才各自存在内部边界、外编码和状态转置。早期把差异全部归因于表示边界,会让旧的 synthetic 模型在自身内部保持漂亮自洽,却无法通过公开样本。 ### 恢复中间八轮 中间八轮为: `state_(r+1) = L(S(state_r XOR E_r)) XOR 40...40` 程序在上下文区生成一段 11×16 字节 schedule。把连续 `NtGetContextThread` 边界的状态按寄存器中的轮计数对齐,再用 `L^-1` 和逆 S-box 逐轮消去,得到八个当前 Windows 实例的等效轮密钥: ```text E1 = 34bc76cd758317f29819e3ceafb42e14 E2 = b788cabb38f694e5fc81fa2de51b9a3a E3 = 0c3f427195ce62710c7d7bd7ebfe81a0 E4 = 63337d33b15bac13517106acbb157f21 E5 = f7504e4e09eaf7bfc32077aa16ae6a5e E6 = 6ba71e00f1e31d4865e357dd6fb8c434 E7 = 5dccb91e3712fe552f86b48aefd77cf0 E8 = c29175a76e25ecab5ea9323e1138ab8c ``` 每一轮在线性层之后还带有 `0x40` 重复 16 次的仿射常量。把它漏掉时,单轮差分仍会看似正确,但多轮绝对值会从第一轮开始整体偏移;加入后,所有捕获状态闭合。 ### 恢复末轮并构造逆变换 末轮为逐字节 S-box、固定输出置换和常量异或: `output[perm[p]] = S(state[p] XOR Kf[p]) XOR Cf[perm[p]]` 其中: - `Kf = b953e4d28e4bc9471ef79b0c0a299327`; - `Cf = 82e740631138c30f7ee6780386670a70`; - `perm = [0,4,8,12,5,9,13,1,10,14,2,6,15,3,7,11]`。 末轮没有 MixColumns。用单字节扰动检查输出位置,每个内部字节只改变一个输出字节,由此唯一确定 `perm`;随后枚举 S-box 输入偏移,得到末轮等效密钥和输出常量。 逆变换按下面的顺序执行: ```text 末轮:state[p] = InvS(output[perm[p]] XOR Cf[perm[p]]) XOR Kf[p] 中轮:state = InvS(L^-1(state XOR 0x40...40)) XOR Er 首层:raw[p] = InvS(L^-1(state XOR B)[q] XOR S(k[p])) XOR k[p] ``` 中间轮按 `E8` 到 `E1` 逆序执行。完整表模型先对十次原生运行的 4320 个块做回归,结果为 4320/4320 精确命中;压缩后的公式模型又与完整表模型比较 1000 个随机块,forward 和 inverse 全部一致。至此可以从任意 6912 字节 Stage4 目标,逐块反推出唯一的 Stage1 payload。 ## Stage4:文本格式和边界 Stage4 解析 1000 个十进制整数,使用 999 个 `-` 分隔。每 10 个数属于同一个 Final state。当前附件对每组执行严格递增检查;组与组之间重新开始,因此第二组可以小于第一组末项。 这一层最初通过公开样本的 Stage3 输出识别:work buffer 的开头直接出现 `40243-231740-361-...`,说明白盒输出并非摘要,而是后续解析器的 ASCII 输入。跟踪字段提交点可以看到索引从 0 增至 999;每提交十项,保存的上一项被重置。最终返回条件可以化简为: ```text parsed_count == 1000 format_error == 0 order_error == 0 ``` 字段只含十进制数字,前导零参与文本长度但不改变整数值。每组十项要求 `x[10g+i] < x[10g+i+1]`,没有跨组比较。 控制变量实验得到精确长度边界: | 有效文本长度 | 结果 | | ---------------: | :--------- | | 6891~6895 | Failed | | 6896~6911 | Successful | | 6912,无终止 NUL | Failed | 在有效文本后的 NUL 区域改填 `0xFF` 也会失败,因此 6912 字节剩余部分保持为零。 边界不是由猜测得出,而是固定同一组 1000 个合法整数,只改变前导零总数并逐个测试。规范根文本长 6891:加入 0~4 个零时仍不足 6896,失败;加入 5~20 个零时依次覆盖 6896~6911,全部成功;加入 21 个零得到 6912 字节且没有 NUL,失败。交换同组前两个根会单独触发顺序失败。由这些实验可分别确定最小长度、终止条件、尾部清零和组内严格递增四条约束。 ## Final:100 个十次多项式 `KCTF` 在 Final 入口生成 100 个 14-bit state。每个 state 对应 11 个系数记录。描述符表位于 VA `0x4240A0`,数据块位于 VA `0x4D40A0`。描述符高 8 位是长度,低 24 位是数据块偏移。 ### 从大整数 VM 定位真实约束 Final 的动态路径规模很大。一次只采样前半段就得到约 80 万个 PC 样本和 1783 个不同地址,主要代码落在 `0x77A000~0x791000`,另有外部分发区。直接按跳转顺序抄 VM 会产生大量与数学语义无关的 trampoline,因此先跟踪 64 字节大整数对象的分配、复制和乘法关系。 以第一状态 `s=11467` 为例,固定状态常量为: `C_s = 2011170000000` 改变该组输入 `x` 后,多个对象始终满足: ```text alloc31 = C_s * x alloc32 = C_s * x^2 alloc20 = x * (x mod 10) alloc30 = (4*x) mod 20 alloc33 = (4*x) mod 10 ``` 这表明 VM 同时在构造 `x` 的幂和处理十进制位,适合从静态大整数记录恢复整体多项式。两个因果实验用于排除伪依赖: 1. 第二 state 入口的自然 `R11=-2` 被改成 `0x12345678` 后,随后 50 万条 PC 的路径哈希完全相同,到第三 state 时全部通用寄存器也重新汇合,因此 `R11` 只是 trampoline 保存值,不是跨 state 的进位或数学状态; 2. 单 state 实验结束后只把 `RSI` 设为 99、`dword [rsp+0x80]` 设为 100,保持所有大整数对象不变,Final 原始返回值由 0 变为 1。这证明早期强制退出时看到的 `RAX=0` 来自外层完成计数未满足,不能据此判定当前 `x` 数学失败。 真正稳定的信息来自每个 state 的固定 record 访问。dispatcher 中可见 `0x4A/0x49/0x45/0x41` 四类 operand,继续追踪其数据源会汇合到静态描述符表。每个 state 使用连续 11 个描述符,正好对应常数项到十次项。此前用错误 keystream seed 解出的“1000 个漂亮整数根”只是能通过表面格式检查的诱饵;换成下面逐字节闭合的 decoder 后,系数、VM 对象和原生成功路径才全部一致。 ### 解码系数记录 state 为 `s`、系数序号为 `r` 时: `base = (s*0x9E3779B9) XOR (r*0x517CC1B7)` 记录第 `i` 字节的密钥由下式产生,所有计算截断为 32 位: ```text x = base XOR (i*0x85EBCA6B) x ^= x >> 16 x *= 0x5BD1E995 x ^= x >> 13 decoded[i] = raw[i] XOR (x & 0xff) ``` 解码结果是带符号 BCD 整数。首字节最高位为符号,其余 7 位为十进制位数,后续字节按高半字节、低半字节排列十进制数字。 例如 `s=11467, r=10` 的描述符为 `0x021F819B`,长度为 2,数据偏移为 `0x1F819B`。解密后记录是 `01 10`:首字节表示 1 位正数,BCD 数字为 1,因此最高次系数为 1。`r=0` 的描述符为 `0x1D1F80E9`,解密后首字节为 `0x38`,表示后面有 56 位十进制数,得到常数项: `62193613838163321199553657179547772100784429239606068480` 11 个系数按常数项到十次项排列: `P_s(x) = c_0 + c_1*x + ... + c_10*x^10` 每个多项式都精确分解为十个互不相同的正整数根。以第一个 state `11467` 为例,十个根为: `149982, 182871, 188530, 199607, 206187, 576399, 756853, 789216, 875674, 969329` 它的 11 个系数为: ```text c0 = 62193613838163321199553657179547772100784429239606068480 c1 = -2101937521426890715182593628862259113248967353816216 c2 = 30548659845599211507745247119377904247139520620 c3 = -250006849543688044390268468065663379167242 c4 = 1268505620929227312733149552700525881 c5 = -4148817229731867084396234768076 c6 = 8835477425930744413483099 c7 = -12113697845839347418 c8 = 10274707608319 c9 = -4894648 c10 = 1 ``` 用整数域分解可直接验证: `P_11467(x) = product(r in roots)(x-r)` 所有 100 个多项式都满足相同结构。每个候选根还会用 Horner 法代回 11 个系数,要求精确结果为零,避免只依赖因式分解库的返回格式。 ### Name 到 100 个 state 这部分不能只把 `KCTF` 的 100 个值动态抓下来再硬编码,否则求解器仍然只适用于一个 Name。最终把生成器本身恢复出来后,可以从任意 Name 重新计算 state。 第一层是大小写敏感的 32-bit `hash31`: ```python def name_hash31(name: bytes) -> int: h = 0 for c in name: h = (h * 31 + c) & 0xFFFFFFFF return h ``` 例如: ```text A -> 0x00000041 AA -> 0x00000820 KCTF -> 0x00231DCA kctf -> 0x003225CA ``` KCTF 的 live 入口寄存器还能看到 `R10=0x00231D84`、`R9=0x00231DCA`;前者恰好是处理最后一个 `'F'` 之前的 `h*31`,后者等于前者加 `'F'`,与公式一致。这里显然**区分大小写**,与父进程 basename 的 casefold hash 是两套完全不同的逻辑。 随后对 32-bit base 使用一个自定义 avalanche: ```python def avalanche(x: int) -> int: x ^= x >> 16 x = (x * 0x5BD1E995) & 0xFFFFFFFF x ^= x >> 13 x = (x * 0xC2B2AE35) & 0xFFFFFFFF x ^= x >> 16 return x & 0xFFFFFFFF ``` 两个乘法常量都经过 live 执行确认:`0x808D17` 执行 `imul ecx, ecx, 0x5BD1E995`,`0x80A07B` 执行 `imul ecx, ecx, 0xC2B2AE35`。以 KCTF 的第一个 candidate 为例,整条数值链为: ```text 0x00231DCA -> xor >>16 = 0x00231DE9 -> *5BD1E995 = 0x48E2799D -> xor >>13 = 0x48E03E8E -> *C2B2AE35 = 0x9BAD7766 -> xor >>16 = 0x9BADECCB ``` 然后取低 14 位作为 state 候选: ```asm 0x803418 and ecx, 3FFFh ``` 候选进入一个按 state 索引的已用表: ```asm 0x7FE82D cmp dword ptr [rax + rcx*4], 0 ``` 未出现过才写入最终数组。真正的数组提交点是: ```asm 0x809926 mov dword ptr [r15 + rdx*4], ecx ``` 现场 `R15=0x7F0000`,因此 100 个 state 以 100 个 DWORD 连续保存在 `0x7F0000`。KCTF 第一次 store 时 `RDX=0`、`ECX=0x2CCB=11467`;Name=A 时同一点 `ECX=0x3751=14161`。 完整递推为: ```python def derive_states(name: str) -> list[int]: H = name_hash31(name.encode()) base = H states = [] seen = set() while len(states) < 100: candidate = avalanche(base) state = candidate & 0x3FFF if state not in seen: seen.add(state) states.append(state) # len(states) 是当前已接受的 state 数;重复时不会增加。 base = (candidate + H + len(states)) & 0xFFFFFFFF return states ``` 这里去重不是理论推断。对 KCTF 连续抓取了 101 个 **full 32-bit candidate**,公式 101/101 精确重放。第 90 次额外尝试产生 `candidate=0x746E6815`,低 14 位仍是先前已经出现过的 `10261`,因此被去重表拒绝、已接受计数不增加;下一次 `0x308A2E9F & 0x3FFF = 11935` 才继续写数组。最终恰好 101 次尝试得到 100 个唯一 state。 KCTF 开头十项仍为: `11467, 11985, 12162, 1385, 12612, 9749, 22, 7340, 8302, 13426` 但 Exp 不再保存这 100 个常量,而是实时调用 `derive_states(Name)`。交叉验证不仅针对 KCTF:独立抓取的 `A` 和 `AA` 两份完整 100×DWORD 数组与离线公式都是 100/100 相等;再用这些动态派生 state 求多项式根并生成新的 9226 字符 Serial,原附件对 `Name=A` 和 `Name=AA` 都直接输出 `Successful!`。 需要注意,**能生成 100 个 state 不代表任意 Name 都可注册**。后续每个 state 还必须对应十个可由十进制整数输入满足、且能通过严格递增约束的根。例如 `KCTG` 的派生序列包含 `state=15631`,当前附件对应多项式没有十个互异整数根,因此这个 Name 无法按正确版本的 10 根组约束构造有效 Serial。 Final 对同一 state 连续检查十个输入,并对每个输入独立验证 `P_s(x)=0`。当前附件再依赖 Stage4 的组内严格递增约束,把可注册 state 的这一组唯一化为升序的十个根。 ## 关键纠错与判据 这题大量使用异常调度和故意可误读的静态字节,局部上“看起来合理”远远不够。最终保留的结论都要求至少有边界回归、因果注入或原附件端到端验证之一: | 中间现象 / 旧结论 | 更强控制实验 | 最终结论 | | --------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------ | | Wine/Unicorn 白盒模型可正反向 | 对公开 432 个真实块回归 | 0/432 命中,模型废弃 | | 旧 seed 解出 1000 个整根 | 用真实 record decoder 对比 VM 对象 | 属于格式漂亮的错误数据 | | `R11` 看似跨 state carry | 第二 state 入口注入随机值 | 后续路径与寄存器重新汇合,不是数学状态 | | 1000 个整数看似全局递增 | 交换跨组边界与组内元素 | 只在每十项内部检查顺序 | | 父进程是“精确 basename 白名单” | 静态 `cmp edx, imm32` + `ljfuxk.exe` hash 碰撞 | 实际是大小写不敏感的 32-bit hash 白名单 | | 晚改进程表 `ImageName` 不影响 gate | 在第二次进程表 syscall 返回、caller 尚未执行时双向注入 | `SYSTEM_EXTENDED_PROCESS_INFORMATION` 父记录 `ImageName` 就是 hash 输入 | | 打开父进程是为了取得 basename | hash compare 与 `NtOpenProcess` 执行顺序 | 打开父进程发生在 hash 命中之后,用于 PEB/ImageBase/MZ 真实性验证 | | KCTF 的 100 states 只能动态抓取后硬编码 | 抓 full candidate、逆 avalanche、去重与数组 store | 已恢复通用 `hash31(Name) -> avalanche -> dedup -> 100 states` | | 6 个零集中首项“实测最快” | 固定工作区、ABBA/轮换顺序的阶段级计时 | 差异会变号,无可重复速度优势;5 个零只因严格表示最短而优先 | 最终判据始终是同一条:局部公式必须命中 live 边界;边界模型必须回归公开样本;最终生成候选必须由原始附件输出 `Successful!`。 ## 多解原因 ### 当前附件的表示多解 100 组根升序后,规范十进制文本长度为 6891。有效长度必须位于 6896~6911,因此需要加入总计 `z=5..20` 个保持数值不变的前导零。把 `z` 个相同的零分配到 1000 个字段有: `C(z+999,999)` 种方式。当前附件至少有且在该语法下恰有: `sum(z=5..20) C(z+999,999) = C(1020,20)-C(1004,4)` `= 506361081139239362675760942888576661652500` 个不同 Serial。Stage3 和 Stage1 都是双射,因此每个不同文本都对应不同 Serial。 这里还要区分“最短有效文本”和“Serial 长度”。Serial 外壳本身始终固定为 9226 字符,所以从最终 Serial 字符数看,所有合法解都一样长;真正可优化的是 Stage4 的 NUL 终止前有效十进制文本长度。最小值 6896 要求**恰好 5 个**前导零,因此仅最短这一层就有: `C(1004,999) = C(1004,5) = 8416958750200` 种不同表示。当前 `serial.txt` 把 5 个零全部放在第一个字段,只是这 8,416,958,750,200 个最短表示中的一个确定性 canonical representative,并不是唯一“最短 Serial”。 这里数值序列本身是唯一的。每个 state 只有十个不同根,Stage4 又要求十个位置严格递增;从十个元素中选十次且不允许相等,只能把全部根各取一次并按升序排列。多解只来自十进制解析允许前导零,而不是多项式还有额外根。 ## 完整候选构造 对题目 Name=`KCTF`,`derive_states` 动态得到 100 个 state;每个 state 的十个互异整数根升序后展平为 1000 个十进制字段。规范文本长度为 6891,而严格有效长度必须在 6896~6911,因此默认选择一个**理论最短的 canonical 表示**:只在第一个字段前补 5 个零,使有效长度恰为 6896。这个摆放只用于得到稳定、可复现的唯一输出,不表示它在同长度候选中被证明为最快。 最终生成过程为: 1. `hash31(KCTF)` 并动态生成 100 个唯一 state; 2. 对每个 state 从原始 PE 解 11 个系数,分解并用 Horner 法验证十个整数根; 3. 每组根升序排列并展平为 1000 个十进制字段; 4. 用 999 个 `-` 连接,在第一个字段前加入 5 个零,使有效文本长度为 6896; 5. 尾部补 16 个 `0x00`,得到完整 6912 字节 Stage4 目标; 6. 分成 432 个块执行 Stage3 逆变换; 7. 对所得 6912 字节执行 Stage1 逆编码,加固定前后缀,得到 9226 字符 Serial。 当前默认最短 KCTF 候选的关键哈希为: | 数据 | 长度 | SHA256 | | ---------------- | ---: | ------------------------------------------------------------ | | 100×DWORD states | 400 | `c7a0133ab4bf7b1c03880708762387b6200f19f2978f77d3332190ba943be286` | | Stage4 目标 | 6912 | `03f5fe9b57edff995f945c60a29814854b50a2660c61a96f1a5136be513e418a` | | Stage1 payload | 6912 | `dbd16b5578b0ebbfb497de3297fcdf9d0bcd22b47355fee11b65d8e38947531d` | | 最终 Serial | 9226 | `875aef6aa3a572fa0b744e0fe9e3aca19f29698a6442355687e0d6fa4aea38bd` | 构造过程中分别执行三类离线回归:每个根用 Horner 法代回为零;每个 Stage3 逆变换结果再次正向计算并与目标块相等;Serial 再正向执行 Stage1 解码并与 payload 相等。随后再用原始附件做最终原生回归。 ## 验证速度 “最短”和“最快”必须分开讨论。KCTF 的最终 Serial 长度恒为 9226;这里的“最短”只指 Stage4 有效十进制文本。前面已经严格证明最小有效长度为 6896,也就是规范 6891 字节文本上恰好增加 5 个前导零。至于“最快”,不能从少量 wall-time 样本里挑一个最小值就下结论。 ### 阶段成本 低扰动观测动态识别大小为 `0x14000` 的工作区,6912 字节 Stage3 buffer 位于 `region_base + 0x10FD8`;只读取特定块和 Final state 变量 `0x9E4E4`,不挂调试器。典型成功运行大致为: ```text 进程启动 / Stage1 / Stage2 到 Stage3 开始 约 0.8 s 432 个 Stage3 白盒块 约 3.6~4.1 s Stage3 末块完成 -> Final 首 state 约 0.04 s Final 100 个 state 约 2.2 s 结果与收尾 数十 ms ``` Stage3 是单个最重阶段,Final 次之。前导零只改变 Stage4 文本表示,不改变解析得到的 1000 个整数,因此 Final 的数学输入完全相同。对所有 `z=5` 最短表示,总字符数、数字字符总数、字段数和分隔符数也完全相同,所以 Stage4 解析层的高层工作量相同。 Stage3 方面,恢复出的变换对每块都是固定的首层、8 个中间轮和末轮,432 块之间无链式状态。不同前导零摆放会改变大量 16 字节块的具体值,但不会改变块数或恢复模型中的轮数。 ### 固定 CPU 的主线程基准 为了排除系统抢占造成的假差异,后续把 CrackMe 固定到同一个逻辑 CPU,只测执行 Stage3 的主线程,并同时记录 `QueryThreadCycleTime`、`GetThreadTimes` 和 wall time。四种同为 `z=5`、有效长度都为 6896 的最短表示分别为:首字段 5 零、末字段 5 零、分散 5 零、固定随机 5 零。 用 4×4 Latin-square 让每种候选在四个运行轮次和四个运行位置各出现一次。Stage3 cycle 的候选效应为 `F(3,6)=0.183, p≈0.904`;wall-time 为 `F(3,6)=0.094, p≈0.961`;主线程 CPU-time 为 `F(3,6)=0.106, p≈0.953`。候选类别只解释约 7.5% 的 cycle 方差和约 4.0% 的 wall 方差,而未解释残差分别占约 81.6% 和 86.5%。四轮中 cycle 最少的候选依次是随机、末字段、首字段、分散,第一名本身就在换。 ### 首字段与末字段的 8 组配对 又做了 8 组 AB/BA 交替配对,只从 Stage3 第一个块完成计到第 432 个块完成。定义差值为 `first-last`:主线程 cycle 差均值约 `-4.54×10^7`,相对单次约 `1.15×10^10` cycles 只有约 `-0.39%`,95% 区间约 `[-1.08×10^8,+1.73×10^7]`,跨过 0;wall-time 平均差约 `-25.1 ms`,95% 区间约 `[-58.1 ms,+8.0 ms]`,也跨过 0。`GetThreadTimes` 的主线程 CPU-time 平均差恰为 0,8 组中正负各 4 次。因此只能说首字段方案在这一批 wall/cycle 样本里有很弱的偏快趋势,不能提升为稳定、可移植的最快候选。 ### 逐块与极端输入控制 把 432 个块逐块记录 cycle 后,相同候选两次运行的 431 个块间隔向量相关系数只有约 `0.06`;不同候选之间也接近 0,具体块值的“固定快慢模式”没有复现。 还构造了五组只用于 Stage3 的极端输入:432 块全部重复全零、全 `FF`、`00..0F`、全 `A5` 或固定随机 16 字节。它们通过真实 Stage3 逆变换生成 Serial,让原程序完整执行 432 个 Stage3 块后再在 Stage4 失败。即使把单一数据模式重复放大 432 倍,Stage3 平均 cycle 的组间跨度仍不到约 1%,且 cycle、CPU-time、wall 的排序并不一致,仍与频率/缓存状态波动同量级。 因此目前没有证据支持存在一个**可重复、跨运行稳定的“最快 Serial”**。更不能在 `8,416,958,750,200` 个最短表示里凭几次 benchmark 宣称全局最优。微架构缓存、CPU 频率、异常调度和系统状态足以改变小于 1% 的排名。 最终策略定义为: - **最短性:确定。** 有效文本最短为 6896,必须恰好 5 个前导零; - **最短候选:不唯一。** 共有 `8,416,958,750,200` 个 `z=5` 最短表示; - **当前 `serial.txt`:正确。** 它是“5 个零全放首字段”的 canonical 最短代表,并已原生 `Successful!`; - **最快性:不作不存在证据的承诺。** 当前无法区分出统计上稳定的最快 `z=5` 摆放;从算法工作量看这些 `z=5` 表示等价。 因此保留当前候选的理由是**最短、确定、简单、可复现**,而不是声称它是已经证明的全局最快候选。 ## 原生验证 默认 KCTF canonical 最短候选已经在原始附件上验证: - Name:`KCTF` - Serial 长度:9226 - Serial SHA256:`875aef6aa3a572fa0b744e0fe9e3aca19f29698a6442355687e0d6fa4aea38bd` - Stage4 目标 SHA256:`03f5fe9b57edff995f945c60a29814854b50a2660c61a96f1a5136be513e418a` - Stage1 payload SHA256:`dbd16b5578b0ebbfb497de3297fcdf9d0bcd22b47355fee11b65d8e38947531d` - 原始附件输出:`Successful!` 所有总前导零数 `z=5..20` 的首项集中 KCTF 候选此前都在原版上输出 `Successful!`;`z=0..4` 与 `z=21` 输出 `Failed!`。这用于确认长度边界,但默认 solver 不再提供“快速模式”,只生成最短严格表示。 通用 Name 派生也做了额外端到端回归:用恢复算法分别为 `A` 和 `AA` 动态生成完全不同的 100 states、1000 根和 Serial,两者长度都为 9226,原附件均输出 `Successful!`。这证明最终 Exp 不依赖 KCTF 的硬编码 state 数组。 稳定原生验证需要让 `CrackMe.exe` 的直接父进程命中前述 parent hash gate;测试中使用 `cmd.exe` 直接创建目标。Name 与 Serial 后都写换行即可,真实 console 与 PIPE 均可。 最终提交候选由文末脚本直接写入 `serial.txt`。为便于独立核对,当前 canonical 最短 KCTF Serial 为: ~~~text lI|0OyBs99jAqk+M(9CSXRL{4|KS(gZgluTyn8airP17Jv0tcszxNwXApuE{z`FR464(K(CWm0H9Ycy0{qKXHz`xE!k`A1j23OT|Ec$jBFgeFJpjn5+CY21T5ce7$J0`O5uF7wpRypm7cziq8VCSXA6UFLtFi(8i+{tNZjUSVNagYrnYaoVsNFrKjWAk1lF71VZAmOrBT8Eo+5sl+$V58pJNa6p23lL!OXLTrnYwVLpLalNDO$nw`86Kq(B6oCUFFWm+y`DKQUw0AVccZT9kNLewsYp2(69PrqOJHgE{{!FXp{s5AS!2NUa0lPIYHp3Hj8IFo`zJvO0QcEoMeKuKgaQ1DM`tDwTwNFuTrZSa+mS$esKreH5(OQqcFwuH6w42NNZLVU5gHS3yJ$voe|WY$X9cyYT$||8M+(0Zi`W!pMtXu9+m6ULOyAP4qPD+w1TLqFeKqcZ584VUsY+k1LtYSso{rXqp339c5KcaN2a1Z{u5LNvHKuTZ9c6qq83nDXiEuX+JW3`9J0TtP5NkTc3z3TXkwDOSna`va2cKTULwaKuIJsx1zEn|tuXsmYSOTw+16+IRvay+Xk2joBBM!ITASO8BvcA{+QH8pT{SC!I0pxM9rLriF9ew`ceiN$HkTQNm0I0TpoiSSMBcVx9C3`wySBr+xx(mKwBme61BWRqDazH$o(eZKm!TA47C6BWF$X8K7ZgBkSCTca`m`07wZ3O8gvwK5SO4t89lmjcWJ`6ATBkzo$lJSKNw6c`090noBDH`E4s7r0$(N{(txI2pDKyDlY0$SluCsnIm3(Aq!!0+ysRMWzi(l4lLN9T`jxRTm6X+M3EHM3sg8HFEjmYM0zRlrq8q0Jeo(Nq+CEwQp$Ds8+qDtDv0Wg0rpt4inY3NwJwxoQvmemgDX8LE9LliXYXwvq3Cg22$6gk0pOoFVgm$a4w4YI$97TlIp8PzFcsMDzNpHFi9mkwMw$S7T|o9MtissCY105sCH`4Xs3FSXNAYj3oCs5ucsOFLOz2W3yUeN`Ug0gU+igqxHkL!(+Rge+cTi8yTD1UuNsSS{E`ipR2Oz4vPkWTS`Y9BRHR0Da9r8XwpcuptSkTSmgeLy376eTYjzEP7g0MVk9+jO!sI6wg4qJ0v|eloEcTN106{776{5ctkBDXovvZ6rW7V2V6T9XU$EqiD|Q4N!a(y0csC!A5UYt0Pj0IrYOkC`O$668vS2t(ixpYrTgXU2Ht9pworQKz0MD1J57ycAw$23KNTom{eLr1`ununwQ4g{VrjQlcK$N7A(H00yIKmTEQIX8xs$|cAqONZCVC15!HmISSx$XPYENr|7T`8``T5CgnjFnR9juxe(Ik9K(vsHx5WT7gx5PxwJoU1oN+(Fq{a2T{Lo|YMP$(p+2(XHCx9`0ZLP7YUC3clcIH0qL(E9HWLKJL49ulBMvxKPA5$cKEBzL15AJIrWKn5c!r+XapgPTK{kCaxW9gPpt|L|NHEc7JYPVeJ!REJ6JQxFVz5Akw$(DLUss!uB5CZj8X+NPJoraMJ31(BJ({(3q7izzTCHQSqg+lxZueuT(CwrFa8NavZw571yWt7icgz0AwiEBn$$MC66Xgi!LmRo|YDNkepyZqCmK9xa1B11(`v{gtQeBLOZLea2tyr3YCMt1Hw0EUen7pCEIiBMc`+WxcFkkyTliQeLEBH8ymN(0z++Zg5m2ENyC48W!4oCl|MaEJ|E|uXIiXMeYFFIXJsaCIxX3CYME9ns||XJiMaetuWmsIaFjVIRv(00azW!{FZ1A4+|JHlTiUZ9cUIn0v65uw2rP1`+sSL2x3e0H4I`+KirNiwHvWoCZe|SFI{LcXxwCKR8H|Jmyyj4mJX|ius3QlrW{MtZL+CQRD5(8sPs9U10(cZHjS8$8EBQy4V5sZg!SmqCRv|u3N69DPcMSZ9jZ3+Dl8CWw70+sH4SsX3O33!H8o2H2wITRUj(wm|Syt6J|$3wcyTzA5FU(Z8N4(BIp36iqLNJgvkmiwMJiyxSuqw8LVITprxzOiQOWluJ${5E68cqtvTEFBUWAFElZW0z2+|0z+qgD$8z(wBC3i{aUuVT+yWUsHWUpr40EPmU{kpi88Por1tj68z{D8Nrs2exrz`ORR|W0IYo5!K9vCWCZWRHyLVDjF`(1eI93tWQ{0Z{69P$BmjuoJ{o`Ez9QHQ6OE7nrRJKzAD!3|SxHS0pJFjrC3qwXBqVEMYw7NuYKA5$c5l7AgtipiO1aMvBQip$0|XZHq361mHV9Xouit$rwvvFNp9KXxg{7$wwt+rpnDiK2PcNxENezoU2TQEzVZjRHRTaH6SW92psvz7$HymwK{spQkCZmptyMDyiyqI9n{sMEWkHOTKvRxFls|524ZlY!Ru+UsEa85Z9XlxjMXx|vTWXTq0MwxzcVB2FJYPwIv1wm6B38KecwjyF6Ztp$qr`1kHr++055SNg7iU|JO2u2J6Y6XWsKO8gsOn6MRPuv9TtkJ(lja$`JRqJwsFC3IBp8cLCHTvONym{2+S9URV|jJaLqrDCX7VTxeLcx6pKVY5DQX3wA9uB`WkSnwP7+1sgJHF0It!A8Lx7Mv$3RDHcJgJHI6K1lAE02MX9WWn8v!zMVa4M(uaRWB$|PLkVnjvnZST(0oIE0LNYOIqEn`y7MCknrg(6tovBpcsy9V$q08$5pILj{tVKO6ZoJOuZlm7m2aP$avU8(!wTswQN6J{$J34mE1w|D28X4WE2Owjz`kSlIosHgcnH69V7urI92lIt(9`qy$E89tlCB!ti9U!8`lg$xEv2OeMOVV!!RZe41MnN3L99n+(|rumvwWvMTPQHcN2IKXTcQr{VFKklo+UZ(E3l7T1Ny0!PkXrkjV`iwF3i80kaDxYnD84Ua3q0ZmMNPIJuT2PP92j!HVckSnZ`wr!TV4z5!V+1qY+i1z62JZyp{6D48yVmrKZC$z+vuO36wy4P9(algiKe0yPc$0sgQECS2zwg6lQoc7WZcqavn7!gLEpaa9HvLk$a|FKm9n91D02LMmF$$w!eM1gINe!(x0V1|Vcl{|AOI2g9aWVIEA!DePoMi32jogS7+LxX4HeTm0gIKpDy9JP6AuKW9(!$JcZn94PO$DkTpYgiKcNuDA+PvMixN0ssvIYr4L!6cFumO7O|H!Pe4XQFaYu7xMBrkDWeZ|s!(F1KNFIIHSZF`!R$H4(TrUjEnir+|65U5+kP(|jiny8l`XcVKSse2gcayk|194PD6q8g8jP|zX5||KrIRKmiSm$sRC`qnCj`28Y$yEcyps+w5iMt1JNytN(X0J69avBDNkp5{YQpuXu{ej$Eg!OcWHuKc4WT+Sw8(UCpBlD!||BiS5zCjHBs+2MoZ8JI3RYqZ7w+qjIw7Ayy9526ti1jX|JT0MMg+pnIn9D{Yi`5M71398JoW(iLPOgsJTaimQZ1PCxlCvMl73U7{QXy3suVErDL`4w51Kl0L6FV0O{aQwYT2HEu97qTIAMB{Al!$so`uSBc+W1UvERCARFoOt{(oV8MS$5c4I!qLI17q8QBQ9Dsa7{|P`HUimvYZT9IJ1z+o{a0PPzQWHLSsLg5r4uZMeacI6DB!(wvApCX1D3t4c`qU0(Mp+XWuc+IK0aYLR0c2`D`93ejVc$!lNqHBz|6IrqA$trA9gcBcZe5jpL3XTyHv6qZlV+r`js19jRRrRPCLeS`|6aU!qkM$S+y6yeFSOTxJtlIrELKRejIru0CZHEO54mWAkwo!kNHl!|pyB{gaLv0uEyIRkpB|S6+q|NsQDKWZ{KV9i9ZK4OQuMHOe$6jV33EtIoWcYeRgeuO|JYeAzrr{eccJL+KXtkFrjiQ4Zp(66P|cD4!NipwU7knCEYPkWnqRj{+EWmUekzvci!o1pVw7Ak+F5Ml9Aux!rem39oL3WDoRSlmoX4NURSSHSzIW60HoWjSVRzY!7WzFjYRizS9y(JDDQLZ{IZO`CN+0TpmUnv13F`|oSDOi6iEu0I40sIt34(mN38AzSXo(g0BgpzQomZY1QmFQS0||C$AqnmP(`QVPLz8$5ZXo0qRqZc4aj09CHcNXL5OiM(0m7RLMUkZcDc+sEw2!KHO9mzOM|MV45z83Z1Oosk4xQLCzYa4Oyg7r0mH1kEQ3E4$+(p7mZRM2`mZTiA3(6a$ma9LLr1FP3`vV{3ZXViUc$o92x9UErz$nr4PSLInk5ArKHHHKwezqpj|R2gg|SujVYV{8iguQ1w(l6sSn!KmD6n7sL0gx`57D4B8`q6(QjsQ$sDtlEEm9pNosDcFHoi9nmsnH$XHB0W2qgm|qs!(RQU8xZDR3zDm5J0YTUIKa2AqksL{O3iwJqK1xCFqgqLF+yYRx9S7{0o1wVlHtHp+Fg9|ARcQ1Clz{4!geB`L2emzzeRU5LAZr9PBMY8BMmezvMR|ujYJP48xyIPj4eJWviil7IFBQ{RHu6|VnjJ{CIWmeOz64yuZxrYDwDuyxNelPt$irqLlF0eBgQx|k2oHFlWVMJc`97k4a$D`QDaljyu7e0ZZHWV5KHQ8{4VT(jpYq!1aTMt9iX87Rk9FAK8aa0Za`!2z1`03sRgw|7AqmRXZs2|ZPB9kgr+6HT0I2H$Jgk{ZXj4Sc2Vi|A$+|6e5r`!rei$!c|BXu8WUl45VSt9r1s60Awex{r(05aOpmPqQ8|26k9TFDJVt{Dlrv{Bmvy!7|DtuEHsZ1Jk{FWSH10enJOwMVXN6{9OJDI9Otkj7t+5qEHuMYl9!UIAspXB3pUnQoRLHpisQ5ZH4B2yVtwkWB7N!y1IDgqSpiys3pFqEq6skk800mO9w5gZ(n2XWEvyRHSPF5VxzsRJtm7E9Y22678lrWavq5pZyUZ$6wew+gNptcanYaQ4jayOCE(3$u4p{OSJm5XV$5l66AWg8rnU69k1TvRe{0!sxPnKOrWUPvVTIxLD${rFQiLMV9HvVl{ro9m5MWuEo{J`pVqOsFq9R`J9JN|`8EH$w8n!vTD2OO$emQ6`U|PB0{PnuBoFOum!!ZuRqOv2wD4l3$e0Js7!7kiH7+xL{`7{|IHos4SaZTWXr!mY1jVe{X6y0kW$q9POlyvacwvm0VpQx4NLjExSD5lgH+gaEC24D7sZtOR3njiyzaroYPyJ`$ZrFmgYLEOC3`7$owTi!A|2u(e!uzxXuij|+au28c0O{W{R(vEo8RqcmLSsauXc`nR6I51lQ4q0wpr8B3OMiVjV82vM7iLoK2jysjLe9kU6WDQqFReW{oSkN|Sl+gwX68nPXssZZVtk34izn1ZWzjucQ!n6ILk428O4(Dn8z1T|4juA!sMR3CeIq$KZH7AWrTozRvjKg478SZ$SOa4pEyWpqY+$m`+i!rA$tiqziSaEAvqN{Y7$kj3898r3p0ZgR4HJWYP|iT4iKVs(+YasL$iUA(J1WHYDM2FInYlc(c$+SBxDKa(I9oC6M67AHY!Wr!ulw+MN3zJM`t1`ZISe6Me0V5KjeNRmXYotnYyR3Sej!Nr7AZua7tj|xuaWMMBM!vp6X|n1(pENm17TmAIr5$HWAs5OangXVoX9YV$IB8mPEMlOOgkF2krW9SBX6I93YU+llHeX9|nyDiQKaos39!YwPwUZExDF+xMlU`NwneXk3TYjA|pzSLIEBO!0AiZnz7+uE$YkXlCN7WDqnw$4FtgcVBOUy2l8tB`ZtM|zvv`tQoB4ZzaN{{5tyRvqxlA{B8$iZM0j0jkPQY9!9qJo(HCiL6|JeFxX4rNAlIVuMmJLNjqp4R|mDxRDPtUgR{p2|zl3`PVxB{HkzJgn`M7zk5RvVaPB(Y(Ak`TeLYTpq5SBPX4vP(t9IH8vOSq5BoQPgX03wTi{r4RVB{J{gVksB`JSaRTm9r87qjKYzwVOLyLAq`pyatng`qv!TAyxjv9gkxHBI93PwYjXoMcv4A97wBtq9sETvXOOCv$xmw4|z0J4P!glNPcFsR3o33cKvcNK8BrIzyiFA`8sKt907SRXR{BxII6`L`4uF6|+xSHTlzXTLz7!p1yI(a$wJgsjQzo{Nxt{(!0q!n!(UKoMJ$V6Ue7jpEYAeIS{9aveKTl6(!sKcVuuJeCa{O7nLcMQS7a8DQkHuRMKYaoJEAjirzlvOnRL9Eu15UP9UzSAp+S+S(eBpBIIigPF!B2Tr(W1j(Eg$MQw$ViOU7nSr7ykTWXjg$ypWpDKpHYqLgTZEsMOiYPNFWmMzLESrEMVRO!2ureBa|$rTF5V9nDe6szT2TR1t``5H4C`xYPcnDr88YX${mes7OrPr83EQmLuE|K{i{UxDF866+ZmrQMRVurLVBIeV{$Rp0D1X!okXj|MT{n7Cvg8E9CCIvcRcr3$IRNn7DtmQ!gMaVOHis1k4j1y`H$oz6n01kTn9PoMX!oA6CvPJ{4Xu7mDZ(q`Wtu4o7I4AHaw5+!19qUj$jJJMFwF1nJ7g5uO2OiwvKe{ISADZk!20oQ|X|267vgJFoVQ8ex+BUvv+reqp14$X3tkV0iPpg3nY{Jy{9vkxQ`8U3uO3jnaw3yQ|L208zNSi6|FMVTtkkS0osY0XOuVVS0mT7zk1COL4yOYDq3Y73Hpi0w6ivHJBTRu$ZH2DO7TOc1|A|jwoQ7ES8xzp0gio+Xtql9NnK601X{k3cELEJ|puaoYyap+pYNpC!ZM1CUVezcs7kapQYlyPLcwTMjSekV4ls3|vlx1Tq+VR8SgUx4KDoqgw|8OZz{QHkW|HxY4lt26ESCC4i+ruFR+gI9k4LBeLD!zzHalO2cF2C|tuB`02L$1O5pjV++VCRKOkgAu+IzEP(DR79scITHVLz8r{pki6R3xXjWmAkD3N5UC322ox3qiHX011vm84X{H(oyRlSwzc1|9tL1eBnEYBWCI7rAC!TF61sRQ!T6vog01iaMN`uznSCC`|2p34Ez+t4vz4kr{${CzuFMU2pA1oDD`Iu4RuDEIRZNT!VWQc3XQCqiTKFgcNzLE1Q3s9klmQC$JDW|PNF5MgIz4BiIlF+uqxoY{DMWryHBEi0Yuu2P5SczA!C8D3KZpeWFwKl4!zYy(ur1(Tp28SZFwTcP5Q1cqoz3zaH5aLa$5sI5Pjn(qwzQTsqeQwIksWEj(k{uXe`MMT$9jQLRBsZjxCoOPImoZ56xVZyItK$Lpi1CDLl4mtxKkiD3or{RT4OK|XpTMRvgvHlPIeucP25`KT$R|7mesm|9u${0zNcx{LW2Epx$So9ApIVDCx$4rSSlU7JQYn9jI6|ax6(zvplZVDU$+{Jy34SFB7m$p(sgvrj1ScHvIyT4`L((PWtmM3k35VB80mKI!qr66Zc4yl4(T!`Tl8r!Ui3V5J8C0ja91|VAXXsnYPaoAtxI6YCBq{2W9!Fw+jeIggV9a59+uCo+nkZoLW63tRM7pOR5$sYUqPqgmgNpOe|Om0J5yXjNW9$O3v+r``waLr6v{3N2xq0oVMn4tHZ9+xJQ36VrRn+tEeXa8`TUu{P{FYB8!azZOVeIBI|jeOMqZm3NU9kOOX+sD5CI`Vaqjtci9tjT0o2snzo8c21SCT+pyPScFCj{2kWZ{LNtpn{lrlcN+qlB8I6(5IVP8e5LWcHc{Y2Ok{xTHSKLelVj0zDlUitoClYl1t|SZuIIgL659Kw(qE!m|ts{U$Z5N5YU|k!ijJwiR$pqc9RTt6`gM6Kry(3m`n6`4ITt5Ivvvzq0DvpQU|Z5`FI9JgKOeQ5W1$w4R5vjU|NvS(zuoBaNPOa6CeLIOSEC2C9a{8z|602o99qZX5tFBR|uxjqQ3mtig`xXQZomkcokyqTLLLQqgM9v`8om(ujt9zo2lj25C{MyPQq6eI59l+pzCxsXO{+FvNkjuCmpipx0cVUSK5DYVvT2i7s7O2D1vnSE0|0rzNNsQRe`ZiItm8gtXxVaaOUgQC6yH5$BFl7ADZS$mL(pm2H5Czt3{k5xx{ZK0(N`a2o|p5rPay4sLWQv|jU2N4`ZS!XWvvva4P5Mk7Ciz6vmKBnvDJZ0kylZMVnq0RT2Mt3eTx8syPk8Nn95WTkr$gDrci5x1k+jsrcA9zX{iwLn9WQCj`FY(!vCs6xmKtT$sAUJ+8xUQmrBETP(4K9JMw3wVrzSR6Z(4sVMWrTZmzN9V88l9+NJ32CgVNtN(g9kYFggP2XKAHsAHsi1!vJJFLSix+0!aTv93l0cKIoyXl9FwwCY+g{r7`cWc84wlnwUlSwt2Cz1YAALgWlalHx|6Nw0wKXipsQ+Lq8{VcQRkBX5mmn2$2rsZONEx3eqmk8BN8FvB{v+YaZlOmeJ9FHB082ukUW2nxzcF`cc(K277nngYH+F3ZFa5H{qRaO$j906r|sKji{D2kOyy9RS{3R2ZsXkCqlAyk$`2F{z`rLTpBDaEM75kJI9x5zAql9P!jEZ9EB2iMPICa4gNUTA8c!CysQR|q3iMsKSgCVIv$ct9x+|U2BHg(clui|S7swvEvIDR!CcJIQoL9Z3n`eL4+3wK5FeD{aFeB02p3nP`7|X3e032oK71ElCPE2yQKe{NF!MAB2BH`lA0j+(RYsmpn9U3CHsTc`5vBsSNaU0NUBKKPAXqu6L+!kpT7U0(R{FEN`AIIXUKBunx6D5PV6RFc+(S5sg!CY`8YvnCHgWJ`3SuPC2Ow7Apy8LyLKM484RTxtaqLRuKtH!L5Szz+S`ecDHm5NZLL0n3nyDoT8U(tJaJHmaC+DVWD3vCuI6yx!2N+vy{NK|sicw(oy6X{I2D`c15qYMl3qkceZ6iJMkcWw+0UsUFk3N6NevN`U0Q{I|Dc4(z1cZLWFe0eBz!cav1BrZ6QY5k8KrK1cvOykpxu3Y8|1AapY8NNVqeuoUrov(VUr(6E7NojSoR!ScPvj9Q22X4aFOR3CgaZxPy+{XXEID09BeK(9UkS2O8{FAEg7|OViBKaE$ZLWtz3DJ{F0p1J3a+kOIcC`F3i9aT5HWKAI92OTcrUC(snS`IYH!vupPV0iAzHO$nK5XArtt7XpB$V16|iMxl+jq!aWQ!{xaVE21D5lQA7PVZCvUzaYuuwkwzyOSIv0XDQ8sY`O5S853mS74n$MKosDM5UEJlwKto|`{CQpXOkcu4VIwqFwD6xlnE+|8aBl1suNQB9SSVLyO|z!(qHItpiUZPD0q`zpM9VqBMyqjiQ{+(4XK(z+Q6LE0B(Q$wz|g1OTtkXtBc+6H2iEiQDo$|${SxVxPYSSk|4|vSHXx`8oAVv{pCZMrFJuoaJVUsLIqPW|npuakg4mE3cDr(vZ$82lKBJIl1|! ~~~ ## Exp 下面脚本只依赖原始 `CrackMe.exe` 和 SymPy。与早期版本不同,它不再硬编码 KCTF 的 100 个 state;`--name` 会实时执行已经恢复的 Name→states 生成器。对可注册 Name,脚本默认选择严格语法下最短的十进制表示。 使用: ```text python exp.py CrackMe.exe --name KCTF -o serial.txt ``` KCTF 当前应输出: ```text name=KCTF name_hash31=0x00231DCA states_sha256=c7a0133ab4bf7b1c03880708762387b6200f19f2978f77d3332190ba943be286 effective_length=6896 target_sha256=03f5fe9b57edff995f945c60a29814854b50a2660c61a96f1a5136be513e418a payload_sha256=dbd16b5578b0ebbfb497de3297fcdf9d0bcd22b47355fee11b65d8e38947531d serial_length=9226 serial_sha256=875aef6aa3a572fa0b744e0fe9e3aca19f29698a6442355687e0d6fa4aea38bd ``` ```python #!/usr/bin/env python3 """KCTF CrackMe solver. Requires: pip install sympy""" from __future__ import annotations import argparse import hashlib import struct from pathlib import Path import sympy as sp EXPECTED_EXE_SHA256 = "338f493766cfc94b6b075138f37cead0a2516d61fc983960c169b7ddf62be760" DESC_VA = 0x4240A0 BLOB_VA = 0x4D40A0 def name_hash31(name: str) -> int: h = 0 for byte in name.encode("utf-8"): h = (h * 31 + byte) & 0xFFFFFFFF return h def state_avalanche(value: int) -> int: value ^= value >> 16 value = (value * 0x5BD1E995) & 0xFFFFFFFF value ^= value >> 13 value = (value * 0xC2B2AE35) & 0xFFFFFFFF value ^= value >> 16 return value & 0xFFFFFFFF def derive_states(name: str, count: int = 100) -> list[int]: name_hash = name_hash31(name) base = name_hash states: list[int] = [] seen: set[int] = set() while len(states) < count: candidate = state_avalanche(base) state = candidate & 0x3FFF if state not in seen: seen.add(state) states.append(state) base = (candidate + name_hash + len(states)) & 0xFFFFFFFF return states class PEImage: def __init__(self, path: Path): self.data = path.read_bytes() pe_offset = struct.unpack_from("<I", self.data, 0x3C)[0] if self.data[pe_offset : pe_offset + 4] != b"PE\0\0": raise ValueError("not a PE file") coff = pe_offset + 4 section_count = struct.unpack_from("<H", self.data, coff + 2)[0] optional_size = struct.unpack_from("<H", self.data, coff + 16)[0] optional = coff + 20 if struct.unpack_from("<H", self.data, optional)[0] != 0x10B: raise ValueError("expected PE32") self.image_base = struct.unpack_from("<I", self.data, optional + 28)[0] section_table = optional + optional_size self.sections = [] for index in range(section_count): entry = section_table + 40 * index virtual_size, virtual_address, raw_size, raw_pointer = struct.unpack_from( "<IIII", self.data, entry + 8 ) self.sections.append((virtual_address, max(virtual_size, raw_size), raw_pointer)) def read_va(self, va: int, size: int) -> bytes: rva = va - self.image_base for section_rva, section_size, raw_pointer in self.sections: if section_rva <= rva and rva + size <= section_rva + section_size: offset = raw_pointer + rva - section_rva return self.data[offset : offset + size] raise ValueError(f"unmapped VA 0x{va:X}") def mur(value: int) -> int: value &= 0xFFFFFFFF value ^= value >> 16 value = (value * 0x5BD1E995) & 0xFFFFFFFF value ^= value >> 13 return value & 0xFFFFFFFF def decode_record(image: PEImage, state: int, round_index: int) -> bytes: descriptor = int.from_bytes( image.read_va(DESC_VA + 4 * (11 * state + round_index), 4), "little" ) offset, size = descriptor & 0xFFFFFF, descriptor >> 24 raw = image.read_va(BLOB_VA + offset, size) base = ((state * 0x9E3779B9) & 0xFFFFFFFF) ^ ( (round_index * 0x517CC1B7) & 0xFFFFFFFF ) return bytes( byte ^ (mur(base ^ ((index * 0x85EBCA6B) & 0xFFFFFFFF)) & 0xFF) for index, byte in enumerate(raw) ) def signed_bcd(record: bytes) -> int: count = record[0] & 0x7F digits = [digit for byte in record[1:] for digit in (byte >> 4, byte & 15)][:count] if len(digits) != count or any(digit > 9 for digit in digits): raise ValueError(f"bad BCD record: {record.hex()}") value = 0 for digit in digits: value = 10 * value + digit return -value if record[0] & 0x80 else value def polynomial_roots(image: PEImage, state: int) -> list[int]: coefficients = [signed_bcd(decode_record(image, state, i)) for i in range(11)] x = sp.Symbol("x") polynomial = sp.Poly( sum(coefficient * x**i for i, coefficient in enumerate(coefficients)), x, domain=sp.ZZ, ) factored = polynomial.ground_roots() roots = sorted( int(root) for root, multiplicity in factored.items() for _ in range(multiplicity) ) if len(roots) != 10 or len(set(roots)) != 10: raise ValueError(f"state {state} does not have ten distinct integer roots") for root in roots: total = 0 for coefficient in reversed(coefficients): total = total * root + coefficient if total: raise AssertionError((state, root, total)) return roots SBOX = bytes.fromhex( "96e86da36f4d5ed081a9829afc384c27fa95bc715b70322bb0a5481d3eb700e6" "354f660e8be3dceeb28ac9f70123d872030a252d778f2867f36a4a2e26248c85" "bf163acb0b6483451e6b9f9104e118c369888973dafbc0de94448d3790eba15f" "785dcd7fcfc17be29e43f656a636b9c5e7f4e5cc0c41d686ff9d58a43b3420c" "762c8800222bbdf6015ae9907746cf85940a2537a2fea1146ec056852a0141c0" "98ed3abe9982c65d5a74b0fbeedfd19db79b8f5c642e0f0aabd50514eb3063d" "3fc4ef4910d18747c2d9a89bfef2f95413ca75b431337e6e762aceb69cb10863" "21925c93297d1b6139305aad57d21ff1af7ce455843c1712d4dd0dacbad7b5971a" ) INV_SBOX = bytes(SBOX.index(value) for value in range(256)) INITIAL_KEY = bytes.fromhex("0902ceb5593c04834174cab1171b4ee9") INITIAL_BASELINE = bytes.fromhex("0c90f0b16813bc8864c5cebe2dcfbc2f") MID_KEYS = tuple( bytes.fromhex(value) for value in ( "34bc76cd758317f29819e3ceafb42e14", "b788cabb38f694e5fc81fa2de51b9a3a", "0c3f427195ce62710c7d7bd7ebfe81a0", "63337d33b15bac13517106acbb157f21", "f7504e4e09eaf7bfc32077aa16ae6a5e", "6ba71e00f1e31d4865e357dd6fb8c434", "5dccb91e3712fe552f86b48aefd77cf0", "c29175a76e25ecab5ea9323e1138ab8c", ) ) FINAL_PERM = (0, 4, 8, 12, 5, 9, 13, 1, 10, 14, 2, 6, 15, 3, 7, 11) FINAL_KEY = bytes.fromhex("b953e4d28e4bc9471ef79b0c0a299327") FINAL_CONSTANT = bytes.fromhex("82e740631138c30f7ee6780386670a70") MIX = ((2, 3, 1, 1), (1, 2, 3, 1), (1, 1, 2, 3), (3, 1, 1, 2)) INV_MIX = ((14, 11, 13, 9), (9, 14, 11, 13), (13, 9, 14, 11), (11, 13, 9, 14)) def xor(a: bytes, b: bytes) -> bytes: return bytes(x ^ y for x, y in zip(a, b)) def gf_mul(a: int, b: int) -> int: result = 0 while b: if b & 1: result ^= a a = ((a << 1) ^ 0x11B) if a & 0x80 else a << 1 b >>= 1 return result & 0xFF GF = {coefficient: bytes(gf_mul(coefficient, value) for value in range(256)) for coefficient in (1, 2, 3, 9, 11, 13, 14)} def shift_rows(state: bytes) -> bytes: return bytes( state[4 * row + ((column + row) & 3)] for row in range(4) for column in range(4) ) def inverse_shift_rows(state: bytes) -> bytes: return bytes( state[4 * row + ((column - row) & 3)] for row in range(4) for column in range(4) ) def column_mix(state: bytes, matrix: tuple[tuple[int, ...], ...]) -> bytes: output = bytearray(16) for column in range(4): source = [state[4 * row + column] for row in range(4)] for row in range(4): value = 0 for coefficient, byte in zip(matrix[row], source): value ^= GF[coefficient][byte] output[4 * row + column] = value return bytes(output) def linear_forward(state: bytes) -> bytes: return column_mix(inverse_shift_rows(state), INV_MIX) def linear_inverse(state: bytes) -> bytes: return shift_rows(column_mix(state, MIX)) def initial_inverse(state: bytes) -> bytes: nonlinear = linear_inverse(xor(state, INITIAL_BASELINE)) raw = bytearray(16) for position, key in enumerate(INITIAL_KEY): transposed = (position % 4) * 4 + position // 4 raw[position] = INV_SBOX[nonlinear[transposed] ^ SBOX[key]] ^ key return bytes(raw) def mid_inverse(state: bytes, key: bytes) -> bytes: nonlinear = linear_inverse(xor(state, bytes([0x40]) * 16)) return bytes(INV_SBOX[value] ^ shift for value, shift in zip(nonlinear, key)) def final_inverse(output: bytes) -> bytes: state = bytearray(16) for position, output_position in enumerate(FINAL_PERM): state[position] = ( INV_SBOX[output[output_position] ^ FINAL_CONSTANT[output_position]] ^ FINAL_KEY[position] ) return bytes(state) def decrypt_stage3_block(output: bytes) -> bytes: state = final_inverse(output) for key in reversed(MID_KEYS): state = mid_inverse(state, key) return initial_inverse(state) ALPHABET = "Il1|!ijJL`oO0QDSs5$Zz2B8gq96nNmMWwUuVvRrPpCc({tT+7xXKkYyAa4Ee3FH" def encode_stage1(payload: bytes) -> str: values = [] accumulator = bits = 0 for byte in payload: accumulator = (accumulator << 8) | byte bits += 8 while bits >= 6: bits -= 6 values.append((accumulator >> bits) & 63) accumulator &= (1 << bits) - 1 body = "".join( ALPHABET[(value - 51 - 27 * index) & 63] for index, value in enumerate(values) ) return "lI|0O" + body + "Il1|!" def serialize(values: list[int], target_length: int, spread_padding: bool) -> bytes: fields = [str(value) for value in values] raw_length = sum(map(len, fields)) + 999 padding = target_length - raw_length if padding < 0: raise ValueError(f"values need {raw_length} bytes, target is {target_length}") if spread_padding: for index in range(padding): fields[index % 1000] = "0" + fields[index % 1000] else: fields[0] = "0" * padding + fields[0] text = "-".join(fields).encode("ascii") if not 6896 <= len(text) <= 6911: raise ValueError(f"effective length {len(text)} is outside the accepted range") return text + bytes(6912 - len(text)) def choose_values(groups: list[list[int]]) -> tuple[list[int], int, bool]: values = [value for group in groups for value in group] raw_length = sum(len(str(value)) for value in values) + 999 target_length = max(6896, raw_length) if target_length > 6911: raise ValueError(f"strict decimal representation needs {raw_length} bytes") return values, target_length, False def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("exe", type=Path, nargs="?", default=Path("CrackMe.exe")) parser.add_argument("-o", "--output", type=Path, default=Path("serial.txt")) parser.add_argument("--name", default="KCTF") args = parser.parse_args() exe_hash = hashlib.sha256(args.exe.read_bytes()).hexdigest() if exe_hash != EXPECTED_EXE_SHA256: raise ValueError(f"unexpected CrackMe.exe SHA256: {exe_hash}") image = PEImage(args.exe) states = derive_states(args.name) groups = [polynomial_roots(image, state) for state in states] values, target_length, spread = choose_values(groups) target = serialize(values, target_length, spread) payload = b"".join( decrypt_stage3_block(target[offset : offset + 16]) for offset in range(0, len(target), 16) ) serial = encode_stage1(payload) if len(serial) != 9226 or not all(33 <= ord(char) <= 126 for char in serial): raise AssertionError("bad final Serial") args.output.write_text(serial, encoding="ascii") print(f"name={args.name}") print(f"name_hash31=0x{name_hash31(args.name):08X}") print(f"states_sha256={hashlib.sha256(struct.pack('<100I', *states)).hexdigest()}") print(f"effective_length={target.index(0)}") print(f"target_sha256={hashlib.sha256(target).hexdigest()}") print(f"payload_sha256={hashlib.sha256(payload).hexdigest()}") print(f"serial_length={len(serial)}") print(f"serial_sha256={hashlib.sha256(serial.encode('ascii')).hexdigest()}") print(f"output={args.output}") if __name__ == "__main__": main() ``` 尝试让ai大致复刻了一下源码,有些信息丢失了复刻不完全,但是总体上问题不大,放附件了
回复或点赞可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
2026-8-16 22:47 被mxym_编辑 ,原因:
上传的附件:
source-v4.zip
(40.19kb,9次下载)
收藏
・
1
点赞
・
28
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
git_51951meggadf3df
非常支持你的观点!
5天前
mb_dvqqrcce
感谢你的积极参与,期待更多精彩内容!
5天前
eappple
非常支持你的观点!
2026-9-9 15:46
晨曦。
为你点赞!
2026-9-9 07:18
FinSectech
为你点赞!
2026-9-8 12:27
__阴__
你的帖子非常有用,感谢分享!
2026-9-5 20:54
pzhxbz
期待更多优质内容的分享,论坛有你更精彩!
2026-9-3 12:33
DannysMask
期待更多优质内容的分享,论坛有你更精彩!
2026-8-31 10:58
mb_pajvlyxs
谢谢你的细致分析,受益匪浅!
2026-8-28 07:58
ntdll
为你点赞!
2026-8-26 23:43
git_46185nixiak-dot
感谢你的积极参与,期待更多精彩内容!
2026-8-26 15:42
mb_lthgjpwj
为你点赞!
2026-8-21 07:32
KuCha128
为你点赞!
2026-8-21 00:12
Magic丶
你的帖子非常有用,感谢分享!
2026-8-20 14:14
mb_vemlmxkm
你的分享对大家帮助很大,非常感谢!
2026-8-19 19:38
mb_ecfxuphh
为你点赞!
2026-8-18 17:43
代陌
谢谢你的细致分析,受益匪浅!
2026-8-18 15:24
孤独的街
为你点赞!
2026-8-18 12:32
微笑:)
谢谢你的细致分析,受益匪浅!
2026-8-18 09:12
ONewTach
感谢你的积极参与,期待更多精彩内容!
2026-8-17 17:20
huangyalei
为你点赞!
2026-8-17 16:34
教教我吧~
感谢你分享这么好的资源!
2026-8-17 14:33
梧桐生
谢谢你的细致分析,受益匪浅!
2026-8-17 14:12
TeddyBe4r
感谢你分享这么好的资源!
2026-8-17 13:05
Dy1an
非常支持你的观点!
2026-8-17 13:00
backer
为你点赞!
2026-8-17 12:44
0xCCCC
你的帖子非常有用,感谢分享!
2026-8-17 12:35
git_79481gogogo2501
非常支持你的观点!
2026-8-17 12:30
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
8
)
mb_tnqoqsdv
雪 币:
0
活跃值:
(2064)
能力值:
( LV2,RANK:10 )
在线值:
发帖
4
回帖
56
粉丝
1
关注
私信
mb_tnqoqsdv
2
楼
前来学习
2026-8-17 13:52
0
number_Z
雪 币:
2653
活跃值:
(2400)
能力值:
( LV8,RANK:125 )
在线值:
发帖
6
回帖
16
粉丝
2
关注
私信
number_Z
2
3
楼
学习
2026-8-17 17:18
0
wufake
雪 币:
366
活跃值:
(191)
能力值:
( LV2,RANK:10 )
在线值:
发帖
3
回帖
6
粉丝
0
关注
私信
wufake
4
楼
nb
2026-8-17 23:24
0
Midsw
雪 币:
29
活跃值:
(1165)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
97
粉丝
1
关注
私信
Midsw
5
楼
nb
2026-8-18 05:17
0
风子
雪 币:
1223
活跃值:
(1302)
能力值:
( LV3,RANK:20 )
在线值:
发帖
2
回帖
22
粉丝
1
关注
私信
风子
6
楼
看看隐藏
2026-8-18 06:52
0
lzq2000
雪 币:
905
活跃值:
(2045)
能力值:
( LV4,RANK:47 )
在线值:
发帖
6
回帖
6
粉丝
1
关注
私信
lzq2000
7
楼
111
2026-8-18 08:20
0
OverCL0ck
雪 币:
33
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
15
粉丝
0
关注
私信
OverCL0ck
8
楼
1
2026-8-22 11:53
0
mb_rgkcnzkm
雪 币:
200
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
1
粉丝
0
关注
私信
mb_rgkcnzkm
9
楼
感谢
2026-8-29 19:13
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
mxym_
2
11
发帖
2
回帖
120
RANK
关注
私信
他的文章
[原创] 第十题:卯时·曦光初现 WP
1512
[原创]第九题:丑寅同墟·星海抉择 WP
63
[原创]第八题:亥子合辰·塔影迷楼 WP
44
[原创]第七题:戌时·暗能潜流
18
[原创]第六题:酉时·书院迷局 WP
1500
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
huangyalei
number_Z
Dy1an
lzq2000
ONewTach
教教我吧~
git_79481gogogo2501
mb_rgkcnzkm
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部