VNCTF2026 逆向题 Shadow WP

考点

  • 驱动PE手动反射注入

  • 键盘消息监控

  • PTE hook

  • 动态ShellCode

程序分析

只是最简单的一个迷宫逻辑程序,并且不包含任何其他功能。

走到终点就会弹出”You reached the end!”消息框。

alt text

驱动分析

1. PE反射注入Sys

驱动入口开始,先根据一个全局Key,对一个0x5E00大小的数据,进行AES加密(注意是加密)。

alt text

其实尝试加密该数据,可以得到MZ文件头,也就是PE头,大概就能猜出来这边加密出来就是一整个的解密后PE文件,

alt text

解密PE文件后,进行PE拉伸、重定位修复、IAT修复、调用DriverEntry,一系列操作进行手动加载该PE文件到内存中运行,具体实现原理可以网上搜”反射注入”,实际就是手动实现加载并运行程序。

alt text

alt text

总结来说表层的这个驱动就是实现了手动加载另一个驱动到内存中运行,是一个壳性质。所以具体需要分析被加载的驱动,

2. Pte hook

原理详见:https://xz.aliyun.com/news/18999

开头解密了一个字符串获取了KeDelayExecutionThread字符串,然后调用MmGetSystemRoutineAddress,获取了该函数在当前ntdll中的地址,然后遍历所有进程找到进程名为Maze的程序,也就是题目附件中的Maze.exe。

alt text

然后对该进程,进行对KeDelayExecutionThread函数单独隔离的Pte hook,Pte hook的特性就是对ntdll函数进行hook,但仅对该进程生效,hook替换成另一个函数。

alt text

3. 键盘监控

设置了MajorFunction的IRP_MJ_READ消息函数,然后创建了FILE_DEVICE_KEYBOARD设备,解密字符串得到”\Device\KeyboardClass0”,然后附加键盘驱动上。此时dispach_read就会接收到键盘相关消息。

alt text

alt text

核心回调就是dispatch_read中的CompletionRoutine函数。

alt text

获取Shift键,并且判断按键是否按下,将按键消息转换到对应按键字符,存入到Input数组中。并且需要按下F12开启输入,输入后再按F12结束输入。

alt text

alt text

然后查看Input数组的交叉调用发现在Hook函数中也有用到,接下来就主要分析Hook函数。

alt text

4. Shellcode加密

Hook函数开头通过KeDelayExecutionThread的第三个参数进行了密钥派生。

alt text

后续分段解密加载了一大段ShellCode。

alt text

通过ShellCode对输入的Flag进行加密,最后与密文比对,若比对成功,则出发蓝屏,Code:0x11111111。

alt text

通过手动抄伪代码对Shellcode解密,或者动调拿到解密后Shellcode均可,然后加载IDA进行分析。

alt text

该算法是一个自定义算法,代码量少,直接逆向解密即可。

alt text

alt text

解密分析

从上文可知,最后对Input加密的密钥是通过KeDelayExecutionThread的第三个参数派生得到的,那么这个第三个参数是什么呢。

alt text

实际上用户态的Sleep函数最终底层会调用这个KeDelayExecutionThread函数,第三个参数就是等待时间,也就是对应Sleep的参数。

1
2
3
4
5
NTSTATUS KeDelayExecutionThread(
[in] KPROCESSOR_MODE WaitMode,
[in] BOOLEAN Alertable,
[in] PLARGE_INTEGER Interval
);

那么KeDelayExecutionThread hook需要被触发,就需要有Sleep调用,可以回想之前Maze.exe程序最终走到终点时候会调用一个Sleep(0x32u),这个0x32就是关键数据,但是直接用0x32进行派生密钥会发现解密失败。

alt text

原因是这个a3参数Interval并不是原始的ms毫秒标准,而是100ns为单位的负数,所以0x32需要进行以下变换

1
2
-50ms = -50000000ns = -500000 × 100ns
Interval = -500000

得到a3值为0xFFFFFFFFFFF85EE0,对这个数值进行密钥派生得到的正确密钥:0xE7D1CC85D2172C16

alt text

然后再解密发现还是不对,此时最好需要进行一下动调。

动态调试

sxe ld Shadow命令断在驱动启动时,断点下在call DriverEntry部分。

alt text

alt text

断点,从rax拿到目标加载驱动的DriverEntry地址。

alt text

需要断在RtlCompareMemory密文比对处,计算一下该地址和DriverEntry的相对偏移,

alt text

alt text

按下F12输出[LDriver] on input,然后此时输入flag,再按下F12结束,输出[LDriver] input end.,断到验证的部分。

alt text

此时db rcx查看密文的位置,发现和静态时密文不一样,使用动调得到的密文即可成功解密。

alt text

alt text

这边用了一个小trick,实际密文数组交叉调用除了密文比对处,没有其他地方。

alt text

若对密文进行断点,会发现会在PTE函数内部的一个地方被进行了异或处理,不过是对密文前面的四十个字节作为起始地址计算,然后i设置40开始,这样访问就会越界到密文处,导致密文查不到被修改代码的交叉调用。

alt text

alt text

解密代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#include <stdint.h>
#include <string.h>
#include <stdio.h>

#define ROR32(x, n) (((x) >> (n)) | ((x) << (32u - (n))))
#define ROL32(x, n) (((x) << (n)) | ((x) >> (32u - (n))))

static uint32_t xorshift32(uint32_t *s)
{
uint32_t x = *s;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
*s = x;
return x;
}

static void gen_sbox(const uint32_t k[2], uint8_t sbox[256], uint8_t inv[256])
{
for (int i = 0; i < 256; i++)
sbox[i] = (uint8_t)i;

uint32_t seed = k[0] ^ ROL32(k[1], 11) ^ 0xA5A5A5A5u ^ 0xB7E15163u;
for (int i = 255; i > 0; i--)
{
uint32_t r = xorshift32(&seed);
int j = (int)(r % (uint32_t)(i + 1));
uint8_t tmp = sbox[i];
sbox[i] = sbox[j];
sbox[j] = tmp;
}
for (int i = 0; i < 256; i++)
inv[sbox[i]] = (uint8_t)i;
}

static uint32_t inv_sub_bytes32(uint32_t x, const uint8_t inv[256])
{
return (uint32_t)inv[(x >> 0) & 0xFF] << 0 |
(uint32_t)inv[(x >> 8) & 0xFF] << 8 |
(uint32_t)inv[(x >> 16) & 0xFF] << 16 |
(uint32_t)inv[(x >> 24) & 0xFF] << 24;
}

static void expand_key(const uint32_t k[2], uint32_t rk[32])
{
uint32_t a = k[0] ^ 0xB7E15163u;
uint32_t b = k[1] + 0x9E3779B9u;

for (uint32_t i = 0; i < 32; i++)
{
uint32_t t = (a ^ ROL32(b, (a & 31u))) + (0xB7E15163u ^ (i * 0x9E3779B9u));
rk[i] = t ^ ROR32(a + b, (b & 31u));
a = b ^ rk[i];
b = t + ROL32(rk[i], (t & 31u));
}
}

static uint32_t tweak32(uint32_t idx, const uint32_t k[2])
{
uint32_t t = (idx + 1u) * 0x45D9F3Bu;
t ^= ROL32(k[0], (idx & 31u));
t += (k[1] ^ 0xDEADBEEFu);
t ^= t >> 16;
t *= 0x7FEB352Du;
t ^= t >> 15;
t *= 0x846CA68Bu;
t ^= t >> 16;
return t;
}

void decrypt_block(uint32_t v[2], const uint32_t k[2],
const uint32_t rk[32],
const uint8_t sbox[256],
const uint8_t inv[256],
uint32_t block_index)
{
uint32_t x = v[0], y = v[1];
uint32_t tw = tweak32(block_index, k);

x ^= (k[1] + ROL32(tw, 11));
y ^= (k[0] ^ tw);

uint32_t sum = 0;
for (uint32_t i = 0; i < 32; i++)
{
sum += (0xB7E15163u ^ rk[i]);
}

for (uint32_t r = 0; r < 32; r++)
{
uint32_t i = 32 - 1u - r;
if (sum & 1u)
{
uint32_t tmp = x;
x = y;
y = tmp;
}

uint32_t rotX = ((y ^ sum) + (rk[i] >> 1)) & 31u;
x = ROR32(x, rotX);

y -= (x ^ rk[i]);

uint32_t rotY = (uint32_t)sbox[x & 0xFFu] & 31u;
y = ROL32(y, rotY);

x ^= (y + ROR32(sum, 3)) ^ ROL32(rk[i], (y & 31u));

x -= (sum ^ rk[i]);

x = inv_sub_bytes32(x, inv);
y = inv_sub_bytes32(y, inv);

sum -= (0xB7E15163u ^ rk[i]);
}

x ^= (k[0] + tw);
y ^= (k[1] ^ ROR32(tw, 7));

v[0] = x;
v[1] = y;
}

void decrypt(uint8_t *buf, size_t len, const uint32_t k[2])
{
uint8_t sbox[256], inv[256];
uint32_t rk[32];
gen_sbox(k, sbox, inv);
expand_key(k, rk);

uint32_t idx = 0;
for (size_t off = 0; off + 8 <= len; off += 8, idx++)
{
uint32_t v[2];
memcpy(v, buf + off, 8);
decrypt_block(v, k, rk, sbox, inv, idx);
memcpy(buf + off, v, 8);
}
}

int main(void)
{
uint64_t Key = 0x17658990C729C992LL;

for (int i = 0; i < 57; ++i )
Key = 0xFFFFFFFFFFF85EE0 ^ (0x10003 * Key);

unsigned char cipher[] =
{
0x51, 0xDA, 0xB8, 0x52, 0x73, 0xB9, 0x17, 0x00, 0xE0, 0x02, 0xF4, 0xB2, 0x2C, 0x5F, 0x22, 0x62,
0x33, 0x0C, 0x01, 0x44, 0xBB, 0x70, 0x9D, 0x92, 0x8A, 0x06, 0xF9, 0x2C, 0x1D, 0x8F, 0x0A, 0xA9,
0x22, 0x7B, 0x84, 0x30, 0x71, 0x13, 0xD0, 0xF9};

decrypt(cipher, 40, (uint32_t *)&Key);

printf("%.40s\n", cipher);

// ebbc8827-c040-4a7d-8bc7-0aeccb1ce094
return 0;
}