analyze_wechathook.py 8.21 KB
#!/usr/bin/env python3
"""
analyze_wechathook.py — 从 Tinker patch DEX 分析 WeChat 8.0.74 消息发送路径
在 Termux 上运行: python3 /sdcard/wechathook/analyze_wechathook.py

目标:
1. 解码 w11.r0 构造器字节码,看它调了什么
2. 解码 gm0.j1.i() 字节码,确认它检查的是字段 m
3. 检查 gm0.j1.m 在运行时是否 null(通过分析 gm0.j1.g() 是否被调用)
"""
import struct, os, subprocess

# Tinker patch DEX (需要有 root 才能访问)
TINKER_APK = "/data/user/0/com.tencent.mm/tinker/patch-cf87f687/dex/tinker_classN.apk"
WORK_DIR = "/data/local/tmp/wh_dex_analysis"

def ensure_dex():
    os.makedirs(WORK_DIR, exist_ok=True)
    dex = f"{WORK_DIR}/classes12.dex"
    if not os.path.exists(dex):
        # 需要 root 拷贝
        r = subprocess.run(["su", "-c", f"cp {TINKER_APK} /data/local/tmp/_tinker.apk && chmod 644 /data/local/tmp/_tinker.apk"],
                           capture_output=True, text=True)
        if r.returncode != 0:
            print("需要 root 权限:", r.stderr)
            return None
        import zipfile
        with zipfile.ZipFile("/data/local/tmp/_tinker.apk") as z:
            z.extract("classes12.dex", WORK_DIR)
        print(f"Extracted classes12.dex ({os.path.getsize(dex)} bytes)")
    return dex

def uleb128(data, off):
    r, s = 0, 0
    while True:
        b = data[off]; off += 1
        r |= (b & 0x7F) << s
        if not (b & 0x80): break
        s += 7
    return r, off

def parse_dex(path):
    data = open(path, "rb").read()
    h = lambda o: struct.unpack_from('<I', data, o)[0]
    ss, so = h(56), h(60)
    ts, to = h(64), h(68)
    fs, fo = h(80), h(84)
    ms, mo = h(88), h(92)
    cs, co = h(96), h(100)
    ps, po = h(72), h(76)

    def gs(i):
        if i >= ss: return f"?s{i}"
        off = h(so + i*4); _, o2 = uleb128(data, off)
        return data[o2:data.index(b'\x00', o2)].decode('utf-8', errors='replace')
    def gt(i):
        if i >= ts: return f"?t{i}"
        return gs(h(to + i*4))
    def gp(pi):
        if pi >= ps: return "?proto"
        pb = po + pi*12
        ret = gt(h(pb+4)); poff = h(pb+8); params = []
        if poff:
            n = h(poff)
            for k in range(n): params.append(gt(struct.unpack_from('<H', data, poff+4+k*2)[0]))
        return f"({','.join(params)}){ret}"

    classes = {}
    for i in range(cs):
        base = co + i*32
        ci = h(base)
        if ci >= ts: continue
        name = gt(ci)
        cdo = h(base+24)
        if cdo == 0: continue
        off = cdo
        sf, off = uleb128(data, off)
        inf, off = uleb128(data, off)
        dm, off = uleb128(data, off)
        vm, off = uleb128(data, off)

        fields, methods = [], []
        fi = 0
        for _ in range(sf+inf):
            d, off = uleb128(data, off); fi += d
            fl, off = uleb128(data, off)
            if fi < fs:
                fb = fo+fi*8
                ft = struct.unpack_from('<H', data, fb+2)[0]
                fn = struct.unpack_from('<I', data, fb+4)[0]
                fields.append((gs(fn), gt(ft), fl))

        mi = 0
        for _ in range(dm+vm):
            d, off = uleb128(data, off); mi += d
            fl, off = uleb128(data, off)
            co2, off = uleb128(data, off)
            if mi < ms:
                mb = mo+mi*8
                pi = struct.unpack_from('<H', data, mb+2)[0]
                mn = struct.unpack_from('<I', data, mb+4)[0]
                methods.append((gs(mn), gp(pi), fl, co2))
        classes[name] = {'fields': fields, 'methods': methods}
    return classes, data, to, ts, fo, fs, mo, ms, po, ps, so, ss

def dump_bytecode(data, code_off, max_insns=80):
    """简单 Dalvik 字节码 dump(只看调用指令)"""
    if code_off == 0: return "  (abstract)"
    # code_item header: registers_size(2) ins_size(2) outs_size(2) tries_size(2) debug_info_off(4) insns_size(4)
    insns_size = struct.unpack_from('<I', data, code_off+12)[0]
    insns_off = code_off + 16
    lines = []
    i = 0
    while i < insns_size and len(lines) < max_insns:
        op = data[insns_off + i*2]
        # Only show invoke-* and sget/sput/iget/iput
        if op in (0x6e, 0x6f, 0x70, 0x71, 0x72,   # invoke-virtual, super, direct, static, interface
                  0x74, 0x75, 0x76, 0x77, 0x78,   # invoke-virtual/range etc
                  0x60, 0x61, 0x62, 0x63, 0x64,   # iget, iget-wide, iget-object, iget-bool, iget-byte
                  0x68, 0x69, 0x6a, 0x6b, 0x6c,   # sget, sget-wide, sget-object, sget-bool, sget-byte
                  0x59, 0x5a, 0x5b, 0x5c, 0x5d,   # iput, iput-wide, iput-object, iput-bool, iput-byte
                  0x67, 0x69, 0x6b):               # sput, sput-object, sput-bool
            ref = struct.unpack_from('<H', data, insns_off + i*2 + 2)[0]
            lines.append(f"  +{i*2:04x} op=0x{op:02x} ref={ref}")
            i += 3
        elif op in (0x0e, 0x0f, 0x10, 0x11):  # return-void, return, return-wide, return-object
            lines.append(f"  +{i*2:04x} return")
            break
        elif op == 0x27:  # throw
            lines.append(f"  +{i*2:04x} throw")
            i += 2
        else:
            # Rough skip — most insns are 1-2 units
            fmt = (op >> 4) & 0xf
            i += 2 if op in range(0x02, 0x0e) or op in range(0x1a, 0x22) or op >= 0x26 else 1
    return '\n'.join(lines) if lines else "  (no invoke/field ops)"

def main():
    dex = ensure_dex()
    if not dex:
        return

    print("Parsing DEX...")
    classes, data, to, ts, fo, fs, mo, ms, po, ps, so, ss = parse_dex(dex)

    # ── 1. gm0.j1 structure ──────────────────────────────────────────
    print("\n" + "="*60)
    print("1. gm0.j1 class")
    print("="*60)
    j1 = classes.get("Lgm0/j1;")
    if j1:
        print("Static fields:")
        for name, typ, flags in j1['fields']:
            print(f"  {name}: {typ}  flags=0x{flags:x}")
        print("\nMethods with signatures:")
        for name, proto, flags, co2 in j1['methods']:
            flag_s = '+'.join(f for f, b in [('public',1),('static',8),('sync',0x20),('ctor',0x10000)] if flags&b)
            print(f"  {name}: {proto}  [{flag_s}]")
    else:
        print("NOT FOUND")

    # ── 2. w11.r0 structure ──────────────────────────────────────────
    print("\n" + "="*60)
    print("2. w11.r0 class")
    print("="*60)
    r0 = classes.get("Lw11/r0;")
    if r0:
        print("Static fields:")
        for name, typ, flags in r0['fields']:
            print(f"  {name}: {typ}")
        print(f"\nConstructors (looking for (String,String,int,int,long)):")
        for name, proto, flags, co2 in r0['methods']:
            if name == "<init>":
                print(f"  <init>{proto}  code_off=0x{co2:x}")
    else:
        print("NOT FOUND")

    # ── 3. Decode gm0.j1.i() bytecode ───────────────────────────────
    print("\n" + "="*60)
    print("3. gm0.j1.i() bytecode (key ops)")
    print("="*60)
    if j1:
        for name, proto, flags, co2 in j1['methods']:
            if name == "i" and "()Lgm0/j1;" in proto:
                print(f"gm0.j1.i() code_off=0x{co2:x}")
                print(dump_bytecode(data, co2))
                break

    # ── 4. Decode w11.r0 (String,String,int,int,long) ctor ──────────
    print("\n" + "="*60)
    print("4. w11.r0 constructor bytecode (key invoke ops)")
    print("="*60)
    if r0:
        for name, proto, flags, co2 in r0['methods']:
            if name == "<init>" and "Ljava/lang/String;Ljava/lang/String;IIJ" in proto:
                print(f"<init>(String,String,int,int,long) code_off=0x{co2:x}")
                print(dump_bytecode(data, co2, max_insns=40))
                break

    # ── 5. Who initializes gm0.j1.m? Search for sput-object on field m ─
    print("\n" + "="*60)
    print("5. Searching gm0.j1.g() bytecode")
    print("="*60)
    if j1:
        for name, proto, flags, co2 in j1['methods']:
            if name == "g":
                print(f"gm0.j1.g{proto} code_off=0x{co2:x}")
                print(dump_bytecode(data, co2))
                break

    print("\n== Done ==")

if __name__ == "__main__":
    main()