我先确认调试器的实际连接状态和当前进程情况。
调试器已附加 crackme(PID 43700,基址 0x00007FF772410000,模块大小 0x4000)。我先建立分析计划。
数据段已露出关键信息(Key:、Access granted.、Access denied.,以及三段疑似密钥/密文数据)。现在反汇编代码段定位校验逻辑。
进程当前阻塞在控制台读取(ReadFile),暂停请求未生效。我先改用静态读取代码段的方式分析。
代码段是一个虚拟机解释器(含跳转表分发)。我需要精确反汇编,先准备本地反汇编环境。
模块已导出到本地。现在做离线反汇编。
静态代码已完整还原——这是一个基于跳转表的栈式虚拟机。现在提取跳转表、字节码和数据段常量。
跳转表已完整解出。现在反编译 VM 字节码并构造模拟器。
FNV 完整性校验通过(0xD26B93B6),说明字节码与 S 盒提取无误。现在求解 key。
静态求解完成,key = Vm_1s_n0t_s3cur3。现在在真实调试器里做动态验证:重启进程、注入 key、绕过控制台读取后运行。
进程重启后已暂停(ntdll 系统断点)。先确认模块基址未变,再注入 key 并打补丁。
补丁长度写错了一位(jne 指令未被完全覆盖),需要修正为 16 字节。先暂停。
进程再次阻塞在控制台读取,无法暂停。改用初始化脚本在入口处断下后再注入。
内存写入不受暂停限制,我直接在系统断点处完成注入。
需要用命令重新启动调试会话。
已在模块入口断下。继续执行到校验入口。
补丁把 jne 也覆盖掉了,导致走入失败分支。修正为保留跳转。
命中 FNV 校验入口断点。记录此时的关键寄存器与内存数据。
输入缓冲区首字节异常为 0,先复核内存写入并修正。
FNV 完整性校验已通过(命中 0x12EA 虚拟机入口)。继续执行。
补丁的相对跳转偏了 2 字节,导致长度变量被置 0。修正后重跑。
长度变量已正确置为 0x10。核对输入缓冲区内容后继续执行。
成功命中 0x15A3(opcode 0xB9 成功分支)。再做一次反向对照:错误 key 应落到 0x164A。
对照实验确认:仅改最后一个字符即落到拒绝分支。生成完整分析报告。
核心校验不在原生代码里,而在一段字节码虚拟机程序中:
结论
正确的 key:Vm_1s_n0t_s3cur3
- 十六进制:
56 6D 5F 31 73 5F 6E 30 74 5F 73 33 63 75 72 33
- 长度:0x10(16)字节,硬编码约束;字符集为大小写字母 + 数字 +
_
- 通过后输出:
Access granted. + FLAG{v1rtu4l_m4ch1n3_1ns1d3}
已在真实调试器内验证(非纯静态推断):
| 注入的 key |
命中断点 |
结果 |
Vm_1s_n0t_s3cur3 |
base+0x125A → base+0x12EA → base+0x15A3 |
通过(opcode 0xB9) |
Vm_1s_n0t_s3cur4(末字符改 1 bit) |
base+0x164A |
拒绝 |
模块基址 0x00007FF772410000(下文均为基址+偏移)。因程序用 GetStdHandle(STD_INPUT)+ReadFile 读控制台,无法喂 stdin,故把 +0x1090 的 call ReadFile 改成「写长度 0x10 + 返回成功 + 跳转 +0x10D5」,key 直接写入输入缓冲区 +0x3010,其余全部走原生校验代码。
一、输入获取与校验逻辑定位
| 偏移 |
关键指令 |
说明 |
+0x1043 |
WriteFile(..,"Key: ",5) |
提示串在 +0x2224 |
+0x107B |
lea rsi,[rip+0x1F8E] |
输入缓冲区 = +0x3010(实测 rsi=0x00007FF772413010) |
+0x1090 |
call [rip+0x1302] |
ReadFile,长度上限 0x40 ← 输入获取点 |
+0x1111 |
mov [rip+0x1F51],ecx |
剥离 CR/LF 后长度 → +0x3068 |
+0x125A |
FNV-1a 循环 |
完整性自校验入口 |
+0x12DE |
cmp ecx,0xD26B93B6 |
哈希比对 |
+0x12EA |
xorps/movaps |
虚拟机初始化 |
+0x15A3 / +0x164A |
opcode 0xB9 / 公共失败出口 |
成功 / 拒绝 |
断点 +0x125A 处实测:rip=0x00007FF77241125A、rsi=0x00007FF772413010、[+0x3010]="Vm_1s_n0t_s3cur3\0"、[+0x3000]=0x10、[+0x3004]=0x41、[+0x3068]=0x10。
静态分析通过 dump_module 导出映像后用 capstone 离线反汇编完成(进程阻塞在 ReadFile 时 pause/force_pause 均失效)。
二、算法
第二层 FNV-1a 自校验(与输入无关,防篡改):h=0x811C9DC5 依次滚过字节码 +0x22C0(0x41 字节)与 S 盒 +0x22B0(0x10 字节),h=(h^b)*0x01000193,结果必须为 0xD26B93B6(复算一致)。
第三层 栈式虚拟机:指令解码被混淆,op(p) = (((p*0x97)+0x25)&0xFF) ^ CODE[p],寄存器操作数用 ((1-p)&0xFF)^CODE[p] & 3;两张跳转表 +0x2000(op 0x0E..0x51)与 +0x2110(op 0x63..0xA7)。4 个寄存器在 [rsp+0x30],栈在 [rsp+0x40](r10d=sp,上限 0x3D)。
字节码程序(+0x22C0,流偏移即 pc)还原后:
0000 PUSH 0x11 ; STORE r1 prev = 0x11
0004 PUSH 0x00 ; STORE r0 i = 0
0008 PUSH_LEN; PUSH 0x10; EQ; JZ 0x3F len == 0x10 ?
000F PUSH 0x0F; LOAD r0; SUB; LOAD_KEY c = key[0x0F - i]
0015 PUSH 0x6D; XOR c ^= 0x6D
0018 LOAD r0; PUSH 0x0B; MUL; ADD + i*0x0B
001E LOAD r1; ADD; AND_IMM 0xFF; STORE r2 + prev,截 8 位
0025 LOAD r2; LOAD r0; TBL; EQ; JZ 0x3F == sbox(i) ?
002E LOAD r2; STORE r1 prev = v
0032 INC r0; LOAD r0; PUSH 0x10; EQ; JNZ 0x40 i == 0x10 ?
003C JMP 0x0008
0040 SUCCESS (0xB9)
S 盒 T16 = 62 F7 08 C6 11 27 1A 1B 71 BC BD 39 78 58 67 A0 09,sbox = 6F 99 C7 F6 80 D5 49 AF 64 CA 6A 01 E1 A2 3C 1C。数据流是 CBC 式链:key[15-i] → XOR 0x6D → +i*0x0B → +上一轮结果 → &0xFF → 与 S 盒输出比较 → 反馈为下一轮 prev。
三、复现步骤
- 直接跑程序,在
Key: 后输入 Vm_1s_n0t_s3cur3 回车。
- 调试器内:
bp +0x125A(查输入/长度)、bp +0x12EA(VM 入口)、bp +0x15A3(成功)、bp +0x164A(失败);单步 +0x1340 取指循环可看 opcode 解码,+0x1494 查 S 盒,+0x145A 取 key 字节。
- 离线求解(已验证输出
b'Vm_1s_n0t_s3cur3'):
T16 = bytes.fromhex("62f708c611271a1b71bcbd39785867a009")
sbox = [(((i*0x61) & 0xFF) + 0x0D & 0xFF) ^ T16[i] for i in range(16)]
key, prev = [0]*16, 0x11
for i in range(16):
key[0x0F - i] = ((sbox[i] - prev - 0x0B*i) & 0xFF) ^ 0x6D
prev = sbox[i]
四、多解与弱校验
- 唯一解,无多解:每轮由已知
sbox(i) 与已知 prev 反解唯一字节,16 轮确定 16 字节,异或与模 256 加法均为双射,解空间 = 1。
- 长度是硬约束,且循环体内每轮复检
len==0x10;索引恒为 0x00..0x0F,LOAD_KEY 的「越界返回 0」兜底不会触发,不存在绕过某一轮的弱校验。
- 链使校验非位置无关,改一字节会让后续所有轮偏移,无法逐字节独立爆破。
- 真正的弱项:全部是 8 位运算 + 16 字节 S 盒,FNV-1a 只用于自校验字节码而不参与输入判定,因此是可完全逆推而非需爆破的校验。
五、内置诱饵
+0x111D 处 (len+0xB)*(len+0xC)&1 恒为 0 → 恒跳转,+0x1136 起整段是死代码,内含假 flag FLAG{llm_gu3ss3d_th1s_0n3}(XOR 串在 +0x2270)。字符串 trust_but_verify(+0x22FF)、debug_bypass_ok(+0x2312)、backup_key_2024(+0x2322)均为诱饵,模拟器验证全部 FAIL。真 flag 在 +0x2290,XOR 解码方式 out[i] = enc[i] ^ ((0x5B + 0xB5*i) & 0xFF)。
备注:调试器中的补丁只改内存,磁盘上的 crackme.exe 未被修改。