diff --git a/tools/tdc_layout.py b/tools/tdc_layout.py index 8b124f4..325c724 100644 --- a/tools/tdc_layout.py +++ b/tools/tdc_layout.py @@ -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 —— " diff --git a/tools/tdc_program.py b/tools/tdc_program.py index 5481453..e6d8c25 100644 --- a/tools/tdc_program.py +++ b/tools/tdc_program.py @@ -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)