508 lines
16 KiB
Python
508 lines
16 KiB
Python
"""Static container parser for the KWS/Jimbei sign script.
|
|
|
|
`kws-11-*.js` is wrapped as a Jimbei/Sabo VM:
|
|
|
|
```
|
|
Jimbei()(window, {"b": "<base64 bytecode>", "d": [constants...]});
|
|
```
|
|
|
|
The full interpreter is still JavaScript, but the container is stable enough
|
|
to parse in pure Python. This module extracts bytecode/constants metadata and
|
|
provides a pure static answer for the currently bundled sign script.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
from collections import Counter
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
H5_KWS_KNOWN_SCRIPT_CODE = "04dd303d63222chdfec0087d058fb1c4c5f2eb16eefgce98b2a503578e5f8433"
|
|
|
|
H5_KWS_KNOWN_SCRIPT_SHA256 = "d944b2bc3754bec85c0a238fb052859295a3ecb0756789c47d99417d3f957615"
|
|
H5_KWS_KNOWN_BYTECODE_SHA256 = "7668fe4c01dc4721a862b3102cd3c183dbd4721cac6af4129fc2adcdd1d701d8"
|
|
H5_KWS_KNOWN_CONSTANTS_SHA256 = "226815ab9e5d21ce285afa698b618be3ff5122cf13880c846600b6e7d1396afe"
|
|
|
|
_KNOWN_CODE_BY_SCRIPT_SHA256 = {
|
|
H5_KWS_KNOWN_SCRIPT_SHA256: H5_KWS_KNOWN_SCRIPT_CODE,
|
|
}
|
|
_KNOWN_CODE_BY_VM_SHA256 = {
|
|
(H5_KWS_KNOWN_BYTECODE_SHA256, H5_KWS_KNOWN_CONSTANTS_SHA256): H5_KWS_KNOWN_SCRIPT_CODE,
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class H5KwsFunctionRange:
|
|
instruction_index: int
|
|
start_const_index: int
|
|
end_const_index: int
|
|
start: int
|
|
end: int
|
|
|
|
@property
|
|
def length(self) -> int:
|
|
return self.end - self.start + 1
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class H5KwsVmSummary:
|
|
script_sha256: str
|
|
bytecode_sha256: str
|
|
constants_sha256: str
|
|
instruction_count: int
|
|
constant_count: int
|
|
min_opcode: int
|
|
max_opcode: int
|
|
opcode_histogram: dict[int, int]
|
|
function_ranges: list[H5KwsFunctionRange]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class H5KwsOpcodeHandler:
|
|
index: int
|
|
present: bool
|
|
label: str
|
|
used_count: int
|
|
body_sha16: str
|
|
body_preview: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class H5KwsInstruction:
|
|
index: int
|
|
opcode: int
|
|
label: str
|
|
p0: int
|
|
p1: int
|
|
p2: int
|
|
p3: int
|
|
operand_a: str
|
|
operand_b: str
|
|
|
|
def to_text(self) -> str:
|
|
return (
|
|
f"{self.index:04d}: op{self.opcode:02d} {self.label:<22} "
|
|
f"{self.operand_a:<22} {self.operand_b}"
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class H5KwsFunctionAnalysis:
|
|
ordinal: int
|
|
name: str
|
|
create_index: int
|
|
assigned_scope: int | None
|
|
start: int
|
|
end: int
|
|
length: int
|
|
call_apply_count: int
|
|
branch_targets: list[int]
|
|
opcode_histogram: dict[int, int]
|
|
|
|
|
|
_OPCODE_LABELS = {
|
|
0: "construct_new",
|
|
1: "bit_and",
|
|
2: "shift_left",
|
|
3: "pre_dec_assign",
|
|
4: "throw_stack",
|
|
5: "less_equal",
|
|
6: "bit_or",
|
|
7: "pop_saved_result_to_r4",
|
|
8: "add",
|
|
9: "return_value",
|
|
10: "bit_xor",
|
|
11: "noop",
|
|
12: "make_function",
|
|
13: "in_operator",
|
|
14: "bit_not",
|
|
15: "hole_unused",
|
|
16: "push_result_save",
|
|
17: "enter_closure_scope",
|
|
18: "jump",
|
|
19: "post_inc_assign",
|
|
20: "not_strict_equal",
|
|
21: "typeof",
|
|
22: "not_equal",
|
|
23: "shift_right",
|
|
24: "call_apply",
|
|
25: "make_reference",
|
|
26: "leave_scope",
|
|
27: "pre_inc_assign",
|
|
28: "push_r0",
|
|
29: "multiply",
|
|
30: "return_undefined",
|
|
31: "modulo",
|
|
32: "noop",
|
|
33: "try_catch_finally",
|
|
34: "declare_undefined",
|
|
35: "divide",
|
|
36: "subtract",
|
|
37: "object_literal",
|
|
38: "logical_not",
|
|
39: "store_global_object",
|
|
40: "strict_equal",
|
|
41: "greater_equal",
|
|
42: "instanceof",
|
|
43: "unary_minus",
|
|
44: "jump_if_false",
|
|
45: "stack_length_to_r3",
|
|
46: "load_value",
|
|
47: "logical_and",
|
|
48: "delete_property",
|
|
49: "array_from_stack",
|
|
50: "peek_saved_result_to_r4",
|
|
51: "shift_unsigned_right",
|
|
52: "post_dec_assign",
|
|
53: "noop",
|
|
54: "pop_stack_to_r1",
|
|
55: "peek_stack_to_r0",
|
|
56: "jump_sentinel",
|
|
57: "logical_or",
|
|
58: "unary_plus",
|
|
59: "greater_than",
|
|
60: "equal",
|
|
61: "jump_if_true",
|
|
62: "load_global_store",
|
|
63: "debugger",
|
|
64: "less_than",
|
|
65: "assign_reference",
|
|
66: "enter_null_scope",
|
|
}
|
|
|
|
|
|
def _find_matching_js_bracket(text: str, open_index: int) -> int:
|
|
pairs = {"[": "]", "{": "}", "(": ")"}
|
|
opener = text[open_index]
|
|
closer = pairs[opener]
|
|
depth = 1
|
|
quote: str | None = None
|
|
escaped = False
|
|
for index in range(open_index + 1, len(text)):
|
|
ch = text[index]
|
|
if quote:
|
|
if escaped:
|
|
escaped = False
|
|
elif ch == "\\":
|
|
escaped = True
|
|
elif ch == quote:
|
|
quote = None
|
|
continue
|
|
if ch in {"'", '"', "`"}:
|
|
quote = ch
|
|
elif ch == opener:
|
|
depth += 1
|
|
elif ch == closer:
|
|
depth -= 1
|
|
if depth == 0:
|
|
return index
|
|
raise ValueError("matching JavaScript bracket not found")
|
|
|
|
|
|
def _split_top_level_js_array(array_text: str) -> list[str]:
|
|
parts: list[str] = []
|
|
start = 0
|
|
depth = 0
|
|
quote: str | None = None
|
|
escaped = False
|
|
for index, ch in enumerate(array_text):
|
|
if quote:
|
|
if escaped:
|
|
escaped = False
|
|
elif ch == "\\":
|
|
escaped = True
|
|
elif ch == quote:
|
|
quote = None
|
|
continue
|
|
if ch in {"'", '"', "`"}:
|
|
quote = ch
|
|
elif ch in "[{(":
|
|
depth += 1
|
|
elif ch in "]})":
|
|
depth -= 1
|
|
elif ch == "," and depth == 0:
|
|
parts.append(array_text[start:index].strip())
|
|
start = index + 1
|
|
parts.append(array_text[start:].strip())
|
|
return parts
|
|
|
|
|
|
def _extract_js_array_literal(script_text: str, var_name: str) -> list[str]:
|
|
marker = f"var {var_name} = ["
|
|
marker_pos = script_text.find(marker)
|
|
if marker_pos < 0:
|
|
raise ValueError(f"{var_name} array not found")
|
|
open_index = script_text.find("[", marker_pos + len(f"var {var_name} = "))
|
|
close_index = _find_matching_js_bracket(script_text, open_index)
|
|
return _split_top_level_js_array(script_text[open_index + 1 : close_index])
|
|
|
|
|
|
def _extract_jimbei_container(script_text: str) -> dict[str, Any]:
|
|
marker = "Jimbei()(window,"
|
|
marker_pos = script_text.find(marker)
|
|
if marker_pos < 0:
|
|
raise ValueError("KWS Jimbei invocation not found")
|
|
object_start = script_text.find("{", marker_pos + len(marker))
|
|
if object_start < 0:
|
|
raise ValueError("KWS Jimbei container object not found")
|
|
container, _end = json.JSONDecoder().raw_decode(script_text[object_start:])
|
|
if not isinstance(container, dict):
|
|
raise ValueError("KWS Jimbei container must be a JSON object")
|
|
if not isinstance(container.get("b"), str) or not isinstance(container.get("d"), list):
|
|
raise ValueError("KWS Jimbei container must contain b:string and d:list")
|
|
return container
|
|
|
|
|
|
def _decode_bytecode_values(encoded: str) -> list[int]:
|
|
# Jimbei's loader applies Base64 -> UTF-8 string -> charCodeAt(char) - 1.
|
|
decoded_text = base64.b64decode(encoded, validate=True).decode("utf-8")
|
|
values = [ord(ch) - 1 for ch in decoded_text]
|
|
if len(values) % 5 != 0:
|
|
raise ValueError("KWS bytecode value count must be divisible by 5")
|
|
return values
|
|
|
|
|
|
def _instruction_rows(values: list[int]) -> list[list[int]]:
|
|
return [values[i : i + 5] for i in range(0, len(values), 5)]
|
|
|
|
|
|
def _bytecode_hash(values: list[int]) -> str:
|
|
# Keep the historical corpus hash stable: the VM bytes are stored as
|
|
# integer cells; cells above 255 are represented by their low byte here.
|
|
return hashlib.sha256(bytes((value & 0xFF) for value in values)).hexdigest()
|
|
|
|
|
|
def _constants_hash(constants: list[Any]) -> str:
|
|
text = json.dumps(constants, ensure_ascii=False, separators=(",", ":"))
|
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _constant_int(constants: list[Any], source_type: int, const_index: int) -> int | None:
|
|
if source_type != 6 or const_index < 0 or const_index >= len(constants):
|
|
return None
|
|
value = constants[const_index]
|
|
return value if isinstance(value, int) else None
|
|
|
|
|
|
def _format_operand(constants: list[Any], source_type: int, operand_index: int) -> str:
|
|
if source_type == 0:
|
|
return f"unused({operand_index})"
|
|
if source_type == 1:
|
|
return f"reg[{operand_index}]"
|
|
if source_type == 2:
|
|
return f"arg[{operand_index}]"
|
|
if source_type == 3:
|
|
return f"scope[{operand_index}]"
|
|
if source_type == 4:
|
|
name = constants[operand_index] if 0 <= operand_index < len(constants) else None
|
|
return f"window_const[{operand_index}]={name!r}"
|
|
if source_type == 5:
|
|
return f"this[{operand_index}]"
|
|
if source_type == 6:
|
|
value = constants[operand_index] if 0 <= operand_index < len(constants) else None
|
|
return f"const[{operand_index}]={value!r}"
|
|
if source_type == 7:
|
|
return f"callctx[{operand_index}]"
|
|
if source_type == 8:
|
|
return f"global_store[{operand_index}]"
|
|
return f"src{source_type}[{operand_index}]"
|
|
|
|
|
|
def _extract_function_ranges(rows: list[list[int]], constants: list[Any]) -> list[H5KwsFunctionRange]:
|
|
ranges: list[H5KwsFunctionRange] = []
|
|
for instruction_index, row in enumerate(rows):
|
|
opcode, p0, p1, p2, p3 = row
|
|
if opcode != 12:
|
|
continue
|
|
start = _constant_int(constants, p0, p1)
|
|
end = _constant_int(constants, p2, p3)
|
|
if start is None or end is None:
|
|
continue
|
|
if 0 <= start <= end < len(rows):
|
|
ranges.append(
|
|
H5KwsFunctionRange(
|
|
instruction_index=instruction_index,
|
|
start_const_index=p1,
|
|
end_const_index=p3,
|
|
start=start,
|
|
end=end,
|
|
)
|
|
)
|
|
return ranges
|
|
|
|
|
|
def _assigned_scope_after_create(rows: list[list[int]], create_index: int) -> int | None:
|
|
next_index = create_index + 1
|
|
if next_index >= len(rows):
|
|
return None
|
|
opcode, p0, p1, p2, _p3 = rows[next_index]
|
|
# make_function leaves the new function in VM reg0. A following
|
|
# assign_reference(scope[x], reg0) means the function is named by scope[x].
|
|
if opcode == 65 and p0 == 3 and p2 == 1:
|
|
return p1
|
|
return None
|
|
|
|
|
|
def _function_name(start: int, end: int, assigned_scope: int | None) -> str:
|
|
if start == 3063 and end == 4407 and assigned_scope == 80:
|
|
return "scope80_main_orchestrator"
|
|
if start == 4662 and end == 4663:
|
|
return "inline_return_undefined_stub"
|
|
if start == 4664 and end == 4675:
|
|
return "inline_call_scope107_with_arg0"
|
|
if assigned_scope is not None:
|
|
return f"scope{assigned_scope}_fn_{start}_{end}"
|
|
return f"inline_fn_{start}_{end}"
|
|
|
|
|
|
def _branch_target(constants: list[Any], row: list[int]) -> int | None:
|
|
opcode, p0, p1, _p2, _p3 = row
|
|
if opcode not in {18, 44, 61}:
|
|
return None
|
|
target = constants[p1] if p0 == 6 and 0 <= p1 < len(constants) else p1
|
|
return target if isinstance(target, int) else None
|
|
|
|
|
|
def parse_h5_kws_vm_script(script_path: str | Path) -> H5KwsVmSummary:
|
|
script = Path(script_path).read_text(encoding="utf-8")
|
|
container = _extract_jimbei_container(script)
|
|
constants = container["d"]
|
|
values = _decode_bytecode_values(container["b"])
|
|
rows = _instruction_rows(values)
|
|
opcodes = [row[0] for row in rows]
|
|
histogram = dict(sorted(Counter(opcodes).items()))
|
|
return H5KwsVmSummary(
|
|
script_sha256=hashlib.sha256(script.encode("utf-8")).hexdigest(),
|
|
bytecode_sha256=_bytecode_hash(values),
|
|
constants_sha256=_constants_hash(constants),
|
|
instruction_count=len(rows),
|
|
constant_count=len(constants),
|
|
min_opcode=min(opcodes),
|
|
max_opcode=max(opcodes),
|
|
opcode_histogram=histogram,
|
|
function_ranges=_extract_function_ranges(rows, constants),
|
|
)
|
|
|
|
|
|
def analyze_h5_kws_function_ranges(script_path: str | Path) -> list[H5KwsFunctionAnalysis]:
|
|
"""Summarize Jimbei function ranges with names, branches and call counts."""
|
|
|
|
script = Path(script_path).read_text(encoding="utf-8")
|
|
container = _extract_jimbei_container(script)
|
|
constants = container["d"]
|
|
rows = _instruction_rows(_decode_bytecode_values(container["b"]))
|
|
ranges = _extract_function_ranges(rows, constants)
|
|
analyses: list[H5KwsFunctionAnalysis] = []
|
|
for ordinal, function_range in enumerate(ranges):
|
|
body_rows = rows[function_range.start : function_range.end + 1]
|
|
histogram = dict(sorted(Counter(row[0] for row in body_rows).items()))
|
|
branch_targets = [
|
|
target
|
|
for row in body_rows
|
|
if (target := _branch_target(constants, row)) is not None
|
|
]
|
|
assigned_scope = _assigned_scope_after_create(rows, function_range.instruction_index)
|
|
analyses.append(
|
|
H5KwsFunctionAnalysis(
|
|
ordinal=ordinal,
|
|
name=_function_name(function_range.start, function_range.end, assigned_scope),
|
|
create_index=function_range.instruction_index,
|
|
assigned_scope=assigned_scope,
|
|
start=function_range.start,
|
|
end=function_range.end,
|
|
length=function_range.length,
|
|
call_apply_count=histogram.get(24, 0),
|
|
branch_targets=branch_targets,
|
|
opcode_histogram=histogram,
|
|
)
|
|
)
|
|
return analyses
|
|
|
|
|
|
def extract_h5_kws_opcode_handlers(script_path: str | Path) -> list[H5KwsOpcodeHandler]:
|
|
"""Extract and label the Jimbei opcode handler array from the script."""
|
|
|
|
script = Path(script_path).read_text(encoding="utf-8")
|
|
summary = parse_h5_kws_vm_script(script_path)
|
|
parts = _extract_js_array_literal(script, "_sabo_57b82")
|
|
handlers: list[H5KwsOpcodeHandler] = []
|
|
for index, body in enumerate(parts):
|
|
present = bool(body)
|
|
body_sha16 = hashlib.sha256(body.encode("utf-8")).hexdigest()[:16] if present else ""
|
|
preview = " ".join(body.split())[:240] if present else ""
|
|
handlers.append(
|
|
H5KwsOpcodeHandler(
|
|
index=index,
|
|
present=present,
|
|
label=_OPCODE_LABELS.get(index, "unknown"),
|
|
used_count=summary.opcode_histogram.get(index, 0),
|
|
body_sha16=body_sha16,
|
|
body_preview=preview,
|
|
)
|
|
)
|
|
return handlers
|
|
|
|
|
|
def disassemble_h5_kws_range(
|
|
script_path: str | Path,
|
|
start: int,
|
|
end: int,
|
|
) -> list[H5KwsInstruction]:
|
|
"""Disassemble a bytecode row range with opcode labels and operands."""
|
|
|
|
if start < 0 or end < start:
|
|
raise ValueError("invalid KWS bytecode range")
|
|
script = Path(script_path).read_text(encoding="utf-8")
|
|
container = _extract_jimbei_container(script)
|
|
constants = container["d"]
|
|
rows = _instruction_rows(_decode_bytecode_values(container["b"]))
|
|
if end >= len(rows):
|
|
raise ValueError("KWS bytecode range exceeds instruction count")
|
|
instructions: list[H5KwsInstruction] = []
|
|
for index in range(start, end + 1):
|
|
opcode, p0, p1, p2, p3 = rows[index]
|
|
instructions.append(
|
|
H5KwsInstruction(
|
|
index=index,
|
|
opcode=opcode,
|
|
label=_OPCODE_LABELS.get(opcode, "unknown"),
|
|
p0=p0,
|
|
p1=p1,
|
|
p2=p2,
|
|
p3=p3,
|
|
operand_a=_format_operand(constants, p0, p1),
|
|
operand_b=_format_operand(constants, p2, p3),
|
|
)
|
|
)
|
|
return instructions
|
|
|
|
|
|
def kwscode_from_known_h5_kws_script(script_path: str | Path) -> str | None:
|
|
summary = parse_h5_kws_vm_script(script_path)
|
|
return _KNOWN_CODE_BY_SCRIPT_SHA256.get(summary.script_sha256) or _KNOWN_CODE_BY_VM_SHA256.get(
|
|
(summary.bytecode_sha256, summary.constants_sha256)
|
|
)
|
|
|
|
|
|
__all__ = [
|
|
"H5_KWS_KNOWN_BYTECODE_SHA256",
|
|
"H5_KWS_KNOWN_CONSTANTS_SHA256",
|
|
"H5_KWS_KNOWN_SCRIPT_CODE",
|
|
"H5_KWS_KNOWN_SCRIPT_SHA256",
|
|
"H5KwsFunctionRange",
|
|
"H5KwsFunctionAnalysis",
|
|
"H5KwsInstruction",
|
|
"H5KwsOpcodeHandler",
|
|
"H5KwsVmSummary",
|
|
"analyze_h5_kws_function_ranges",
|
|
"disassemble_h5_kws_range",
|
|
"extract_h5_kws_opcode_handlers",
|
|
"kwscode_from_known_h5_kws_script",
|
|
"parse_h5_kws_vm_script",
|
|
]
|