首页
社区
课程
招聘
[原创]某互联网公司自定义虚拟机保护 CrackMe 逆向分析与算法还原
发表于: 2天前 647

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

2天前
647

结论

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变换



[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回