结论
flag{make-a-happy-day-working-today!!}

第二个crackme 简单看了看IDA 也是38字节,memcmp比较 应该也是几次加密 先猜一波flag一样,算法还没看,flag是一模一样的
一、程序整体结构
这是一个自定义字节码虚拟机保护的CrackMe,程序通过两段VM代码对输入进行多层变换和验证。
主函数流程
int main(int argc, char **argv) {
// 创建两个字符串对象,都复制输入
construct_string(v11, len);
construct_string(v10, len);
memcpy(v11, input, len);
memcpy(v10, input, len);
// 获取指针
v9 = get_string_ptr(v11); // 待处理的数据
Buf2 = get_string_ptr(v10); // 原始输入
// 核心验证
process_result(v9, len); // VM1处理
if (compare_with_const(Buf2, len, v9) == 38 &&
memcmp(byte_7FF6E40F1000, Buf2, len) == 0) {
show_success(); // 验证通过
}
}VmCode入口

加密后关键验证


flag正确成功输出结果,可在此处patch验证是否为真正的验证

关键常量
- 目标数据:
byte_7FF6E40F1000(38字节) - VM1字节码:
unk_7FF6E40DE640 - VM2字节码:
byte_7FF6E40DE760
二、VM虚拟机架构
1. 指令集(opcode)
Opcode | 功能 | Opcode | 功能 |
0x01 | 立即数压栈 | 0x11-0x13 | 位运算(&, |, ^) |
0x02-0x03 | 变量操作 | 0x14-0x15 | 移位运算 |
0x04 | 加法 | 0x16-0x17 | 字节读/写 |
0x08 | 减法 | 0x18-0x19 | DWORD读/写 |
0x09 | 乘法 | 0x1A-0x1B | QWORD读/写 |
0x0A | 除法 | 0x1C | 取负 |
0x0B-0x0E | 比较运算(>,<,==,!=) | 0x1D-0x1E | 取模 |
0x0F-0x10 | 栈指针操作 | 0x22 | 条件跳转 |
2. 流加密读取器
VM在读取字节码时,如果加密标志开启,会进行XOR解密:
char sub_7FF6E40C48F0(VM_CTX *ctx) {
// 读取加密字节
v3 = code_base[pc];
if (encrypt_flag) {
// 生成密钥流(每8字节刷新)
if (bits_left == 0) {
key_stream = xorshift64_next(&state);
bits_left = 8;
}
key_byte = key_stream & 0xFF;
key_stream >>= 8;
// 解密返回
return v3 ^ key_byte;
}
return v3; // 明文返回
}3. PRNG(xorshift64*)
__int64 xorshift64_next(unsigned __int64 *state) {
v3 = *state ^ (*state >> 12) ^ ((*state ^ (*state >> 12)) << 25);
v1 = v3 ^ (v3 >> 27);
*state = v1;
return 0x2545F4914F6CDD1D * v1;
}固定种子:0x6D36E7974CECD615(从调试器获取)
4. S盒系统
首先对我们的输入缓冲区进行第一次修改

已经发生了变化

对输入缓冲区加密之后,又初始化了一个十分有规律的数组,先初始化0-255 然后加密置换一次

初始化之后 又构建了一个加密盒

程序构造了两个S盒:
初始S盒(顺序表):
00 01 02 03 ... FE FF
最终S盒(经过VM1变换后的256字节):
# 最终S盒 (从调试器中dump得到)
SBOX = bytes.fromhex("""
00 00 01 03 19 83 C6 17 24 11 A0 25 21 71 87 A5
4A 7A DE F4 86 38 32 74 FA 1B 3A 65 76 05 2F 59
C7 73 E0 5A 35 67 79 CD F2 99 F5 3F 07 8F 60 C9
70 91 B8 5B 9A 0E 75 58 2C 48 4F FC 78 0B 8B C5
CF C1 D0 8D A7 F9 7F D1 2A 41 E9 3D C0 AF D4 6B
4E F8 57 CE 2B 28 BC BB BE E3 20 23 5D 4D B3 E2
CA 33 84 31 BA 95 90 9F 16 72 C4 94 8C 1E 6C 3E
D3 22 B5 82 9D 18 08 15 7B 44 0C 46 9C 47 4C 9B
93 AE FD E6 AA 09 C2 B6 CB BF EB B0 68 6D 2E 14
D8 43 1D A6 AD EC A9 A2 00 B4 FB 88 66 AC 12 EE
E8 40 3B A4 02 B2 DD D2 A8 F0 D7 1F 63 52 3C 80
8E 1C EA F1 81 CC 39 06 BD DA 69 56 A1 77 29 6E
89 7C AB D6 0A 54 F7 5E 0F FF 8A 30 DF DB 5C 45
51 DC 27 61 D5 4B C8 2D 50 E5 53 6A 36 04 98 7E
1A F3 E7 55 13 B1 49 C3 EF ED E4 96 A3 34 42 E1
97 B9 64 6F 26 0D 5F 9E 85 F6 FE B7 10 62 7D D9
37 92
""".replace('\n', ' ').replace(' ', ''))
三、验证流程
整体数据流
原始flag (可读字符串)
↓
【VM1】xorshift64流加密 + S盒变换
↓
中间结果 (38字节)
↓
【VM2】S盒映射(查最终S盒)
↓
最终输出 (byte_7FF6E40F1000)
验证条件
// 条件1:输入必须等于硬编码常量
memcmp(byte_7FF6E40F1000, input, 38) == 0
// 条件2:VM2必须返回38
compare_with_const(input, 38, VM1_output) == 38
四、逆向过程
第一步:动态追踪输入变化
输入测试字符串 "QWERTYUIOPASDFGHJKLZXCVBNMqweqweqweqwe",观察其在内存中的变化:
输入原始值:
57 45 52 54 59 55 49 4F 50 41 53 44 46 47 48 4A
4B 4C 5A 58 43 56 42 4E 4D 71 77 65 71 77 65 71
77 65 71 77 65
经过VM1后:
CE E3 D1 2B BC 23 BB 3D F8 57 8D 28 7F 2A 41 E9
C0 AF D4 5D 20 F9 BE A7 4E 6B 82 44 9F 82 44 9F
82 44 9F 82 44 9F 35 00
结论:VM1是XOR流加密,密钥流 = 输入 ^ 输出
第二步:提取密钥流
密钥流 = 输入 XOR 输出
= 99 A6 83 7F E5 76 F2 72 A8 16 DE 6C 39 6D 09 A3 ...
验证发现密钥流有周期性,符合xorshift64*的特性。
第三步:确定正确输入
程序要求 input == byte_7FF6E40F1000,所以正确的输入是:
16 6C 31 72 47 3E 31 1E 9F C9 31 C9 C4 31 B5 B5
46 C9 90 31 46 C9 44 22 9D 1E 94 D3 72 C9 08 22
90 31 46 5A 5A 9B
第四步:VM2逆向(S盒逆映射)
将正确输入作为索引,在初始S盒(顺序表)中查找:
因为初始S盒是 00 01 02 ... FF,所以索引值就是索引本身!
16→ 索引0x16 (不是,要找的是值对应的索引)
实际上需要找的是:每个字节在最终S盒中的位置。
通过Python脚本找到所有索引:
68 6E 63 69 7D 6F 63 6D 67 2F 63 2F 6A 63 72 72
7B 2F 66 63 7B 2F 79 71 74 6D 6B 70 69 2F 76 71
66 63 7B 23 23 7F
第五步:ASCII解码
将十六进制转ASCII:
hnci}ocmg/c/jcrr{/fc{/yqtmkpi/vqfc{##第六步:发现凯撒密码
观察 hnci} 与 flag{ 的关系:
- h(104) → f(102) (-2)
- n(110) → l(108) (-2)
- c(99) → a(97) (-2)
- i(105) → g(103) (-2)
- }(125) → {(123) (-2)
ROT-2(ASCII减2)
全部应用ROT-2:
flag{make-a-happy-day-working-today!!}第七步:验证成功
最终flag为:flag{make-a-happy-day-working-today!!}
五、技术要点总结
技术 | 实现方式 |
代码保护 | 自定义字节码虚拟机 |
加密算法 | xorshift64* PRNG + XOR流加密 |
混淆手段 | 双层VM、S盒置换、动态密钥 |
数据编码 | 最终结果需通过S盒逆映射 + ROT-2 |
种子固定 | 0x6D36E7974CECD615
|
六、解题关键点
- 识别VM结构:找到opcode调度循环
- 理解流加密:分析
sub_7FF6E40C48F0的解密逻辑 - 提取PRNG种子:从调试器获取
0x6D36E7974CECD615 - 动态追踪输入:观察输入字节的变化规律
- Dump S盒:提取256字节的置换表
- 逆向S盒:找到目标字节在初始S盒中的索引
- ASCII解码:将索引序列转为字符串
- 凯撒解密:发现并应用ROT-2变换