analyze_wechathook.py
8.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
#!/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()