首页
社区
课程
招聘
[原创]某互联网公司自定义虚拟机保护 CrackMe 逆向分析与算法还原
发表于: 2026-3-27 19:42 2480

[原创]某互联网公司自定义虚拟机保护 CrackMe 逆向分析与算法还原

2026-3-27 19:42
2480

结论

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

image.png

第二个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入口

image.png

加密后关键验证

image.png

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

image.png

关键常量

  • 目标数据: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盒系统

首先对我们的输入缓冲区进行第一次修改

image.png


已经发生了变化

image.png


对输入缓冲区加密之后,又初始化了一个十分有规律的数组,先初始化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


六、解题关键点

  1. 识别VM结构:找到opcode调度循环
  2. 理解流加密:分析sub_7FF6E40C48F0的解密逻辑
  3. 提取PRNG种子:从调试器获取 0x6D36E7974CECD615
  4. 动态追踪输入:观察输入字节的变化规律
  5. Dump S盒:提取256字节的置换表
  6. 逆向S盒:找到目标字节在初始S盒中的索引
  7. ASCII解码:将索引序列转为字符串
  8. 凯撒解密:发现并应用ROT-2变换



[招生]科锐逆向工程师培训(2026年11月13日实地,远程教学同时开班, 第57期)

收藏
点赞 ・1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回