ksjsb/tools/deobf2.py
2026-07-30 20:25:56 +08:00

133 lines
4.6 KiB
Python

"""通用多表反混淆器:自动探测所有 obfuscator.io 字符串表并全文替换。
每张表结构:
var ARR=["b64",...];
(function(e,n){var t=function(n){while(--n)e["push"](e["shift"]())};t(++n)})(ARR,N); // N 次旋转
var DEC=function(e,n){e-=K;var t=ARR[e]; ... CpCnJS(t)=utf8(atob(t)) ...}
解码: rotated_ARR[e-K] 经 base64->utf8.
"""
import re, sys, json, base64
from pathlib import Path
SRC = Path(sys.argv[1])
s = SRC.read_text(encoding="utf-8", errors="ignore")
def bracket_extract(src: str, open_idx: int) -> int:
"""给定 '[' 的位置(已位于 '['),返回匹配 ']' 的位置。字符串感知。"""
depth = 0
in_str = False
q = ""
esc = False
j = open_idx
while j < len(src):
ch = src[j]
if in_str:
if esc:
esc = False
elif ch == "\\":
esc = True
elif ch == q:
in_str = False
else:
if ch in "\"'`":
in_str = True
q = ch
elif ch == "[":
depth += 1
elif ch == "]":
depth -= 1
if depth == 0:
return j
j += 1
return -1
def parse_array_literal(src: str, arr: str, before: int = -1) -> list[str]:
"""找 arr=[ 字面量。before>=0 时只在该偏移之前找最近一个(按邻近消歧跨模块短名冲突)。"""
pat = re.compile(r"(?<![A-Za-z0-9_$.])" + re.escape(arr) + r"\s*=\s*\[")
matches = list(pat.finditer(src))
if before >= 0:
matches = [m for m in matches if m.start() < before]
if not matches:
return []
m = matches[-1] # 离解码器最近的那个
ob = m.end() - 1 # 指向 '['
cb = bracket_extract(src, ob)
body = src[ob + 1:cb]
# 拆分顶层逗号
out, cur, in_str, q, esc, depth = [], [], False, "", False, 0
for ch in body:
if in_str:
cur.append(ch)
if esc: esc = False
elif ch == "\\": esc = True
elif ch == q: in_str = False
else:
if ch in "\"'`": in_str, q = True, ch; cur.append(ch)
elif ch == "[": depth += 1; cur.append(ch)
elif ch == "]": depth -= 1; cur.append(ch)
elif ch == "," and depth == 0: out.append("".join(cur)); cur = []
else: cur.append(ch)
if "".join(cur).strip(): out.append("".join(cur))
res = []
for e in out:
e = e.strip()
if e and e[0] in "\"'`": e = e[1:-1]
res.append(e)
return res
def b64utf8(raw: str) -> str:
try:
return base64.b64decode(raw).decode("utf-8")
except Exception:
try: return base64.b64decode(raw + "==").decode("utf-8", "replace")
except Exception: return f"<FAIL:{raw}>"
def rotate(arr, n):
a = list(arr)
for _ in range(n):
a.append(a.pop(0))
return a
# 1) 探测所有解码器: var DEC=function(e,n){e-=K;var t=ARR[e];
decoders = []
for m in re.finditer(r"var\s+(\w+)\s*=\s*function\(e,n\)\{e-=(\d+);var t=(\w+)\[e\];", s):
decoders.append((m.group(1), int(m.group(2)), m.group(3), m.start()))
print(f"[+] 探测到 {len(decoders)} 个解码器")
# 2) 构建 map 并替换(每个解码器用自己偏移前的数组 + 自己的旋转数)
out = s
tables = {}
for dec, off, arr, doff in decoders:
raw = parse_array_literal(s, arr, before=doff)
if not raw:
print(f" [-] {dec}/{arr}@{doff}: 数组字面量未找到")
continue
# 旋转数: 解码器前最近的 })(ARR,N)
rpat = re.compile(r"\}\)\(" + re.escape(arr) + r",(\d+)\)")
rm = [x for x in rpat.finditer(s) if x.start() < doff]
rot = int(rm[-1].group(1)) if rm else 0
rotated = rotate(raw, rot)
mp = {i: b64utf8(v) for i, v in enumerate(rotated)}
tables[dec] = (arr, off, rot, mp)
def make_repl(mp, off):
def repl(m):
idx = int(m.group(1), 16) - off
v = mp.get(idx)
return json.dumps(v, ensure_ascii=False) if v is not None else m.group(0)
return repl
out = re.sub(re.escape(dec) + r'\(\s*"(0x[0-9a-fA-F]+)"\s*\)', make_repl(mp, off), out)
out = re.sub(re.escape(dec) + r"\(\s*'(0x[0-9a-fA-F]+)'\s*\)", make_repl(mp, off), out)
print(f" [+] {dec}/{arr}@{doff}: {len(mp)} 项, rotate={rot}, off={off}, 0x0={mp.get(0)!r}")
dest = SRC.with_name(SRC.stem + ".deobf2.js")
dest.write_text(out, encoding="utf-8")
print(f"\n[+] 写出 {dest}")
json.dump({dec: {f"0x{int(k)+off:x}": v for k, v in info[3].items()} for dec, info in tables.items()},
open(SRC.with_name("all_tables.json"), "w", encoding="utf-8"), ensure_ascii=False)
print("[+] 全表导出 all_tables.json")