首页
社区
课程
招聘
[原创]第三题:午时·永数囚笼 WP
发表于: 1天前 219

[原创]第三题:午时·永数囚笼 WP

1天前
219

写在ai题解前:格式不会调喵所以就这样发了,复制过来的好乱(),题目质量挺好,ai运气不错,大概猜测了一下方向丢给ai就出了,下面都是ai写的了,markdown文件我放附件吧()

# AntiAi Writeup


> Category: Reverse Engineering  

> Platform: Windows x64 PE  

> Method: Full static analysis + deterministic reimplementation + algebraic inversion + bounded exhaustive search / Meet-in-the-Middle  

> No symbolic execution, no SMT/Z3, no execution of the target binary.


## Flag


```text

flag{v4_opaque_trace_2026}

```


---


## 0. TL;DR


这题最阴的地方并不是 VM 本身,而是 **最终正确条件不是四个 worker 都成功**。


程序会对同一份输入运行 4 个 VM variant,然后把四份 worker record 再送进一个 aggregator。其中只有 **variant 2** 需要走完整 clean-success 路径;variant 0/1/3 的失败状态本身也是最终正确 digest 的组成部分。


整体数据流可以概括为:


```text

26-byte input

    │

    ▼

invertible preprocessing

    │

    ▼

32-byte transformed state

    │

    ├── worker variant 0 ──┐

    ├── worker variant 1 ──┤

    ├── worker variant 2 ──┼──> 4 records

    └── worker variant 3 ──┘

                              │

                              ▼

                         aggregator

                              │

                              ▼

                      128-bit final target

                              │

                              ▼

                           ok / no

```


最终通过纯静态求解得到 transformed state:


```text

f1b4801d0d18245b437c05da5b354020

262485099321b50a6615bbd15e601d3f

```


将 preprocessing 逆回原始输入:


```text

flag{v4_opaque_trace_2026}

```


完整静态模型最终得到:


```text

outer mismatch = 0

return bytes   = 6f 6b 0d 0a

               = "ok\r\n"

```


---


# 1. 基本信息


目标是一个原生 Windows x64 PE:


```text

AntiAi.exe

```


SHA-256:


```text

b867b0c61156d7372ac084016ad98ad0bbc78f92ccf137811996f2e51b44e157

```


程序不是 .NET。


主要函数大致如下:


| 地址          | 功能                        |

| ------------- | --------------------------- |

| `0x140002588` | anti-debug helper           |

| `0x1400025ec` | `.text` integrity hash      |

| `0x140002880` | 固定 seed 生成              |

| `0x140002940` | 四 worker record aggregator |

| `0x140002ba0` | 26-byte input preprocessing |

| `0x140002da4` | 辅助 hash                   |

| `0x140002f2c` | 主输入输出逻辑              |

| `0x14000306c` | 外层 wrapper/checker        |

| `0x14000330c` | worker / VM                 |


主函数逻辑很简单:


```c

printf("input flag: ");

read_input(buf, 64);


strip_crlf(buf);


check(buf, len, result);


write(result, 4);

```


外层明确要求输入长度:


```text

len == 26

```


否则最终 mismatch 中会混入:


```c

len ^ 26

```


所以正确输入固定为 **26 字节**。


---


# 2. 完整性校验


程序有一段 `.vseal`:


```text

SEALVMTX...

```


完整性函数位于:


```text

0x1400025ec

```


它会:


1. 找到 `.text`;

2. 取 `min(VirtualSize, SizeOfRawData)`;

3. 对 `.text` 做自定义双状态 hash;

4. 如果扫描到映像内绝对地址,会将其 canonicalize 为 RVA,以兼容 ASLR;

5. 和 `.vseal` 中保存的结果比较。


原文件静态计算得到:


```text

calc:

98aef31e 57644cb6


expected:

98aef31e 57644cb6

```


因此原始文件完整性检查通过。


这部分主要用于阻止随意 patch,不参与 flag 的核心数学约束。


---


# 3. Anti-Debug


anti-debug helper 位于:


```text

0x140002588

```


它综合检查:


- `IsDebuggerPresent`

- PEB flags

- Trap Flag 等状态


最终形成一个 bitmask。


正常无调试环境下:


```text

anti_debug_mask = 0

```


静态求解时直接使用 clean path 即可。


---


# 4. 固定 Seed


函数:


```text

0x140002880

```


最终生成两个固定值:


```text

seed0 = 0xb0adeff3

seed1 = 0x03db2dd2

```


worker 的初始状态中:


```c

r13 = seed0 ^ 0x70732f2d;

```


所以:


```text

r13_init = 0xc0dec0de

```


另外一个 persistent seed:


```text

0x31415926

```


以及 opcode XOR key 的低字节:


```text

0x1f

```


这里能看到大量故意设计的人眼 magic:


```text

C0DEC0DE

31415926

A5A55A5A

C3D2E1F0

```


但直接拼这些常量并不能得到正确 flag。


---


# 5. 输入预处理


真正送入 VM 的不是 26-byte 原始输入。


预处理函数位于:


```text

0x140002ba0

```


它先将输入构造成 8 个 little-endian dword。


设输入为 `input[0..25]`,则:


```c

d0 = LE32(input +  0);

d1 = LE32(input +  4);

d2 = LE32(input +  8);

d3 = LE32(input + 12);

d4 = LE32(input + 16);

d5 = LE32(input + 20);


d6 = input[24]

   | ((uint32_t)input[25] << 8)

   | 0x005a0000

   | 0xa5000000;


d7 = 0xc3d2e1f0;

```


也就是说,合法 preprocessing 初始状态一定满足:


```text

d6 >> 16 = 0xa55a

d7       = 0xc3d2e1f0

```


这两个 sentinel 在后续反解时是非常强的合法性约束。


## 5.1 Whitening


8 个 dword 会先经过逐 word whitening。


其中包含典型常量:


```text

0x61c88647

0x6d2b79f5

0x3c6ef372

0x7f4a7c15

0xa5a55a5a

```


所有运算均为:


- `+/- mod 2^32`

- XOR

- ROL / ROR


没有信息损失。


## 5.2 8 轮 ARX


之后执行 8 轮 8×32-bit ARX permutation。


其核心操作仍然只有:


```text

ADD mod 2^32

XOR

ROL / ROR

```


因此整个 preprocessing 是严格可逆的。


我实现了完整 `inverse_preprocess()`,并做随机 round-trip:


```text

input

  ↓

preprocess

  ↓

inverse_preprocess

  ↓

input

```


100 组随机输入全部通过。


因此,只要能求出 VM 使用的 32-byte transformed state,就可以直接逆回 26-byte flag。


---


# 6. VM 总体结构


worker 位于:


```text

0x14000330c

```


其中实现了一套 23 opcode 的 VM。


opcode 大致包括:


```text

0 / 11   读取 transformed byte

1        混合

2 / 14   rotate

3        add

4 / 13   写输出

5        更新 r13

6 / 16   更新 x34 / persistent seed

7 / 17   pos++ + 动态跳转

8        成功终止 opcode

9        错误终止

10       accumulator

12       mix

15       store + mix

18~21    四种 8-bit 可逆 primary transform

22       secondary-byte relation / stack group

```


---


# 7. 最容易踩的坑:op7 / op17


最开始静态翻译时,很容易把 VM 误判成 8 步后 OOB。


原因是 `op7/op17` 的跳转混合中存在两次右移。


相关汇编:


```asm

mov  r8d, seed

shr  r8d, 0xc

xor  r8d, seed

shr  r8d, 0x7

```


正确表达式是:


```c

r8 = ((seed >> 12) ^ seed) >> 7;

```


如果漏掉第二个 `>> 7`,VM 会在模型中迅速跳出 opcode 表,从而产生错误结论。


修正后,四个 variant 都正好运行:


```text

257 steps

pos = 32

```


最终 PC:


```text

v0 -> 0x408

v1 -> 0x410

v2 -> 0x400

v3 -> 0x418

```


这四个 PC 与程序尾部生成的目标 PC 完全一致。


---


# 8. 将 257 条 VM 指令压缩成 32 个 Block


继续观察 VM trace,可以发现每处理一个 output byte,都遵循相对固定的控制结构:


```text

op0

 │

 ▼

动态选择 transformed[index]

 │

 ▼

18 / 19 / 20 / 21 中的一种字节双射

 │

 ▼

可选 op22

 │

 ▼

写 output[pos]

 │

 ▼

更新 r13

 │

 ▼

op6 / op16 更新 x34 / seed

 │

 ▼

op7 / op17 跳到下一 block

```


因此 257-step VM 可以压缩成 **32 个 logical block**。


## 8.1 动态 primary index


每个 block 首先根据:


```text

variant

pos

r13

x34

helper constant

```


计算:


```text

idx ∈ [0,31]

```


然后读取:


```c

b = transformed[idx];

```


成功路径要求 32 个 primary index 恰好构成完整排列。


因此最终:


```text

maskA = 0xffffffff

```


## 8.2 Primary transform


四种核心字节变换是 opcode:


```text

18

19

20

21

```


它们都是 8-bit 双射。


形式大致为:


```c

op18:

    out = rol8(b ^ r8, k) + r10;


op19:

    out = ror8(r10 + b, k) ^ r8;


op20:

    out = rol8(bl * b + r10, k) ^ r8;


op21:

    out = (ror8(b ^ r8, k) - r10) * bl;

```


其中 `bl` 始终为奇数,因此在模 `2^8` 下存在乘法逆元。


所以这四种变换都可逆。


## 8.3 op22


部分 block 后还有 opcode 22。


它引入第二个 transformed byte:


```text

transformed[j]

```


最后 output 形如:


```c

output[pos] =

    primary_transform(transformed[idx])

    ^ rol8(transformed[j], rr + 1);

```


`op22` 还维护:


```text

stack

flag

```


并且存在两个四联块:


```text

pos 12 ~ 15

pos 24 ~ 27

```


这是后续搜索中自由度最大的部分。


---


# 9. 验证 Block 模型


为了防止第二次发生“漏一条汇编”的问题,我没有直接相信压缩模型。


我同时保留:


1. 完整 257-step VM interpreter;

2. 32-block compressed model。


然后随机生成:


```text

hash12

transformed[32]

variant

```


逐 block 比较:


```text

r13

x34

stack

flag

output[pos]

```


500 组随机测试全部一致:


```text

RELAXED 32-BLOCK CROSSCHECK PASS 500 trials

```


因此后续求解使用的 32-block 模型与实际 VM 数据路径一致。


---


# 10. Worker 尾部目标生成


每个 worker 跑完 VM 后,还要检查:


```text

r13

persistent seed

x34

PC

steps

pos

stack

若干计数器

maskA / maskB

```


目标不是明文存储,而是由:


```text

seed0

seed1

variant

常量表

ROL

fmix 风格整数混合

```


动态生成。


独立重算得到:


```text

variant 0:

r13  = 17cac189

seed = 0c49f956

x34  = ef8b64c2

PC   = 00000408


variant 1:

r13  = 072183de

seed = c8782b4d

x34  = d520b1db

PC   = 00000410


variant 2:

r13  = e6a68d04

seed = 408cf867

x34  = 2e8c989c

PC   = 00000400


variant 3:

r13  = 7247a3a0

seed = 934004d6

x34  = ecdc4e20

PC   = 00000418

```


---


# 11. VM 输出后还有 4 轮 ARX


32-byte VM output 并不会直接与表比较。


worker 尾部会把它视为:


```text

8 × uint32_t

```


然后执行 4 轮可逆 ARX。


最终与 `.text` 中:


```text

0x1400021a0

```


附近的目标表比较。


这层 ARX 同样只有:


```text

ADD

XOR

ROL/ROR

```


所以可逆。


我实现了 `inv_arx_core()`,并用 1000 组随机 round-trip 验证通过。


---


# 12. Variant 2 的原始 VM 输出目标


variant 2 最终 8 个 mixed dword 目标:


```text

7f3a3627

92e3afa5

94944e09

fead080a

b84615b9

8d6de414

8c64782f

ff692b07

```


逆掉尾部四轮 ARX,得到 v2 的 VM raw output 必须是:


```text

40e086d0ceff2d270de6e28bd49f57af

beb5316bd8322983820999bccb16ab1d

```


也就是:


```text

40 e0 86 d0 ce ff 2d 27

0d e6 e2 8b d4 9f 57 af

be b5 31 6b d8 32 29 83

82 09 99 bc cb 16 ab 1d

```


---


# 13. Outer Wrapper


外层函数:


```text

0x14000306c

```


逻辑为:


```text

input

  │

  ├── preprocess -> transformed[32]

  │

  ├── worker(v0)

  ├── worker(v1)

  ├── worker(v2)

  └── worker(v3)

          │

          ▼

      records[4]

          │

          ▼

      aggregator

          │

          ▼

      final compare

```


调用顺序被一个 hash 打乱,但 record pointer 同时按 variant index 放置,所以最终:


```text

records[0] = v0

records[1] = v1

records[2] = v2

records[3] = v3

```


调用顺序只是干扰项。


---


# 14. Aggregator


aggregator:


```text

0x140002940

```


初始 4 个状态:


```c

S0 = seed0 ^ 0x243f6a88;

S1 = seed1 ^ 0x85a308d3;


S2 = ror(seed0, 21)

   ^ seed1

   ^ 0x13198a2e;


S3 = ror(seed1, 13)

   ^ seed0

   ^ 0x03707344;

```


数值为:


```text

S0 = 9492857b

S1 = 86782501

S2 = 7fbd3a79

S3 = dd4d826e

```


## 14.1 每个 record 生成 q_i


对于第 `i` 个 record:


```c

r9 = (0xb5297a4d - i * 0x4ad685b3) ^ record.err;


q = rol(record.seed, i + 9)

  ^ rol(r9, i + 13)

  ^ rol(record.x34, i + 5)

  ^ record.r13

  ^ ((record.steps + 1) * 0x045d9f3b)

  ^ (record.pos << 17)

  ^ record.pc;

```


然后对 8 个 mixed word 继续:


```c

for (j = 0; j < 8; j++) {

    last =

        rol(

            ((j + 1) * 0x7f4a7c15)

            ^ mixed[j]

            ^ q,

            ((3 * i + j) & 7) + 5

        );


    q = last + 0x6d2b79f5;

}

```


得到最终:


```text

q_i

```


## 14.2 State 更新


aggregator 对四个 q 做类似:


```c

S[i] =

    rol(q_i ^ S[i], i + 5)

    - 0x61c88647;


S[i+1] ^= B_i(q_i);

```


最后再经过一层 128-bit whitening,得到外层 compare 值。


---


# 15. 逆 Outer 128-bit Whitening


最后一层对四个 dword 只有 XOR/rotate,视作 GF(2) 线性变换。


构造 128×128 bit matrix 后求 rank:


```text

rank = 128

```


所以它可逆。


逆最终 expected aggregate:


```text

892a48af

b45dcd47

006c5ac3

b0b1062a

```


得到 required internal states:


```text

a24ae331

3a0d7edb

c4b33663

de22dab8

```


---


# 16. 求唯一 q-chain


将 aggregator 的四次 state update 展开,可以把整个问题化成:


```text

q0 -> q1 -> q2 -> q3

```


其中只剩一个 32-bit 自由变量。


不需要 SMT。


直接 C 遍历完整:


```text

q0 ∈ [0, 2^32)

```


最终整个空间只有一个根:


```text

q0 = 1f1cc1bc

q1 = 1b7d21a2

q2 = 47d3d751

q3 = 27ff56e7

```


这一步很重要,因为它揭示了题目的真正设计。


---


# 17. 不是四个 Worker 都成功


如果假设四个 variant 都满足各自的 clean-success worker target,可以算出对应 aggregator q。


其中:


```text

variant 2 clean-success q

= 47d3d751

```


恰好等于外层唯一要求:


```text

q2 = 47d3d751

```


而其它 variant 并不满足外层所需 record。


进一步用最终 flag 完整复算后可以看到:


```text

v0 err = ffffffff

v1 err = ffffffff

v2 err = 00000000

v3 err = ffffffff

```


所以这题的真正结构是:


> **只有 variant 2 走 clean success。variant 0/1/3 的失败行为本身就是正确 aggregator fingerprint 的一部分。**


这也是 `AntiAi` / `opaque_trace` 这个设计最核心的误导。


---


# 18. 开始解 Variant 2


Variant 2 的严格目标:


```text

steps = 257

pos   = 32

PC    = 0x400


seed = 408cf867

r13  = e6a68d04

x34  = 2e8c989c


maskA = ffffffff

maskB = ffffffff

err   = 00000000

stack = 32

```


raw VM output:


```text

40e086d0ceff2d270de6e28bd49f57af

beb5316bd8322983820999bccb16ab1d

```


---


# 19. r13 更新可逆


每个 block 的 r13 更新可写成:


```c

r13_next =

    rol32(

        b * 0x045d9f3b

        + r13_old

        + (pos ^ 0xa7),

        (pos % 13) + 5

    )

    ^ 0x9e3779b9;

```


其中:


```text

0x045d9f3b

```


是奇数,因此在 `Z / 2^32 Z` 下可逆。


其逆元:


```text

0x119de1f3

```


所以若知道:


```text

r13_old

r13_next

pos

```


可以直接求 primary byte:


```c

t =

    ror32(

        r13_next ^ 0x9e3779b9,

        (pos % 13) + 5

    );


diff =

    t

    - r13_old

    - (pos ^ 0xa7);


b =

    diff * 0x119de1f3 mod 2^32;

```


合法时必须:


```text

b < 256

```


这是后续 MITM 的重要剪枝。


---


# 20. x34 更新同样可逆


x34 的更新依赖:


```text

r13_next

x34_old

output[pos]

pos

block helper constants

```


但在反向时:


```text

x34_old

```


可以由:


```text

r13_next

x34_next

output[pos]

pos

```


直接逆出,并且 **不需要先知道 primary byte**。


这使得反向搜索比直接枚举 256 字节快很多。


---


# 21. 前 12 Block Prefix


v2 在 pos2 / pos3 的 `op22` secondary index 是固定:


```text

pos2 -> transformed[3]

pos3 -> transformed[2]

```


因此可以直接枚举:


```text

hash12 ∈ [0,4095]

transformed[2] ∈ [0,255]

transformed[3] ∈ [0,255]

```


然后从 block0 正向跑到 block11。


总搜索空间:


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 1天前 被mxym_编辑 ,原因:
上传的附件:
收藏
免费 5
打赏
分享
最新回复 (3)
雪    币: 2306
活跃值: (916)
能力值: ( LV3,RANK:27 )
在线值:
发帖
回帖
粉丝
2
学习
12小时前
0
雪    币: 2462
活跃值: (1790)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
3
牛逼
11小时前
0
雪    币: 0
活跃值: (1929)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
06666
6小时前
0
游客
登录 | 注册 方可回帖
返回