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 argparse
import json import json
import re
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple 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]]: def find_manifest_module(p: tp.Program) -> Tuple[int, int, List[int]]:
"""找"清单模块": 体内 `push(require(N))` 次数最多者。 """找"清单模块": 体内 `push(require(N))` 次数最多者。
@@ -130,18 +226,28 @@ def main() -> int:
print(f"真机明文 cd 槽数: {len(plain_cd)}") print(f"真机明文 cd 槽数: {len(plain_cd)}")
table = [] table = []
print("\n槽位 模块id 工厂entry 语义线索 真机值") print("\n槽位 模块id 工厂entry 取值语义(静态还原) 真机值")
print("-" * 108) print("-" * 112)
for i, m in enumerate(seq): for i, m in enumerate(seq):
tgt = reg.get(m) tgt = reg.get(m)
strs = transitive_strings(p, tgt) if tgt else [] expr = slot_value_expr(p, tgt) if tgt else None
hints = env_hints(strs) # 有些模块只是"再 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 = "" val = ""
if plain_cd is not None and i < len(plain_cd): if plain_cd is not None and i < len(plain_cd):
v = repr(plain_cd[i]) v = repr(plain_cd[i])
val = v if len(v) <= 34 else v[:34] + "..." val = v if len(v) <= 30 else v[:30] + "..."
table.append({"slot": i, "module": m, "entry": tgt, "hints": hints}) sem = expr or ""
print(f" {i:2d} {m:4d} @{tgt:<7} {str(hints)[:34]:<34} {val}") 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: if plain_cd is not None:
print(f"\n(真机 cd 里还有 {len(plain_cd) - len(seq)} 个槽不来自 require —— " print(f"\n(真机 cd 里还有 {len(plain_cd) - len(seq)} 个槽不来自 require —— "
+104
View File
@@ -175,6 +175,110 @@ class Program:
out.append((mid, tgt)) out.append((mid, tgt))
return out return out
# ---------------------------------------------------------- 轻量符号求值
def symbolic_assignments(self, entry: int, limit: int = 30) -> List[Tuple[str, str]]:
"""对一个小函数(如采集器模块)做**线性符号求值**, 还原 `rN = <表达式>`。
只覆盖这类小模块实际用到的形状: 字面串 / BIND_THIS / MAKE_(REG_)PROP_REF /
GET_PROP / GET_GLOBAL_PROP / NEW_FUNC / CALL_FUNC / CALL_METHOD / STORE_REG /
LOAD_REG_IMM / REVERSE_TOP2 / DUP / 算术。
目的: 把"这一槽取的是什么值"变成可读路径, 例如 `c.navigator.userAgent`。
说明: 遇到分支时按 pc 顺序近似处理, 结果用于**定位语义**, 不作为执行依据。
"""
regs: Dict[int, str] = {}
out: List[Tuple[str, str]] = []
st: List[Tuple[str, str]] = [] # ("reg", n) | ("sym", text)
def top() -> Tuple[str, str]:
return st[-1] if st else ("sym", "?")
def popn(n: int) -> List[Tuple[str, str]]:
r = st[-n:] if n else []
del st[len(st) - n:]
return r
for inst in self.function_code(entry):
n, ops = inst["name"], inst["operands"] or []
if n == "PUSH_IMM_ARRAY":
st.append(("reg", ops[0]))
elif n == "PUSH_EMPTY_STR":
st.append(("sym", ""))
elif n == "PUSH_STR_CHAR":
k, v = top()
st[-1] = ("sym", v + chr(ops[0] & 0xFF))
elif n == "PUSH_IMM":
st.append(("sym", str(ops[0])))
elif n == "PUSH_UNDEFINED":
st.append(("sym", "undefined"))
elif n == "PUSH_NULL":
st.append(("sym", "null"))
elif n == "PUSH_TRUE":
st.append(("sym", "true"))
elif n == "PUSH_FALSE":
st.append(("sym", "false"))
elif n == "LOAD_REG_IMM":
st.append(("sym", regs.get(ops[0], f"r{ops[0]}")))
elif n == "LOAD_REG_INDIRECT":
k, v = st.pop() if st else ("sym", "?")
st.append(("sym", regs.get(int(v), f"r{v}") if str(v).isdigit() else v))
elif n == "BIND_THIS":
k, v = st.pop() if st else ("sym", "?")
st.append(("sym", f"c.{v}"))
elif n == "MAKE_PROP_REF":
key = top()[1]
popn(1)
base = top()[1]
popn(1)
st.append(("sym", f"{base}.{key}"))
elif n == "MAKE_REG_PROP_REF":
key = top()[1]
popn(1)
k, v = st.pop() if st else ("sym", "?")
st.append(("sym", f"{regs.get(v, f'r{v}')}.{key}"))
elif n == "GET_GLOBAL_PROP":
st[-1] = ("sym", f"c.{top()[1]}")
elif n in ("GET_PROP", "GET_ELEM"):
pass
elif n in ("ADD", "SUB", "MUL", "DIV", "MOD", "BIT_AND", "BIT_OR", "BIT_XOR",
"SHL", "SHR", "SHR_U", "EQ", "EQ_STRICT", "GREATER"):
if len(st) >= 2:
b = top()[1]
popn(1)
a = top()[1]
popn(1)
st.append(("sym", f"({a} {n} {b})"))
elif n in ("NEW_FUNC", "NEW_METHOD"):
args = popn(int(ops[0]) if ops else 0)
fn = st.pop()[1] if st else "?"
st.append(("sym", f"new {fn}({','.join(x[1] for x in args)})"))
elif n == "CALL_FUNC":
args = popn(int(ops[0]) if ops else 0)
fn = st.pop()[1] if st else "?"
st.append(("sym", f"{fn}({','.join(x[1] for x in args)})"))
elif n == "CALL_METHOD":
args = popn(int(ops[0]) if ops else 0)
tgt = st.pop() if st else ("sym", "?")
st.append(("sym", f"{tgt[1]}({','.join(x[1] for x in args)})"))
elif n == "STORE_REG":
v = st.pop()[1] if st else "?"
k, ref = st.pop() if st else ("sym", "?")
if k == "reg":
regs[ref] = v
out.append((f"r{ref}", v))
elif n == "DUP":
if st:
st.append(st[-1])
elif n == "REVERSE_TOP2":
if len(st) >= 2:
st[-1], st[-2] = st[-2], st[-1]
elif n in ("POP", "SET_PROP", "ALLOC_REG", "SET_STACK_LEN", "REPLACE_TOP_IMM"):
if n in ("POP", "SET_PROP") and st:
st.pop()
if len(out) >= limit:
break
return out
def find_strings(self, pattern: str) -> List[Tuple[int, str, int]]: def find_strings(self, pattern: str) -> List[Tuple[int, str, int]]:
"""返回 (pc, 串, 所属子程序入口)。""" """返回 (pc, 串, 所属子程序入口)。"""
rx = re.compile(pattern, re.I) rx = re.compile(pattern, re.I)