feat(VM): cd 槽位表可静态导出 + 槽位语义还原(符号求值)

tdc_program 新增 symbolic_assignments(): 对小程序做线性符号求值, 把字节码还原成
`r6 = c.navigator.userAgent` 这样的可读表达式(覆盖字面串/BIND_THIS/MAKE_(REG_)PROP_REF/
GET_GLOBAL_PROP/NEW_FUNC/CALL_*/STORE_REG/LOAD_REG_IMM/REVERSE_TOP2/DUP/算术)。

tdc_layout 新增:
  * slot_value_expr(): 精确规则 —— 采集器模块把值存进某寄存器, `get` 闭包只返回它
    (caps 的父寄存器就是取值寄存器); 含"模块里唯一闭包"的兜底
  * slot_semantics(): 合并"工厂赋值链 + require 委派 + 内部闭包"三条线索
  * 输出改成叶子语义 + 真机明文并排核对

实测确认(线上构建 + 同会话解出的真机 collect):
  * 清单模块 = registry[7], 构造方式是 `r6[r6.length] = require(N)` 追加
    -> **require 顺序即 cd 槽位顺序**(37 个 require + 1 个字面量槽 = 38, 与真机完全吻合)
  * 语义已精确命中: cd[3] <- c.navigator.userAgent, cd[5] <- c.navigator.languages
  * 仍有若干槽的语义标签未对齐(取值寄存器规则需再收口), 已在账本记录
This commit is contained in:
yml2213
2026-10-12 00:17:30 +08:00
parent c4fe365e8c
commit fe1415508a
2 changed files with 217 additions and 7 deletions
+113 -7
View File
@@ -33,6 +33,7 @@ from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple
@@ -56,6 +57,101 @@ ENV_KEYS = [
]
def _fold_str_before(code: List[dict], end: int, span: int = 40) -> Optional[str]:
"""从 end 往前折叠一个字面串(PUSH_EMPTY_STR + 连续 PUSH_STR_CHAR)。"""
chars: List[str] = []
i = end
while i >= 0 and i > end - span and code[i]["name"] == "PUSH_STR_CHAR":
chars.append(chr(code[i]["operands"][0] & 0xFF))
i -= 1
if i >= 0 and code[i]["name"] == "PUSH_EMPTY_STR":
return "".join(reversed(chars))
return None
def slot_value_expr(p: tp.Program, entry: int) -> Optional[str]:
"""**精确**取一个槽位模块"这一槽的值是什么"。
规则(实测形状): 采集器模块最后做
r3.exports.get = function () { ... return <被捕获的那个寄存器> ... }
也就是 `get` 闭包**只返回它捕获的寄存器**。所以:
1. 在工厂里找到被赋给属性 `get` 的那个 CREATE_CLOSURE;
2. 从它的 caps 里取出**父寄存器号**;
3. 该寄存器在工厂里的赋值表达式, 就是这一槽的语义。
例: 模块 18 -> caps=[r3=r6], 工厂里 `r6 = c.navigator.userAgent`。
"""
code = p.function_code(entry)
assigns = dict(p.symbolic_assignments(entry, limit=60))
for k, inst in enumerate(code):
if inst["name"] != "CREATE_CLOSURE":
continue
# 形状: <"get" 字面量>; REVERSE_TOP2; JUMP -> @site; @site: CREATE_CLOSURE
j = k - 1
if j >= 0 and code[j]["name"] == "JUMP":
j -= 1
if _fold_str_before(code, j) != "get":
continue
site = next((c for c in p.closures if c.pc == inst["pc"]), None)
if site is None:
continue
for _child, parent in site.caps: # caps 是 (子寄存器, 父寄存器)
expr = assigns.get(f"r{parent}")
if expr:
return expr
# 兜底(覆盖更广): 采集器模块里通常**只有一个闭包**,它捕获的那个父寄存器就是取值寄存器
for c in p.closures:
if p.owner(c.pc) != entry or len(c.caps) != 1:
continue
_child, parent = c.caps[0]
expr = assigns.get(f"r{parent}")
if expr:
return expr
return None
def slot_semantics(p: tp.Program, reg: Dict[int, int], mid: int, depth: int = 2) -> List[str]:
"""还原某个槽位子模块"到底取什么值"。
三条线索合并(都来自 tdc_program 的符号求值, 不跑 JS):
1. 模块工厂自身的赋值链, 例如 `r6 = c.navigator.userAgent`;
2. **委派**: 有些模块只是 `r6 = r5(N)`(require 另一个模块再导出),要跟进去;
3. **`get` 闭包内部**: canvas/WebGL 这类是延迟取值, 真正读环境在它内部创建的闭包里。
"""
exprs: List[str] = []
seen: set = set()
def visit(m: int, d: int) -> None:
if m in seen or d < 0 or m not in reg:
return
seen.add(m)
entry = reg[m]
for _r, v in p.symbolic_assignments(entry, limit=14):
v = v.strip()
if not v:
continue
exprs.append(v)
mm = re.match(r"r\d+\((\d+)\)$", v) # 纯委派 r6 = r5(N)
if mm:
visit(int(mm.group(1)), d - 1)
for c in p.closures: # 该模块内部创建的闭包(含 get 实现)
if p.owner(c.pc) != entry:
continue
for _r, v in p.symbolic_assignments(c.target, limit=10):
if v.strip():
exprs.append(v.strip())
mm = re.match(r"r\d+\((\d+)\)$", v.strip())
if mm:
visit(int(mm.group(1)), d - 1)
visit(mid, depth)
uniq, out = set(), []
for e in exprs:
if e not in uniq:
uniq.add(e)
out.append(e)
return out
def find_manifest_module(p: tp.Program) -> Tuple[int, int, List[int]]:
"""找"清单模块": 体内 `push(require(N))` 次数最多者。
@@ -130,18 +226,28 @@ def main() -> int:
print(f"真机明文 cd 槽数: {len(plain_cd)}")
table = []
print("\n槽位 模块id 工厂entry 语义线索 真机值")
print("-" * 108)
print("\n槽位 模块id 工厂entry 取值语义(静态还原) 真机值")
print("-" * 112)
for i, m in enumerate(seq):
tgt = reg.get(m)
strs = transitive_strings(p, tgt) if tgt else []
hints = env_hints(strs)
expr = slot_value_expr(p, tgt) if tgt else None
# 有些模块只是"再 require 一层"(r6 = r5(M)),跟进一层
mm = re.match(r"r\d+\((\d+)\)$", (expr or "").strip())
if mm and int(mm.group(1)) in reg:
sub = slot_value_expr(p, reg[int(mm.group(1))])
if sub:
expr = f"{sub} [via require({mm.group(1)})]"
if not expr:
expr = "; ".join(slot_semantics(p, reg, m)[:2]) if tgt else ""
val = ""
if plain_cd is not None and i < len(plain_cd):
v = repr(plain_cd[i])
val = v if len(v) <= 34 else v[:34] + "..."
table.append({"slot": i, "module": m, "entry": tgt, "hints": hints})
print(f" {i:2d} {m:4d} @{tgt:<7} {str(hints)[:34]:<34} {val}")
val = v if len(v) <= 30 else v[:30] + "..."
sem = expr or ""
if len(sem) > 44:
sem = sem[:44] + "..."
table.append({"slot": i, "module": m, "entry": tgt, "expr": expr})
print(f" {i:2d} {m:4d} @{tgt:<7} {sem:<46} {val}")
if plain_cd is not None:
print(f"\n(真机 cd 里还有 {len(plain_cd) - len(seq)} 个槽不来自 require —— "