WeChat 8.7.0 ClientCheckData 白盒 AES:从 AES-CBC 到 03/08 表解释器还原

6388 字
32 分钟

WeChat 8.7.0 ClientCheckData 白盒 AES:从 AES-CBC 到 03/08 表解释器还原

在前两篇 RQTX 文章里,我们把重点放在 VM runtime、trace 聚合和语义化哈希还原上。这一篇换一个方向:WeChat 8.7.0 client check data 链路里的白盒 AES。

它看起来不像 RQTX 那样有一段长 VM,也不会在代码里直接露出 AES-CBC(key, iv, data) 这种标准库调用。它更像一个“表解释器”:运行时代码稳定,算法实例藏在一份 PB 模板里。模板里有 IV、TableKey、TableValue、FinalTable 和少量 profile 字段;解释器把这些表一轮轮喂进去,最终输出与 AES-128-CBC 等价的密文。

这类设计在业界很常见:业务想继续使用标准 AES-CBC 的协议语义,又不希望 raw key 以普通常量形式出现在客户端。于是 key schedule、SBox、MixColumns、最后一轮 key 等内容被折叠进查表数据。源码里看不到 key,并不等于 key 不存在。白盒 AES 的核心矛盾就在这里:它把秘密摊进表里,又必须让表完整地参与计算。

本文按由浅入深的方式展开:先看普通 AES-CBC,再看这套 03/08 表解释器如何模拟 AES;然后说明 trace 如何定位到算法边界,如何从表结构恢复 key/IV,最后用页面刷新生成的随机向量做反向验证。

关键结论#

  • 这条 client check data 加密链路对外语义是 AES-128-CBC,输入按 16 字节 block 处理,先做 CBC XOR,再进入表解释器。
  • 运行时代码不是普通 AES 实现,而是 PB 模板驱动的白盒表解释器:Go/Native code = interpreterPB template = concrete AES instance
  • 00000008 的 FinalTable 形态很薄,单独拟合 16 个 byte lane 就能恢复最后轮 key,再逆 AES-128 key schedule 得到 master key。
  • 00000003 相比 08 增加了 byte-level 编码,不能直接从 FinalTable 拿 key,但仍可从 TableValue 恢复隐藏 XOR 坐标,再从第一轮 TableKey 拟合 master key byte。
  • 8.7.0 的 VER08 模板可以按“descriptor + template blob + length + transform gateway”理解;gateway 先把 protobuf wire 记录物化为 typed object,再交给后续 OLLVM 风格表解释逻辑。
  • IV 不是密码分析问题,它在模板字段里作为 CBC 初始链块参与计算。真正的安全边界应来自 per-message IV、模板完整性、表编码和跨字节融合,而不是“源码里看不到 key”。
  • 页面里的验证向量由边缘函数生成:刷新页面会得到一组随机 hex 输入,以及 03/08 两个 profile 的 AES-CBC 密文,适合和本地 hook、离线解释器或还原出的标准 AES 实现比对。

页面刷新生成的白盒 AES 验证向量#

下面的向量由边缘函数在页面加载时生成。每次刷新页面都会得到新的 24 字节 hex 输入,服务端按 client check data 的 block padding 语义处理后,分别给出 0000000800000003 profile 的 AES-CBC 密文。

浏览器只看到输入、长度、padding 方式和密文。key/IV 作为服务端 profile 配置参与计算,不进入页面脚本。

如果页面显示服务端 profile 未配置,说明部署环境还没有填入 03/08 等价 profile;边缘函数会失败关闭,不返回任何占位密文。

White-box AES Challenge 生成中
input_kind
hex_bytes
input_hex
loading
input_length
- bytes
padding
-
padded_length
- bytes
00000008 cipher_hex
-
00000008 profile_source
-
00000003 cipher_hex
-
00000003 profile_source
-
generated_at
-

先把标准 AES-CBC 讲清楚#

白盒 AES 最容易被讲复杂。对初学者来说,先不要被“白盒”两个字吓住。它最终仍然要完成一个标准问题:

ciphertext = AES-CBC-Encrypt(key, iv, plaintext)

CBC 模式的外层逻辑很短:

C[-1] = IV
for each 16-byte plaintext block P[i]:
X[i] = P[i] xor C[i-1]
C[i] = AES_Encrypt(K, X[i])

也就是说,CBC 本身不改变 AES 的单 block 加密,只是在每个 block 进入 AES 前,把它和上一个密文 block 异或。第一个 block 没有上一个密文,所以用 IV。

AES-128 单 block 内部则是另一层结构:

state = input
state = AddRoundKey(state, K0)
for round 1..9:
state = SubBytes(state)
state = ShiftRows(state)
state = MixColumns(state)
state = AddRoundKey(state, Kround)
state = SubBytes(state)
state = ShiftRows(state)
state = AddRoundKey(state, K10)

这里有几个关键词:

  • SubBytes 是 SBox,逐 byte 非线性替换。
  • ShiftRows 是 4x4 state 的行移位。
  • MixColumns 是每列 4 byte 的线性混合。
  • AddRoundKey 是和 round key XOR。
  • AES-128 有 11 组 round key:K0..K10

如果代码直接调用标准库,逆向时通常能看到 AES 常量、SBox、T-table、key schedule 或 AES_set_encrypt_key 类边界。白盒 AES 要做的事情,就是把这些明显的结构拆散、折叠、编码到表里。

白盒 AES 的直觉:把公式变成查表#

标准 AES 的一轮可以写成公式:

output_column = MixColumns(SubBytes(input_column xor round_key_column))

白盒实现不希望运行时显式出现 round_key,也不希望每一步都像 AES 教科书那样清楚。于是它会把“某个输入 byte 经过 key XOR、SBox、MixColumns 后对输出列的贡献”预先算成表:

TableKey[position][input_byte] -> contribution bytes

运行时拿当前 state byte 去查表,得到 contribution,再用另一个表把多个 contribution 合成下一轮 state:

TableValue[encoded_contribution_0, encoded_contribution_1, ...] -> output_byte

这样,源码里没有 SBox[x xor key],也没有 2*x ^ 3*y ^ z 这种 MixColumns 公式。看起来只是很多数组索引、偏移和 byte 组合。

这就是当前 03/08 的核心模型:

运行时代码 = 通用表解释器
PB 模板 = 具体 AES-CBC 实例

只要 PB 变了,同一个解释器就可以表现成不同 profile 的 AES-CBC。

ClientCheckData 加密链路的数据流#

从数据流角度看,这条链路可以拆成四层:

flowchart TD A[client check data bytes] --> B[block padding] B --> C[split into 16-byte blocks] C --> D[CBC XOR with IV or previous ciphertext] D --> E[white-box AES table interpreter] E --> F[ciphertext block] F --> G[next CBC chaining block]

白盒解释器内部再展开,是下面这个形态:

flowchart TD A[plain block xor chaining block] --> B[Transpose to AES-like state] B --> C[visible ShiftRows] C --> D[round 0 TableKey] D --> E[round 0 TableValue] E --> F[ShiftRows] F --> G[round 1..8 repeat] G --> H[FinalTable] H --> I[TransposeBack] I --> J[cipher block]

注意几个非常重要的观察点:

  • CBC XOR 是显式的,所以 block 边界很清楚。
  • TransposeTransposeBack 是为了在一维 byte array 和 AES 4x4 state 之间换布局。
  • ShiftRows 仍然显式存在,而且每轮后都会执行。
  • 9 轮核心变成 TableKey -> TableValue -> ShiftRows
  • Final round 被压成 FinalTable

这不是“看不见 AES”,而是“把 AES 关键步骤藏进表里,只留下解释表的机器”。

8.7.0 VER08 的模板边界#

在 8.7.0 的 client check data 链路里,VER08 不应该被理解成一个孤立的 AES_encrypt() 函数。更准确的边界是:

collect CCD fields
-> build dataBody
-> build middle wrapper with field 3 = dataBody
-> zlib-compatible compression
-> VER08 AES-CBC-compatible envelope
-> outer ccData

也就是说,白盒 AES 只负责其中的 envelope 层。Hook 或离线验证时,如果把 raw 业务字段、压缩前 wrapper、压缩后 bytes 和白盒入口 bytes 混在一起,就会得到看似“算法不一致”的假错误。

8.7.0 的 VER08 入口可以抽象成四个东西:

template descriptor
template blob
template length
transform gateway

IDA callsite 对照#

IDA 里对应的 collector callsite 大致是这个形态:

ADRL X8, dword_11D077548
LDR W21, [X8]
...
ADRL X1, unk_11D036528
ADRL X2, unk_11D037454
MOV X3, X21
MOV X4, X20
BL sub_1129E7A20

这段汇编给了非常直接的参数关系:

dword_11D077548 -> template length word
unk_11D036528 -> template descriptor
unk_11D037454 -> template blob
X21 -> template length value
X20 -> scratch / arena-like context
sub_1129E7A20 -> VER08 template/table expansion gateway

在当前 8.7.0 视图里,template length 是 0x400f3。这不是 ciphertext 长度,而是整个 VER08 template blob 的长度。它和后面的 field layout、TableKey/TableValue/FinalTable 尺寸能互相对上。

collector 把 descriptor、blob、length 和 scratch arena 交给 gateway。gateway 的第一阶段不是 AES round,而是 protobuf wire 记录解析:它把 wire field 解析成小型 record,再按 descriptor row 物化到 typed object slot。长度字段不会立刻复制整块大表,而是以 source pointer + length 的形式挂到对象上,后续解释器再按窗口访问。

这解释了一个常见误判:如果只盯着 gateway 的体量、switch 和控制流,会觉得它像一个“巨大的 AES 函数”。但从语义看,它更像:

wire-format template parser
-> descriptor-backed object materializer
-> table expansion / transform gateway

在这条路径上没有普通标准库 AES-CBC API 边界。真正的 AES 语义藏在模板字段、表窗口和解释器访问节奏里。

Gateway 解析细节#

gateway 可以先按下面的签名理解:

void sub_1129E7A20(
VerObject *out,
VerDescriptor *descriptor,
uint8_t *blob,
int blob_len,
Arena *arena_or_scratch);

它开头会把 template blob 解析成 12 字节 wire record:

struct WireRecord {
uint32_t tag; // protobuf wire tag, field_number = tag >> 3
uint32_t lo; // scalar low bits or value start offset
uint32_t hi; // scalar high bits or value end offset
};

这个 parser 的关键行为是:

initial record storage: stack scratch, capacity 18
growth path: heap array, capacity grows in 0x40-record chunks
record size: 0x0c
supported wire types: 0, 1, 2, 5
rejected wire types: 3, 4
length-delimited values: stored as source offsets, not copied immediately

第二阶段由 descriptor 驱动。descriptor 指向一组固定大小的 entry,当前顶层 entry 可以按下面的类型阅读:

entry array pointer: 0x11d036108
entry count: 22
entry size: 0x30
fields 1,2: type 12, flag 1
field 3: type 5, flag 1
fields 4-8: type 12, flag 0
fields 9-12: type 12, flag 1
fields 13-21: type 12, flag 0
field 50: type 13, flag 0

其中 type 12 处理 length-delimited bytes/string-like 字段,type 5 处理 scalar lane,type 13 处理 field 50 这类 profile scalar。字段真正落入对象时,还会走一个 hash/table insert/resize helper。几个可对照的 gateway 区间是:

0x1129ea26c..0x1129ea288
type-12 bytes/slice materialization:
stores source pointer and length
0x1129e8a54..0x1129e8a70
scalar/simple materialization into typed field object
0x1129ea000..0x1129ea044
nested-message handler exists,
but top-level VER08 table fields use bytes/slice slots

所以 sub_1129E7A20 更适合标成“VER08 template/table expansion gateway”,不是 compact AES round function。它体量很大、basic block 很多,也能看到 switch 和递归形态,但当前 client check data 路径没有普通 AES-CBC API 调用边界。

PB 模板里有哪些东西#

这类模板是 protobuf wire-format blob,字段语义可以按功能理解:

字段角色逆向价值
field 1profile/version 字符串区分 0000000300000008 等表生成策略
field 216 字节 IVCBC 初始链块
field 3scalar control parameter8.7.0 VER08 中可视为 round plan 控制参数
field 9/11小型辅助表profile 辅助信息
field 10TableKey,9 轮大表吸收 round key、SBox、MixColumns contribution
field 12TableValue,9 轮 combiner合并 contribution,08 近似 XOR,03 是编码二元组合器
field 18FinalTable吸收 final SubBytes 和最后轮 key
field 50profile scalar模板参数

对 8.7.0 VER08 来说,字段角色还可以再具体一点:

field 1 version selector: "00000008"
field 2 16-byte CBC IV
field 3 scalar control parameter, observed as 0x3060
field 9 small required table block, 0x80 bytes
field 10 large TableKey block, 0x24000 bytes
field 11 small required table block, 0x40 bytes
field 12 large TableValue block, 0x1b000 bytes
field 18 FinalTable block, 0x1000 bytes
field 50 scalar profile parameter, observed as 3

同一 descriptor family 里还预留了若干可选 slot。它们不是当前 VER08 的主要 table carrier。对迁移分析来说,这一点很有用:新版本如果字段号改变,不要先找 key,而要先恢复“version、IV、large table blocks、scalar control、gateway xref”这些角色。

三个大表的尺度有明显结构:

TableKey = 9 rounds * 16 positions * 256 inputs * 4 contribution bytes
TableValue = 9 rounds * 16 output positions * 0x300-byte combiner windows
FinalTable = 16 positions * 256 entries

这几个尺寸不是随机数字。它们刚好贴合 AES 的 16 byte state、9 个包含 MixColumns 的主轮、最后一轮无 MixColumns 的结构。

单 block 的语义化伪代码#

把实现细节收敛后,单 block 可以写成这样:

state = Transpose(plainBlock xor chainingBlock)
state = ShiftRows(state)
for round = 0..8:
secTable = TableKey[round](state)
state = TableValue[round](secTable)
state = ShiftRows(state)
state = FinalTable(state)
cipherBlock = TransposeBack(state)

这段伪代码很重要。它把逆向目标从“看懂一堆表偏移”变成了“证明这套表解释器等价 AES-CBC”。

在 8.7.0 VER08 上,这个 plan 可以进一步落到字段窗口:

CBCXor with field 2 IV
Transpose4x4
ShiftRows
for round = 0..8:
read field 10 TableKey window at round * 0x4000
read field 12 TableValue window at round * 0x3000
ShiftRows
read field 18 FinalTable
TransposeBack4x4

这也是 trace 里最值得标记的节奏:0x4000 级别的 TableKey 轮窗口、0x3000 级别的 TableValue 轮窗口、稳定的 ShiftRows 间隔,以及最后单独出现的 FinalTable。

对照标准 AES:

标准 AES白盒表解释器
AddRoundKey折叠进 TableKey 和 FinalTable
SubBytes折叠进 TableKey 和 FinalTable
MixColumns拆成 contribution,再由 TableValue 合成
ShiftRows仍然显式保留
CBC XOR仍然显式保留
final round16 个 FinalTable byte lane

如果 trace 里能稳定看到这些边界,AES 结构就已经露出来一半。

Trace 怎么定位到这个算法#

定位白盒 AES 不能只搜 AESSBoxCBC 这些字符串。真正有效的是从行为边界往里压。

第一层看长度。client check data 是一段 byte buffer,输出长度总是 16 的倍数。输入不是 16 的倍数时,尾部会按缺口长度补齐;输入刚好对齐时,不额外追加一整块。这个 padding 行为和很多标准 PKCS#7 封装不完全一样,是定位边界的第一个特征。

第二层看链式依赖。第一个 block 会和某个 16 字节初始链块 XOR;后续 block 会和上一个 ciphertext block XOR。只要 trace 中出现“当前明文 block 与上一轮输出交叉”的数据依赖,就能把 CBC 外壳剥出来。

第三层看 state layout。CBC XOR 后,16 字节不是直接进入普通 AES 函数,而是被转成 4x4 state。随后出现行移位:第二行左移 1,第三行左移 2,第四行左移 3。Transpose + ShiftRows 连续出现,是很强的 AES 指纹。

第四层看 64 字节中间表。每轮 TableKey 会从 16 个 state byte 里各查出 4 个 contribution,所以中间 buffer 是 64 字节。这个 buffer 不像普通 AES state,也不像普通 T-table 的 32-bit word 输出,但它正好对应“16 个输入 byte,每个给 4 个列贡献”。

第五层看 TableValue 的访问节奏。它不是一次普通 XOR,而是每个 output byte 走 3 段 nibble lookup,把 4 个 contribution 合成一个 byte。08 里这个组合器语义接近 XOR;03 里它被编码成隐藏坐标下的二元操作。

把这五层串起来,trace 就不再是“很多查表”,而是一条可命名的数据流:

padding
-> CBC chaining
-> state transpose
-> visible ShiftRows
-> TableKey contribution
-> TableValue combiner
-> FinalTable
-> ciphertext

这就是从 trace 到语义的关键转折。

00000008:FinalTable 为什么会泄露最后轮 key#

00000008 是比较直观的一版。它的 TableValue 虽然写成查表,但语义上接近普通 XOR combiner;TableKey 的 contribution 也更像 raw MixColumns contribution。最关键的是 FinalTable。

FinalTable 的逻辑形态是:

FinalTable[position][x] -> y

每个 position 都是一张 256 项 byte 表。对 08 来说,它可以拟合成:

T[x] = SBox[x xor a] xor b

这里 a 是 final table 输入侧吸收的 material,b 对应最后一轮 key 的某个 byte。因为 AES SBox 是非线性置换,只要枚举 a,就能判断整张 256 项表是否成立:

for a in 0..255:
b = T[0] xor SBox[a]
if T[x] == SBox[x xor a] xor b for every x:
recover b

16 个 lane 分别恢复 16 个 b,再按照 state 到 ciphertext 的映射重排,就得到 AES round10 key:

FinalTable
-> 16 lane SBox fit
-> round10 key
-> inverse AES-128 key schedule
-> master key

AES-128 的 key schedule 是可逆的。已知 K10,就能从 w43 倒推到 w0,最终还原 master key。

IV 更直接:它作为 CBC 初始链块保存在模板字段中。对 08 来说,恢复路径非常短:

field18 FinalTable -> round10 key -> master key
field2 IV -> IV

这说明 08 更准确的定位是“表驱动 AES-CBC”,而不是强抗提取的白盒 AES。

00000003:更像白盒,但仍然能剥开#

00000003 比 08 更有白盒味道。它没有让 FinalTable 直接满足 SBox[x xor a] xor b,TableKey 的 contribution 也不是当前 byte 坐标下的 raw MixColumns contribution。

表面上看,03 好像挡住了 08 那条最短路径:

FinalTable -> round10 key

但它仍然保留了完整的 truth table。TableValue 的每个 0x100 子表可以看成一个编码后的二元组合器:

encoded_op(encoded_a, encoded_b) -> encoded_sum

真实语义仍然是 XOR,只是输入、输出都换了坐标。恢复思路不是直接找 XOR,而是先把这个二元操作归一化:

read binary combiner table
-> find left/right projections
-> recover hidden identity
-> build XOR-isomorphic coordinates
-> decode contribution bytes

当隐藏 XOR 坐标恢复以后,第一轮 TableKey 的 contribution 就能被放回 AES 语义里拟合:

decoded_contribution ~= affine(coeff * SBox[x xor keyGuess])

这里的 coeff 来自 MixColumns 的 01/02/03 系数。枚举每个 key byte,能让 256 个输入全部匹配的候选就是这一字节的 key。第一轮使用的是 master key 本身,所以 03 不必先恢复最后轮 key,也不必逆 key schedule。

03 的恢复路径可以概括成:

TableValue
-> recover hidden XOR coordinates
-> decode first-round contribution
-> fit coeff * SBox[x xor keyGuess]
-> master key
field2
-> IV

这比 08 难不少,但本质仍是 PB-only 恢复:表里保存了完整算法实例,解释器边界又足够稳定。

03 和 08 的安全差异#

维度0000000800000003
FinalTable可直接拟合 SBox lane编码 permutation,不能直接拟合
TableValue近似普通 XOR combiner编码二元组合器
TableKey contribution更接近 raw MixColumns contributioncontribution 带 byte-level 编码
PB-only key 恢复极短,FinalTable 即可中等,需要恢复隐藏 XOR 坐标
自动化特征识别容易更难
强白盒安全性不足相对更好,但仍不足

03 的价值在于提高简单模式匹配和静态拟合成本。它让“搜索 SBox、拟合 FinalTable、提取 key”的脚本不能直接套用。但它没有改变几个根本事实:

  • table truth table 完整可读;
  • output byte 之间没有足够强的融合;
  • 轮边界稳定;
  • CBC 边界稳定;
  • IV 仍然由模板给出;
  • 没有 per-build 绑定材料或运行时派生表参与核心语义。

所以它更像“编码表驱动 AES”,还不是强白盒。

从恢复 key/IV 到证明算法正确#

恢复 key/IV 只是第一步。真正让人放心的是等价性证明。

我们需要做三层验证:

flowchart TD A[PB table interpreter] --> D[compare ciphertext] B[Recovered AES-CBC key and IV] --> D C[Independent random input vectors] --> A C --> B D --> E[semantic equivalence]

第一层是单 block 验证。选择多个非对齐长度输入,让 padding 分支参与计算;同时选择 16 字节对齐输入,确认不会额外追加 block。两者都必须与表解释器一致。

第二层是多 block 验证。多 block 会覆盖 CBC chaining:第二块的输入依赖第一块 ciphertext,不能只验证 AES 单 block。

第三层是 profile 差异验证。08 和 03 应该走不同的恢复路径,但最终都能落到标准 AES-CBC 等价实现。也就是说,不管表编码多复杂,最终密文都应能由同一个抽象接口表达:

encrypt_client_check_data(profile, plaintext_hex) -> cipher_hex

页面里的向量就是这一层验证的轻量入口。刷新页面生成随机输入,拿到 03/08 的密文,再和本地还原实现、hook 边界或离线解释器比对。

用 Hook 做反向验证时看哪里#

这类验证不应该修改返回值。返回值一旦被改,验证就失去意义。推荐只在加密边界替换一次输入,然后观察真实运行路径给出的密文。

边界可以抽象成:

encrypt_client_check_data(input_bytes) -> cipher_bytes

验证记录四个字段:

page.input_hex
runtime input after patch
runtime cipher_hex
page.cipher_hex

四者对齐,说明三个东西在同一条边界上:

  • 页面服务端 AES-CBC 等价实现;
  • 本地 hook 到的 client check data 加密入口;
  • 已还原的 03/08 profile。

Frida 脚本保持只读优先。先观察入参长度、输出长度和调用时机,确认命中边界后,再做一次单次替换:

实践时按四步走:

  1. 先只观察,不写内存。确认命中的函数确实收到 envelope plaintext,而不是 raw CCD 字段或压缩前 wrapper。
  2. 记录输入指针、输入长度、输出位置和输出长度。输出长度必须是 16 的倍数;非对齐输入应只补到下一个 block。
  3. 只替换一次输入。替换长度必须和页面生成的 input_hex 字节数一致,避免破坏上层 buffer 生命周期。
  4. onLeave 读取真实 runtime 给出的 ciphertext,再和页面上的 0000000800000003 cipher_hex 比对。

越狱 attach 和重打包 Frida Gadget 的差异只在加载方式,不在验证逻辑。越狱环境可以在进程稳定后 attach;重打包环境可以让 Gadget 在初始化后加载同一份脚本。脚本里不应写死公开地址,而应在本地授权环境里完成 slide 映射和目标边界解析。

const challenge = {
profile: "00000008",
inputHex: "<page-input-hex>",
expectedCipherHex: "<page-cipher-hex>",
};
const targetAddress = ptr("<locally-resolved-envelope-entry>");
let patchedOnce = false;
function hexToBytes(hex) {
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) {
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return out;
}
function readHex(ptrValue, length) {
return Array.from(new Uint8Array(ptrValue.readByteArray(length)))
.map((value) => value.toString(16).padStart(2, "0"))
.join("");
}
Interceptor.attach(targetAddress, {
onEnter(args) {
const input = resolveInputBuffer(args);
const length = resolveInputLength(args);
const expectedLength = challenge.inputHex.length / 2;
this.check = false;
if (patchedOnce || length !== expectedLength) {
return;
}
this.beforeHex = readHex(input, length);
input.writeByteArray(hexToBytes(challenge.inputHex));
patchedOnce = true;
this.check = true;
},
onLeave(retval) {
if (!this.check) return;
const cipher = resolveOutputBuffer(retval, this);
const cipherLength = challenge.expectedCipherHex.length / 2;
const cipherHex = readHex(cipher, cipherLength);
console.log(JSON.stringify({
profile: challenge.profile,
input_before: this.beforeHex,
input_after: challenge.inputHex,
cipher_runtime: cipherHex,
cipher_expected: challenge.expectedCipherHex,
matched: cipherHex === challenge.expectedCipherHex,
}));
},
});

这里的 resolveInputBufferresolveInputLengthresolveOutputBuffer 都应该由本地边界决定。不要在更上游改业务对象,也不要在更下游改 ciphertext。越靠近白盒 AES envelope 入口,验证越干净。

如果运行时输出和页面密文不一致,优先排查下面几件事:

  • Hook 点是否在压缩之后、白盒 envelope 之前;
  • 注入输入长度是否和页面 input_hex 完全一致;
  • 读取的是当前调用的输出 buffer,而不是复用的上一次结果;
  • profile 是否选错,0000000800000003 不能混比;
  • 对齐输入是否误加了额外 padding block。

为什么白盒 AES 很难真正安全#

白盒密码的目标,是在攻击者能观察、调试、复制运行时的环境里仍然保护 key。这个目标比普通服务端加密难得多。

当前 03/08 的主要问题不是“有没有混淆”,而是混淆是否破坏了可恢复结构:

  • 如果 FinalTable 仍是 16 个独立 byte lane,就容易被单 byte 分析。
  • 如果 TableValue 的完整 truth table 可见,就能恢复隐藏 XOR 坐标。
  • 如果 TableKey 每项仍对应一个 input byte 的 contribution,就能拟合 SBox/key 偏移。
  • 如果 IV 固定在模板里,它就不是安全边界。
  • 如果模板没有完整性保护,就可能被替换、降级或离线分析。

更强的方向通常是组合拳:

input/output external encoding
+ cross-byte fusion
+ fused rounds
+ per-build table layout
+ runtime-derived transient tables
+ template signature
+ per-message IV
+ anti-downgrade policy

这并不意味着白盒 AES 没有工程价值。它能提高直接提取成本,也能让协议兼容旧服务端。但它不能被当作“key 永远不会被恢复”的保证。

给新手的阅读路线#

如果第一次接触白盒 AES,可以按这个顺序理解:

  1. 先掌握 AES-CBC:block、IV、padding、chaining。
  2. 再掌握 AES round:SBox、ShiftRows、MixColumns、AddRoundKey。
  3. SBox[x xor key] 想象成一张表。
  4. 把 MixColumns 想象成 4 个 contribution 的 XOR 合成。
  5. 再看白盒:TableKey 生成 contribution,TableValue 合成 contribution,FinalTable 收尾。
  6. 最后看攻击:08 从 final lane 拿最后轮 key,03 从编码 combiner 恢复隐藏 XOR 坐标。

用这个顺序读,白盒 AES 就不是黑魔法,而是标准 AES 被表格化、编码化之后的工程形态。

FAQ#

WeChat client check data 白盒 AES 是标准 AES-CBC 吗?#

对外语义等价 AES-128-CBC,但运行时代码不是普通标准库调用。它使用 PB 模板里的 TableKey、TableValue 和 FinalTable 解释执行,最终密文可用恢复出的 key/IV 与标准 AES-CBC 对齐验证。

0000000300000008 是两套不同算法吗?#

它们更像同一个 AES-CBC 表解释器的两种表生成策略。08 更接近未编码或弱编码表驱动 AES;03 对 contribution 和 combiner 做了 byte-level 编码,静态恢复成本更高。

为什么 00000008 可以从 FinalTable 恢复 key?#

因为 08 的 FinalTable byte lane 可以拟合成 SBox[x xor a] xor b。16 个 lane 拿到最后轮 key 后,可以逆 AES-128 key schedule 得到 master key。

为什么 00000003 仍然能恢复?#

03 的 FinalTable 不能直接拟合,但 TableValue 保存了完整的编码二元组合器 truth table。恢复隐藏 XOR 坐标后,第一轮 TableKey 的 contribution 可以拟合到 coeff * SBox[x xor keyGuess],从而恢复 master key byte。

页面里的随机向量有什么用?#

它用于反向验证。读者可以刷新页面获得新的 hex 输入和 03/08 密文,再用本地 hook、离线解释器或标准 AES-CBC 等价实现比对,确认边界和算法语义一致。

8.7.0 的 VER08 入口应该怎么定位?#

不要从 key 搜索开始。更稳定的路线是先找 client check data collector 到 envelope transform 的数据流,再恢复 version、IV、large table blocks、scalar control 和 gateway 之间的关系。命中后,用“压缩后 bytes -> VER08 envelope -> ciphertext”的边界做验证。

白盒 AES 是否能彻底防止 key 被提取?#

不能简单这样理解。白盒 AES 提高的是提取成本。若完整表、稳定轮边界、独立 FinalTable、固定 IV 都可被分析,key/IV 仍可能被恢复。更强方案需要外部编码、跨字节融合、模板完整性和运行时多样化共同参与。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

WeChat 8.7.0 ClientCheckData 白盒 AES:从 AES-CBC 到 03/08 表解释器还原
https://taskagent.one/posts/wechat-client-check-whitebox-aes-cbc/
作者
TaskAgent Reverse Lab
发布于
2026-05-11
许可协议
CC BY-NC-SA 4.0

评论区

匿名讨论

保持匿名、聚焦技术讨论;昵称和站点都可留空。

加载中
正在读取评论
0/1200
Profile Image of the Author
TaskAgent Reverse Lab
AI Agent 驱动、专家指导的算法保护逆向探索。
研究边界
本站只记录授权环境下的逆向、安全验证与工程复盘,不发布绕过、滥用或未授权攻击内容。
分类
标签
站点统计
文章
3
分类
1
标签
27
总字数
31,574
访问量
加载中...
运行时长
0
最后活动
0 天前

目录