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:
+113
-7
@@ -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 —— "
|
||||
|
||||
@@ -175,6 +175,110 @@ class Program:
|
||||
out.append((mid, tgt))
|
||||
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]]:
|
||||
"""返回 (pc, 串, 所属子程序入口)。"""
|
||||
rx = re.compile(pattern, re.I)
|
||||
|
||||
Reference in New Issue
Block a user