docs(huya): 模拟器存活闪退诊断报告与 Frida 探测脚本证据

- 诊断报告: attach 主进程静默退出/EGL 崩溃, 仅约 4s 窗口可抓帧
- scripts: attach/spawn/hook/emu 系列 Frida 脚本与抓帧/验证工具
- evidence: identity/reqchain/frame/inputbuf/magic_buf/propedge 抓取样本,
  emu_* 存活对比, diag_* 策略实验, baseline 裸测基准
This commit is contained in:
yml2213
2026-08-27 17:58:32 +08:00
parent 36b5d78050
commit 49c5c36c05
100 changed files with 8666 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""attach + 全部 bypass: 观察 attach 路径加载 bypass 后 App 是否存活。
用于定位 attach 后反调试点。
"""
from __future__ import annotations
import frida, time, subprocess
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
BYPASSES = ["bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js", "patch_guard_block_termination.js"]
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE], capture_output=True)
time.sleep(1)
subprocess.run(["adb","-s","127.0.0.1:5555","shell","monkey","-p",PACKAGE,"-c","android.intent.category.LAUNCHER","1"], capture_output=True)
print("app launched normally, waiting 8s...", flush=True)
time.sleep(8)
d = frida.get_device_manager().add_remote_device(REMOTE)
ps = [p for p in d.enumerate_processes() if 'kiwi' in p.name or 'duowan' in p.name]
if not ps:
print("no kiwi process", flush=True); return
pid = ps[0].pid
print("attach pid", pid, flush=True)
s = d.attach(pid)
for name in BYPASSES:
try:
sc = s.create_script((RE/"evidence/scripts"/name).read_text())
def on_msg(m, dd, n=name):
if m.get('type')=='send':
print(f"[{n}] {m.get('payload')}", flush=True)
elif m.get('type')=='error':
print(f"[{n}] ERR {str(m)[:120]}", flush=True)
sc.on('message', on_msg)
sc.load(); time.sleep(0.3)
except Exception as e:
print(f"[{name}] load ERR {e}", flush=True)
print("all bypass loaded. observing 30s", flush=True)
prev = 0
for t in [3, 6, 10, 15, 20, 30]:
time.sleep(t - prev); prev = t
try:
alive = [p for p in d.enumerate_processes() if p.pid == pid]
except Exception:
break
print(f" +{t}s alive={bool(alive)}", flush=True)
if not alive:
print(f"=> DEAD at +{t}s", flush=True); break
try: s.detach()
except: pass
if __name__ == "__main__":
main()
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""attach + 全部 bypass + 主 hook: 验证 attach 路径能存活且抓到 dfpReport。
这是"不挂起、不 spawn"的路线, 无 EGL 崩问题。
"""
from __future__ import annotations
import frida, time, subprocess, json, re
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/emu_attach_full.json")
BYPASSES = ["bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js", "patch_guard_block_termination.js"]
MAIN_JS = """
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',len:len,hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
try{
var r2=new ApiResolver('module');
r2.enumerateMatchesSync('exports:*!SSL_read').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){this.buf=a[1];},
onLeave:function(ret){
var nn=ret.toInt32();
if(nn<=0||nn>4000)return;
send({type:'resp',len:nn,hex:hexb(this.buf,nn)});
}});
});
send({type:'readhooked'});
}catch(e){send({type:'readerr',e:String(e)});}
"""
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE], capture_output=True)
time.sleep(1)
subprocess.run(["adb","-s","127.0.0.1:5555","shell","monkey","-p",PACKAGE,"-c","android.intent.category.LAUNCHER","1"], capture_output=True)
print("app launched, waiting for dfp cold-start... attach fast", flush=True)
d = frida.get_device_manager().add_remote_device(REMOTE)
# 用 adb pidof 确定的 pid (更可靠)
pid = None
for _ in range(15):
r = subprocess.run(["adb","-s","127.0.0.1:5555","shell","pidof",PACKAGE], capture_output=True, text=True)
p = r.stdout.strip()
if p:
pid = int(p)
break
time.sleep(1)
if not pid:
# fallback frida enumerate
ps = [x for x in d.enumerate_processes() if 'kiwi' in x.name or 'duowan' in x.name]
pid = ps[0].pid if ps else None
print("attach pid", pid, flush=True)
s = d.attach(pid)
for name in BYPASSES:
try:
sc = s.create_script((RE/"evidence/scripts"/name).read_text()); sc.load(); time.sleep(0.2)
except Exception as e:
print(f"bp err {name} {e}", flush=True)
events = []
def on_main(m, dta):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:120], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'armed': print("[*] armed", flush=True)
elif t == 'hooked': print("[*] SSL_write hooked", flush=True)
elif t in ('err','readerr'): print("[*]", t, p.get('e'), flush=True)
elif 'cls' in p:
print(f"[*] {p['cls']} len={p['len']}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
elif t == 'resp':
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p.get('hex') or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
print(f"[*] RESP len={p['len']}{mark}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
sc = s.create_script(MAIN_JS)
sc.on('message', on_main)
sc.load()
# 观察 60s, 若触发 dfpReport 会有; 同时可通过 am 触发
print("[*] observing 60s", flush=True)
prev = 0
for t in [5,10,15,20,30,40,50,60]:
time.sleep(t-prev); prev=t
try:
alive = [p for p in d.enumerate_processes() if p.pid==pid]
except Exception: break
if not alive: print(f"=> DEAD at +{t}s", flush=True); break
print(f"[*] done, {len(events)} events", flush=True)
if __name__ == "__main__":
main()
+46
View File
@@ -0,0 +1,46 @@
"""模拟器 attach + 真机G2-0055组合bypass 存活测试. attach避开EGL崩, G2-0055过反调试."""
from pathlib import Path
import frida, time, subprocess
REMOTE="127.0.0.1:31878"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
BYPASS="bypass_msaoaid_maps_skip_cleanup.js"
def main():
d=frida.get_device_manager().add_remote_device(REMOTE)
for r in range(1,4):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1)
subprocess.run(["adb","-s","127.0.0.1:5555","shell","monkey","-p",PACKAGE,"-c","android.intent.category.LAUNCHER","1"],capture_output=True)
time.sleep(7)
# attach
pid=None
for _ in range(10):
rr=subprocess.run(["adb","-s","127.0.0.1:5555","shell","pidof",PACKAGE],capture_output=True,text=True)
if rr.stdout.strip(): pid=int(rr.stdout.strip()); break
time.sleep(1)
if not pid: print(f"r{r} no pid"); continue
try:
s=d.attach(pid)
except Exception as e:
print(f"r{r} attach err {e}"); time.sleep(2); continue
# 加载 G2-0055
def on(m,dd):
if m.get('type')=='send':
p=m.get('payload');
if isinstance(p,dict) and p.get('event') in ('installed','predicate-branch-patched','art-callsite-patched','name-masked','fd-masked','maps-entry','art-entry','art-callsite'):
print(f" r{r} {p['event']}",flush=True)
elif m.get('type')=='error': print(f" r{r} ERR {str(m)[:80]}",flush=True)
sc=s.create_script((RE/"evidence/scripts"/BYPASS).read_text()); sc.on('message',on); sc.load()
print(f"r{r} attach pid={pid} +G2-0055 loaded, 观察存活",flush=True)
prev=0; died=None
for t in [3,6,10,15,20,30,40,50,60]:
time.sleep(t-prev); prev=t
try: alive=[p for p in d.enumerate_processes() if p.pid==pid]
except: break
if not alive:
died=t
print(f" r{r} DEAD at +{t}s",flush=True); break
if not died: print(f" r{r} alive 60s!",flush=True)
try: s.detach()
except: pass
time.sleep(2)
if __name__=="__main__": main()
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
""""一帧"dfpReport(含响应 actionV)—— 自动重试, 直到拿到为止。
目标:每一台干净模拟器, 抓到一帧完整 dfpReport wire + 对应的 606B 响应(actionV),
即获得这台"设备"的纯 Python 可重放身份凭证。App 存活 3-6s 足够(dfpReport 启动即发)。
"""
from __future__ import annotations
from pathlib import Path
import frida, time, subprocess, json, re
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/frame_" + time.strftime("%H%M%S") + ".json")
BYPASS = "bypass_msaoaid_maps_skip_cleanup.js" # 真机验证过的组合, 过 frida 反调试
MAIN_JS = """
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
var got_dfp=false;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0){
got_dfp=true;
send({type:'dfp',len:len,hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
try{
var r2=new ApiResolver('module');
r2.enumerateMatchesSync('exports:*!SSL_read').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){this.buf=a[1];},
onLeave:function(ret){
var nn=ret.toInt32();
if(nn<=0||nn>4000)return;
send({type:'resp',len:nn,hex:hexb(this.buf,nn)});
}});
});
send({type:'readhooked'});
}catch(e){send({type:'readerr',e:String(e)});}
"""
def one_try(d, attempt, result):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE], capture_output=True)
time.sleep(1.2)
pid = d.spawn([PACKAGE])
print(f"[att{attempt}] spawn pid={pid}", flush=True)
s = d.attach(pid)
# 挂起内加载 bypass (快速)
try:
sc = s.create_script((RE/"evidence/scripts"/BYPASS).read_text()); sc.load()
except Exception as e:
print(f"[att{attempt}] bypass err {e}", flush=True)
d.resume(pid)
print("[att%d] resumed (bypass loaded)" % attempt, flush=True)
got_dfp = False
def on_main(m, dd):
nonlocal got_dfp
if m.get('type') == 'error':
print(f"[att{attempt}] JSErr {str(m)[:100]}", flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'hooked':
print(f"[att{attempt}] SSL_write hooked", flush=True)
elif t == 'dfp':
got_dfp = True
print(f"[att{attempt}] dfpReport len={p['len']}", flush=True)
result['dfp_wire'] = p['hex']
result['dfp_pid'] = pid
elif t == 'resp':
# 保存 606B 响应 (可能含 actionV)
result.setdefault('resps', []).append({'len': p['len'], 'hex': p['hex']})
if p['len'] == 606:
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p['hex'] or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
else:
mark = ""
print(f"[att{attempt}] resp len={p['len']}{mark}", flush=True)
mainsc = s.create_script(MAIN_JS)
mainsc.on('message', on_main)
mainsc.load()
# 观察 ~15s 内抓 dfp (dfp 启动即发, 快)
t0 = time.time()
while time.time() - t0 < 15:
time.sleep(2)
if got_dfp:
time.sleep(3) # 再等一帧响应
break
# 确认拿到 dfp 且其响应缺失则不强求
print(f"[att{attempt}] 结束观察, got_dfp={got_dfp}", flush=True)
try: d.kill(pid)
except: pass
return got_dfp
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
result = {}
for attempt in range(1, 6):
try:
ok = one_try(d, attempt, result)
if ok and result.get('dfp_wire'):
print(f"[*] SUCCESS att{attempt}: dfp_wire_len={len(result['dfp_wire'])//2}$", flush=True)
json.dump(result, open(OUT, 'w'), indent=2)
print(f"[*] saved {OUT}", flush=True)
return
except Exception as e:
print(f"[att{attempt}] ERR {repr(e)}", flush=True)
time.sleep(2)
print("[*] 5 次均未抓到 dfpReport", flush=True)
if __name__ == "__main__":
main()
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""Runtime edge capture on live libhydeviceid generation:
1) hook __system_property_get -> what system inputs drive triple/device derivation
2) hook SSL_write -> the dfpReport wire (to correlate)
Only needs the ~3-6s spawn window (dfpReport fires ~1s after start).
"""
from pathlib import Path
import frida, time, subprocess, json, sys
REMOTE="127.0.0.1:31878"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT=Path("/Users/yml/codes/douyu_login_py/evidence/propedge_" + time.strftime("%H%M%S") + ".json")
JS = r"""
'use strict';
send({type:'armed'});
function rcs(p,n){try{return p.readCString(n)||'';}catch(e){return '';}}
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
var START = Date.now(); var t0 = Math.floor(Date.now()/1000);
// 1) system property reads
try{
var pg = Module.findExportByName('libc.so','__system_property_get');
if(pg){
Interceptor.attach(pg,{onEnter:function(a){this.name=rcs(a[0],256);this.valbuf=a[1];},
onLeave:function(ret){
try{
var val=rcs(this.valbuf,512);
// 只关心设备/身份派生相关属性, 避免把框架进程的读取混进来
var isDevice = this.name.indexOf('ro.product')===0 || this.name.indexOf('ro.serialno')===0
|| this.name.indexOf('ro.boot')===0 || this.name.indexOf('ro.hardware')===0
|| this.name.indexOf('ro.build')===0 || this.name=== 'ro.secure'
|| this.name==='ro.debuggable' || this.name.indexOf('ro.kernel')===0
|| this.name.indexOf('gsm.')===0 || this.name.indexOf('persist.sys')===0
|| this.name.indexOf('qemu')===0;
if(isDevice){ send({type:'prop',dt:Date.now()-START,name:this.name,val:val.slice(0,200)}); }
}catch(e){}
}});
}
send({type:'prop_hooked'});
}catch(e){send({type:'err',e:String(e)});}
// 2) SSL_write dfpReport wire
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=rcs(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0){
send({type:'dfp',dt:Date.now()-START,len:len,hex:hexb(a[1],len)});
}
}});
});
send({type:'dfp_hooked'});
}catch(e){send({type:'dfperr',e:String(e)});}
// 3) also capture plaintext JSON candidate: hook memcpy/strlen leaving std::string? skip, noisy.
"""
def one_run(d, a, result, dump_props):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1.2)
pid=d.spawn([PACKAGE])
print(f"[att{a}] spawn pid={pid}", flush=True)
s=d.attach(pid)
try:
b=s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text()); b.load()
except Exception as e:
print(f"[att{a}] bypass err {e}", flush=True)
d.resume(pid)
print(f"[att{a}] resumed", flush=True)
got=False; dfp_hook=False
def on(m,dd):
nonlocal got, dfp_hook
if m.get('type')=='error': print(f"[att{a}] JSErr {str(m)[:120]}", flush=True); return
p=m.get('payload') or {}
t=p.get('type')
if t=='prop_hooked': print(f"[att{a}] prop hooked", flush=True)
elif t=='dfp_hooked': dfp_hook=True; print(f"[att{a}] SSL_write hooked", flush=True)
elif t=='prop':
result.setdefault('props',[]).append({'dt':p.get('dt'),'name':p['name'],'val':p['val']})
elif t=='dfp':
got=True; print(f"[att{a}] dfp len={p['len']}", flush=True)
result['dfp_wire']=p['hex']; result['dfp_dt']=p.get('dt'); result['pid']=pid
sc=s.create_script(JS); sc.on('message',on); sc.load()
t0=time.time()
while time.time()-t0<20:
time.sleep(2)
if got: time.sleep(3); break
print(f"[att{a}] end got_dfp={got} (props={len(result.get('props',[]))})", flush=True)
# unique props by name
seen={}
for pr in result.get('props',[]):
seen.setdefault(pr['name'],[]).append(pr['val'])
if dump_props and seen:
print("--- unique props read ---")
for k,v in sorted(seen.items()):
print(f" {k} = {v[0][:120]} (x{len(v)})", flush=True)
try: d.kill(pid)
except: pass
return got
def main():
d=frida.get_device_manager().add_remote_device(REMOTE)
result={}
ok=False
for a in range(1,6):
try:
if one_run(d,a,result,dump_props=(a>=1)):
print(f"[*] SUCCESS att{a}", flush=True)
ok=True; break
except Exception as e:
print(f"[att{a}] ERR {repr(e)}", flush=True); time.sleep(2)
if ok or result.get('props'):
json.dump(result, open(OUT,'w'), indent=2)
print(f"[*] saved {OUT}", flush=True)
else:
print("[*] no capture", flush=True)
if __name__=="__main__": main()
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""精确定位三元组派生输入: hook __system_property_find + __system_property_read_callback,
只记录 name(不读值避免乱码), 并 hook Build 类 Java 侧读取.
目标: 找出 hdid/deviceId/appkey 由哪些系统属性派生.
"""
from pathlib import Path
import frida, time, subprocess, json
REMOTE="127.0.0.1:31878"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT=Path("/Users/yml/codes/douyu_login_py/evidence/propnames_" + time.strftime("%H%M%S") + ".json")
JS=r"""
'use strict';
var names=new Set(); var dfpGot=false;
function rcs(p,n){try{return p.readCString(n)||'';}catch(e){return '';}}
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
// 1) NDK property接口
try{
var pf=Module.findExportByName('libc.so','__system_property_find');
if(pf) Interceptor.attach(pf,{onEnter:function(a){var n=rcs(a[0],256); if(n&&!names.has(n)){names.add(n);send({type:'find',name:n});}}
});
}catch(e){send({type:'e',e:String(e)});}
try{
var pr=Module.findExportByName('libc.so','__system_property_read_callback');
if(pr) Interceptor.attach(pr,{onEnter:function(a){var n=rcs(a[0],256); if(n&&!names.has(n)){names.add(n);send({type:'readcb',name:n});}}
});
}catch(e){send({type:'e',e:String(e)});}
// 2) Java Build 字段访问 (System.getProperty / Build.MODEL等)
try{
Java.perform(function(){
var Sys=Java.use('java.lang.System');
Sys.getProperty.overload('java.lang.String').implementation=function(k){var v=this.getProperty(k); send({type:'sysprop',name:String(k),val:String(v)}); return v;};
var B=Java.use('android.os.Build');
var FINGERPRINT=B.FINGERPRINT.value; var MODEL=B.MODEL.value; var SERIAL=B.SERIAL.value; var MAN=B.MANUFACTURER.value;
send({type:'build',MODEL:String(MODEL),FINGERPRINT:String(FINGERPRINT),SERIAL:String(SERIAL),MAN:String(MAN)});
});
}catch(e){send({type:'jerr',e:String(e)});}
// 3) SSL_write dfp
try{
new ApiResolver('module').enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){var len=a[2].toInt32(); if(len<50||len>50000)return;
var h=rcs(a[1],Math.min(len,1500)); if(h.indexOf('dfpReport')>=0){dfpGot=true;send({type:'dfp',len:len,hex:hexb(a[1],len)});}}});
});
}catch(e){send({type:'e',e:String(e)});}
"""
def main():
d=frida.get_device_manager().add_remote_device(REMOTE)
result={}
for a in range(1,6):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True); time.sleep(1)
pid=d.spawn([PACKAGE]); s=d.attach(pid)
try:
b=s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text()); b.load()
except: pass
d.resume(pid)
found=[]; build=None; dfp=None
def on(m,dd):
nonlocal build, dfp
if m.get('type')=='error': print(f" JSErr {str(m)[:120]}",flush=True); return
p=m.get('payload') or {}; t=p.get('type')
if t=='find':
# 只记设备/系统相关
n=p['name']
if n.startswith(('ro.','persist.','gsm.','init.','sys.')) : found.append(n)
elif t=='sysprop': pass
elif t=='build': build=(p.get('MODEL'),p.get('FINGERPRINT'),p.get('SERIAL'),p.get('MAN')); print(f"[att{a}] Build:",p,flush=True)
elif t=='dfp': dfp=p['hex']; print(f"[att{a}] dfp len={p.get('len')}",flush=True)
sc=s.create_script(JS); sc.on('message',on); sc.load()
t0=time.time()
while time.time()-t0<20:
time.sleep(2)
if dfp: time.sleep(3); break
print(f"[att{a}] 属性共 {len(found)} 个; dfp={'got' if dfp else 'no'}",flush=True)
if found:
result['props']=sorted(set(found))
if dfp: result['dfp_wire']=dfp
try: d.kill(pid)
except: pass
if dfp: break
time.sleep(1)
json.dump(result,open(OUT,'w'),indent=2)
print("saved",OUT,flush=True)
if result.get('props'):
print("=== 设备/系统属性读取清单 ===")
for n in result['props']: print(" ",n,flush=True)
if __name__=="__main__": main()
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""抓帧自动验证: spawn抓dfpReport → 重放 → 输出actionV(hdid).
用于对比"改IMEI/设备标识后身份是否变化"
"""
from pathlib import Path
import frida, time, subprocess, json, re, ssl, socket
REMOTE="127.0.0.1:31878"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT=Path("/Users/yml/codes/douyu_login_py/evidence/identity_" + time.strftime("%H%M%S") + ".json")
MAIN_JS="""
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
var got=false;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0){got=true;send({type:'dfp',len:len,hex:hexb(a[1],len)});}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
"""
def main():
d=frida.get_device_manager().add_remote_device(REMOTE)
result={}
for attempt in range(1,7):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1.2)
try:
pid=d.spawn([PACKAGE]); s=d.attach(pid)
except Exception as e:
print(f"[att{attempt}] spawn/attach err {e}",flush=True); time.sleep(2); continue
try:
b=s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text()); b.load()
except: pass
d.resume(pid)
got=False
def on(m,dd):
nonlocal got
if m.get('type')!='send':return
p=m.get('payload') or {}
if p.get('type')=='dfp':
got=True
result['dfp_wire']=p['hex']; result['pid']=pid
print(f"[att{attempt}] dfpReport len={p['len']}",flush=True)
sc=s.create_script(MAIN_JS); sc.on('message',on); sc.load()
t0=time.time()
while time.time()-t0<18:
time.sleep(2)
if got: time.sleep(3); break
if got and result.get('dfp_wire'):
# 重放拿 actionV
wire=bytes.fromhex(result['dfp_wire'])
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
ss=ctx.wrap_socket(socket.create_connection(("wsapi.huya.com",443),timeout=10),server_hostname="wsapi.huya.com")
ss.sendall(wire); ss.settimeout(25); buf=b""
try:
while True:
c=ss.recv(8192)
if not c: break
buf+=c
if len(buf)>5000: break
except socket.timeout: pass
ss.close()
av=re.search(rb'actionV\(([0-9a-f]{40})',buf)
avd=av.group(1).decode() if av else None
result['actionV']=avd
print(f"[att{attempt}] actionV = {avd}",flush=True)
json.dump(result,open(OUT,'w'),indent=2)
print(f"[*] saved {OUT}",flush=True)
try: d.kill(pid)
except: pass
return
try: d.kill(pid)
except: pass
print("[*] 未抓到 dfpReport",flush=True)
if __name__=="__main__": main()
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""对照测试: attach + bypass 存活(多轮), 附加 main hook 存活(多轮)。
每种配置重复 ROUNDS 次, 记录每次的存活时间与崩溃点, 避免单次误判。
"""
from __future__ import annotations
import frida, time, subprocess, json
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
BYPASSES = ["bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js", "patch_guard_block_termination.js"]
MAIN_JS_HOOK = """
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
// 只 hook SSL_write, 最小主 hook
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',len:len});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
"""
ROUNDS = 3
OBSERVE = 35 # 每轮观察秒数
def launch():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE], capture_output=True)
time.sleep(1.2)
subprocess.run(["adb","-s","127.0.0.1:5555","shell","monkey","-p",PACKAGE,"-c","android.intent.category.LAUNCHER","1"], capture_output=True)
def get_pid(d, timeout=20):
for _ in range(timeout):
r = subprocess.run(["adb","-s","127.0.0.1:5555","shell","pidof",PACKAGE], capture_output=True, text=True)
p = r.stdout.strip()
if p:
return int(p)
time.sleep(1)
return None
def load_bypass(s):
for name in BYPASSES:
try:
sc = s.create_script((RE/"evidence/scripts"/name).read_text()); sc.load(); time.sleep(0.2)
except Exception as e:
print(f" bp {name} err {e}", flush=True)
def observe(d, pid, s, with_hook, tag):
t0 = time.time()
events = []
if with_hook:
sc = s.create_script(MAIN_JS_HOOK)
sc.on('message', lambda m, dd: events.append(m.get('payload')) if m.get('type')=='send' else None)
sc.load()
# 观察
prev = 0
died_at = None
for t in [5, 10, 15, 20, 25, 30, OBSERVE]:
time.sleep(t - prev); prev = t
try:
alive = [p for p in d.enumerate_processes() if p.pid == pid]
except Exception:
broken = True
died_at = t
break
if not alive:
died_at = t
break
status = f"DEAD at +{died_at}s" if died_at else f"alive>{OBSERVE}s"
dfp = [e for e in events if isinstance(e, dict) and e.get('cls')=='dfpReport']
print(f" [{tag}] {status} | dfp hooks={len(dfp)}", flush=True)
return status, len(dfp)
def run_round(d, with_hook):
launch()
pid = get_pid(d)
if not pid:
return "launch-fail", 0
try:
s = d.attach(pid)
except Exception as e:
return f"attach-err {e}", 0
load_bypass(s)
return observe(d, pid, s, with_hook, "hook" if with_hook else "bypass-only")
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
print("=== A: 纯 attach + bypass (无主hook), 重复 %d 轮 ===" % ROUNDS, flush=True)
for i in range(1, ROUNDS+1):
st, n = run_round(d, with_hook=False)
print(f" A#{i}: {st}", flush=True)
time.sleep(2)
print("=== B: attach + bypass + SSL_write主hook, 重复 %d 轮 ===" % ROUNDS, flush=True)
for i in range(1, ROUNDS+1):
st, n = run_round(d, with_hook=True)
print(f" B#{i}: {st} (dfp={n})", flush=True)
if __name__ == "__main__":
main()
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""dfpReport 生成流程 python 验证模型。
目标: 用手头真实 wire 验证对格式的理解:
wire = HTTP POST / (wsapi.huya.com, okhttp/3.14.9)
+ body(taf/wup): 长度头 + servant + "dfpReport" + "tReq"
+ MAGIC(571882cf664bb39401ee) + cw
cw = [明文JSON ^ keystream][collection ^ keystream]
其中 keystream 是每帧独立随机流(未知生成器)。本脚本验证"格式+布局"理解,
并验证给定完整明文(JSON+collection)与keystream可精确重建真实wire。
"""
from __future__ import annotations
import json, binascii, struct, re
MAGIC = bytes.fromhex('571882cf664bb39401ee')
PREFIX_JSON = b'{"appId":"5008","appVer":"13.4.22"'
# 真机已验证的完整明文 JSON(含三元组)
REAL_JSON = ('{"appId":"5008","appVer":"13.4.22","appkey":"865a4924a40897ac1fcfe6b4c2cbb0e3",'
'"channel":"xiaomi","deviceId":"02df398797432eadefcc12767119ad5e80999389",'
'"deviceName":"M2102J2SC","hdid":"7c5387e0539c023c31c4ff0e807e7256117385ee",'
'"heightPixels":"2120","isCloud":0,"isForbidLog":1,"isHome":0,"isPre":0,'
'"openAppId":"","savePath":"/data/user/0/com.duowan.kiwi/files/huyaudb",'
'"sdkVer":"1.0.80138","servantName":"huyaudbwebui",'
'"shareAppDataPath":"/data/user/0/com.duowan.kiwi/files/huyaudb",'
'"systemInfo":"android","systemVer":"M2102J2SC,30,11",'
'"terminalType":1,"testEnv":0,"widthPixels":"1080"}')
def build_json(triple=None, device_name='M2102J2SC', system_ver='M2102J2SC,30,11'):
t = triple or dict(appkey='865a4924a40897ac1fcfe6b4c2cbb0e3',
channel='xiaomi',
deviceId='02df398797432eadefcc12767119ad5e80999389',
hdid='7c5387e0539c023c31c4ff0e807e7256117385ee')
return ('{"appId":"5008","appVer":"13.4.22","appkey":"%(appkey)s","channel":"%(channel)s",'
'"deviceId":"%(deviceId)s","deviceName":"'+device_name+'","hdid":"%(hdid)s",'
'"heightPixels":"2120","isCloud":0,"isForbidLog":1,"isHome":0,"isPre":0,'
'"openAppId":"","savePath":"/data/user/0/com.duowan.kiwi/files/huyaudb",'
'"sdkVer":"1.0.80138","servantName":"huyaudbwebui",'
'"shareAppDataPath":"/data/user/0/com.duowan.kiwi/files/huyaudb",'
'"systemInfo":"android","systemVer":"'+system_ver+'",'
'"terminalType":1,"testEnv":0,"widthPixels":"1080"}') % t
def parse_wire(wire):
"""从真实 wire 提取结构化字段. 返回 dict."""
body_start = wire.find(b'\r\n\r\n') + 4
body = wire[body_start:]
# taf 头: 4字节大端长度, 然后 TLV
# 简化解析: servant名
mi = body.find(MAGIC)
cw = body[mi+10:]
# JSON 密文段长度 + 明文校验
result = {
'body': body,
'cw': cw,
'magic_offset_in_body': mi,
'json_len': None, 'coll': None,
}
# 用已知JSON前缀恢复ks并反推JSON长度定位
if len(cw) >= len(PREFIX_JSON):
# JSON数据是明文XOR (明文可打印). 但我们不知道JSON结束位置(collection开始).
# 用"JSON固定长度"真机值(586)推断: 若明文JSON=XOR后全是可打印ascii且结构符合.
jlen = len(REAL_JSON.encode())
coll = cw[jlen:]
result['json_len'] = jlen
result['json_offset_in_cw'] = 0
result['coll'] = coll
return result
def verify_rebuild(wire):
"""验证: 用解析出的 cw, 从"明文JSON+keystream"能否精确重建 cw[:json_len]."""
p = parse_wire(wire)
cw = p['cw']
jb = REAL_JSON.encode()
jlen = len(jb)
# keystream = cw[:jlen] ^ jb
ks_json = bytes(cw[i]^jb[i] for i in range(jlen))
# 重建 encoded_json = jb ^ ks_json = jb (恒等, 因为ks由它们导出)
# 真正验证: 检查明文JSON在 cw 中位置对应的XOR是否=keystream连续(即JSON在此) — 无法独立验证,
# 但可验证: 明文JSON偏移与 ta f 头一致.
# 关键验证: keystream 在 JSON 段与 collection 段是否"同一生成器连续流".
# 我们对比 JSON 段 ks 和 collection 段(from某plaintext假设). 目前无collection明文故无法.
# 输出 keystream JSON段, 供分析.
return {
'jlen': jlen, 'cw_len': len(cw),
'ks_head': ks_json[:48].hex(),
'coll_len': len(p['coll']),
'ks_json_randomness': len(set(ks_json)),
}
def main():
frames = json.load(open('evidence/frame_real.json'))
wire = binascii.unhexlify(frames[0])
p = parse_wire(wire)
print("wire 总长: %d, body 长: %d, cw 长: %d" % (len(wire), len(p['body']), len(p['cw'])))
print("MAGIC 在 body 偏移: %d" % p['magic_offset_in_body'])
print("JSON 密文长度(586), collection 长度: %d" % len(p['coll']))
v = verify_rebuild(wire)
print("JSON keystream 前48B:", v['ks_head'])
print("keystream JSON段随机性(不同字节):", v['ks_json_randomness'], "/ 586")
# 打印 taf 头可读字符串
b = p['body']
for kw in [b'huyaudbwebui', b'dfpReport', b'android', b'tReq']:
print("body 含 '%s': @%d" % (kw.decode(errors='replace'), b.find(kw)))
if __name__ == '__main__':
main()
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""挂起策略梯度实验: 确认 attach+maps遮蔽 是否能绕过 msaoaidsec 且不EGL崩.
E0: spawn→attach(空)→resume = 复现 session gone(静默死)
E1: spawn→attach→load mask脚本→resume = attach+maps遮蔽
E2: spawn→attach→load mask+sslhook→resume = 最终抓包形态
E3: 纯spawn→resume(不attach) = B组对照(应活)
每个 case 跑 2 轮, 每轮观察 30s 或死亡, logcat crash 同步。
"""
from pathlib import Path
import frida, time, json, subprocess, sys, re
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT_DIR = Path("/Users/yml/codes/douyu_login_py/evidence/diag_egl_strategy")
SSL_HOOK = r"""
'use strict';
send({type:'armed'});
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){send({type:'ssl',len:a[2].toInt32(),t:Date.now()});}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
"""
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def detach_cb(reason, detail):
print(f" [detached] reason={reason} detail={detail}", flush=True)
def run_case(d, case, rnd):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.2)
adb("shell", "logcat", "-c")
pid = None
session = None
t_spawn = time.time()
try:
if case in ("E4", "E5"):
# B组: 纯spawn→立即resume→App起来后再attach
pid = d.spawn([PACKAGE])
d.resume(pid)
print(f"[{case}.r{rnd}] spawn pid={pid} 立即resumed", flush=True)
time.sleep(1.5 if case == "E4" else 5.0)
t_att = time.time()
session = d.attach(pid)
session.on('detached', detach_cb)
print(f"[{case}.r{rnd}] attach at +{(time.time()-t_spawn)*1000:.0f}ms (attach耗时{(time.time()-t_att)*1000:.0f}ms)", flush=True)
suspend_ms = (time.time() - t_spawn) * 1000
elif case in ("E6", "E7"):
# 极短挂起: 只加载最少 bypass
pid = d.spawn([PACKAGE])
suspend_start = time.time()
session = d.attach(pid)
session.on('detached', detach_cb)
if case == "E6":
sc = session.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()); sc.load()
else:
sc = session.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()); sc.load()
sc2 = session.create_script((RE/"evidence/scripts/mask_frida_maps_only.js").read_text()); sc2.load()
suspend_ms = (time.time() - suspend_start) * 1000
d.resume(pid)
print(f"[{case}.r{rnd}] spawn pid={pid} 挂起{suspend_ms:.0f}ms(1个art_callsite补丁) resumed", flush=True)
else:
pid = d.spawn([PACKAGE])
suspend_start = time.time()
if case in ("E1", "E2"):
session = d.attach(pid)
session.on('detached', detach_cb)
if case == "E1":
sc = session.create_script((RE/"evidence/scripts/mask_frida_maps_only.js").read_text()); sc.load()
else:
sc = session.create_script((RE/"evidence/scripts/mask_frida_maps_only.js").read_text()); sc.load()
sc2 = session.create_script(SSL_HOOK); sc2.load()
elif case == "E0":
session = d.attach(pid)
session.on('detached', detach_cb)
elif case == "E3":
pass # 纯spawn不attach
suspend_ms = (time.time() - suspend_start) * 1000
d.resume(pid)
print(f"[{case}.r{rnd}] spawn pid={pid} 挂起{suspend_ms:.0f}ms resumed", flush=True)
except Exception as e:
print(f"[{case}.r{rnd}] setup ERR {e}", flush=True)
return None
# 观察 30s
t0 = time.time()
died_at = None
pid_changed = None
while time.time() - t0 < 30:
time.sleep(2)
mp = main_pid()
if mp is None:
died_at = time.time() - t0
break
if mp != pid:
pid_changed = (mp, time.time() - t0)
break
cr = adb("shell", "logcat", "-d", "-b", "crash", "-t", "20").stdout
egl = "EGL" in cr or "libEGL" in cr
crash_banner = bool(re.search(r"Fatal|signal \d|Abort", cr))
try:
if session: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1)
result = {
"case": case, "round": rnd, "pid": pid, "suspend_ms": round(suspend_ms, 1),
"died_at_s": round(died_at, 1) if died_at else None,
"pid_changed": pid_changed, "egl_crash": egl,
"crash_banner": crash_banner,
"crash_tail": cr[:300],
}
print(f"{result}", flush=True)
return result
def main():
cases = sys.argv[1:] or ["E0", "E1", "E2", "E3", "E4", "E5", "E6", "E7"]
OUT_DIR.mkdir(parents=True, exist_ok=True)
d = frida.get_device_manager().add_remote_device(REMOTE)
results = []
for c in cases:
for rnd in (1, 2):
r = run_case(d, c, rnd)
if r: results.append(r)
out = OUT_DIR / "egl_strategy.json"
out.write_text(json.dumps(results, ensure_ascii=False, indent=1))
print(f"[*] {len(results)} 结果 -> {out}", flush=True)
if __name__ == "__main__":
main()
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""存活/闪退四组对照诊断器 (统一度量):
指标1 proc_main: /proc/<pid>/cmdline == 包名 (主进程存活)
指标2 ui_focus: dumpsys window mCurrentFocus 是否 com.duowan.kiwi/* (UI 未闪退)
指标3 crash: logcat -b crash 是否出现 Fatal (native/java 崩溃)
A. 无frida 正常启动 (baseline)
B. spawn 挂起~0s 立即resume, 不加载任何脚本
C. spawn 挂起 加载2个bypass脚本后再resume (模拟v8/v9/stale)
D. attach 正常启动7s后 attach 主进程 (裸attach, 无脚本)
用法: python3 diag_lifecycle.py [a|b|c|d]
"""
from pathlib import Path
import frida, time, subprocess, sys, json, re
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT_DIR = Path("/Users/yml/codes/douyu_login_py/evidence/diag_lifecycle")
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def focus_ok():
r = adb("shell", "dumpsys", "window")
m = re.search(r"mCurrentFocus=Window\{([^}]+)\}", r.stdout)
if m:
f = m.group(1)
return PACKAGE in f, f
return False, "?"
def crash_count():
r = adb("shell", "logcat", "-d", "-b", "crash", "-t", "50")
return r.stdout.count("Fatal"), r.stdout[-2000:]
def snapshot(label):
p = main_pid()
ok, foc = focus_ok()
return {"label": label, "proc": p, "ui_ok": ok, "focus": foc}
def main():
which = (sys.argv[1] if len(sys.argv) > 1 else "a").lower()
OUT_DIR.mkdir(parents=True, exist_ok=True)
adb("shell", "logcat", "-c")
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = None
t_start = time.time()
timeline = []
label = which.upper()
if which in ("a",):
print("[A] 无frida 正常启动", flush=True)
adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1")
elif which in ("b",):
print("[B] spawn 挂起~0 立即 resume", flush=True)
pid = d.spawn([PACKAGE])
dt = time.time() - t_start
t_start = time.time()
d.resume(pid)
print(f" spawn->resume 挂起 {dt*1000:.0f}ms", flush=True)
elif which in ("c",):
print("[C] spawn 挂起+加载2个bypass再resume", flush=True)
pid = d.spawn([PACKAGE])
s = d.attach(pid)
for name in ("bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js"):
t0 = time.time()
sc = s.create_script((RE / "evidence/scripts" / name).read_text())
sc.load()
print(f" loaded {name} +{time.time()-t0:.2f}s", flush=True)
dt = time.time() - t_start
t_start = time.time()
d.resume(pid)
print(f" spawn->resume 挂起 {dt*1000:.0f}ms", flush=True)
elif which in ("d",):
print("[D] 正常启动7s后 attach 主进程(裸)", flush=True)
adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1")
time.sleep(7)
pid = main_pid()
print(f" pid={pid}, 开始attach", flush=True)
try:
s = d.attach(pid)
sc = s.create_script("console.log('bare attach ok');")
sc.load()
print(" bare attach loaded", flush=True)
except Exception as e:
print(f" attach err {e}", flush=True)
t_start = time.time()
else:
print(f"未知: {which}"); return
# 观察 60s, 每 2s 快照
t0 = time.time()
while time.time() - t0 < 25:
time.sleep(2)
s = snapshot(label)
s["elapsed"] = round(time.time() - t0, 1)
timeline.append(s)
if s["ui_ok"]:
print(f" +{s['elapsed']:5.1f}s proc={s['proc']} UI={s['focus']}", flush=True)
else:
print(f" +{s['elapsed']:5.1f}s proc={s['proc']} UI_FAIL focus={s['focus']}", flush=True)
if not s["proc"] and s["elapsed"] > 3:
break
crashes, tail = crash_count()
final = snapshot(label)
result = {"case": label, "timeline": timeline, "final": final,
"crashes_in_buffer": crashes, "crash_tail": tail[:1500]}
out = OUT_DIR / f"diag_{label.lower()}.json"
out.write_text(json.dumps(result, ensure_ascii=False, indent=1))
print(f"[*] 结果 -> {out} | 最终: proc={final['proc']} ui_ok={final['ui_ok']} crashes={crashes}", flush=True)
if __name__ == "__main__":
main()
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""模拟器 attach+三件套 对照实验:
1) 干净启动 App → 自动走到账号密码登录页 → 填表 → 登录 → 观察验证码是否加载(基线)
2) attach frida 三件套(bypass_msaoaid_maps_skip_cleanup + mask_frida_maps_only + patch_guard)
→ 观察存活 → 重新走登录 → 观察验证码是否加载(对照)
用 RE 仓库 .venv 环境与 RE 仓库 evidence/scripts 三件套。"""
from pathlib import Path
import frida, time, subprocess, json, sys
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/tmp/emu_attach_captcha.json")
TRIO = [
(RE / "evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js", "bypass_main"),
(RE / "evidence/scripts/mask_frida_maps_only.js", "mask_frida"),
(RE / "evidence/scripts/patch_guard_block_termination.js", "patch_guard"),
]
def sh(*args, **kw):
return subprocess.run(args, capture_output=True, text=True, **kw)
def adb(*args):
return sh(*ADB, *args)
def force_stop():
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
def launch():
adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1")
time.sleep(6)
def wait_pid(timeout=20):
for _ in range(timeout):
r = adb("shell", "pidof", PACKAGE)
if r.stdout.strip():
return int(r.stdout.strip().split()[0])
time.sleep(1)
return None
def ui_dump():
adb("shell", "uiautomator", "dump", "/data/local/tmp/ui.xml")
r = adb("shell", "cat", "/data/local/tmp/ui.xml")
return r.stdout
def find_bounds(xml, text):
import re
m = re.search(r'<node[^>]*text="' + re.escape(text) + r'"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', xml)
if m:
x1, y1, x2, y2 = map(int, m.groups())
return (x1 + x2) // 2, (y1 + y2) // 2
return None
def tap(x, y):
adb("shell", "input", "tap", str(x), str(y))
def input_text(s):
adb("shell", "input", "text", s)
def go_login_page():
"""从首页走到账号密码登录页. 返回 True 若成功."""
xml = ui_dump()
# 我的
b = find_bounds(xml, "我的")
if not b:
return False
tap(*b); time.sleep(2.5)
xml = ui_dump()
b = find_bounds(xml, "立即登录")
if not b:
return False
tap(*b); time.sleep(3)
xml = ui_dump()
b = find_bounds(xml, "账号密码登录")
if not b:
# 已经是验证码登录页, 尝试切换
print(" [!] 未找到账号密码登录tab", flush=True)
return False
tap(*b); time.sleep(2)
return True
def fill_and_submit():
xml = ui_dump()
b = find_bounds(xml, "手机号/虎牙号")
if not b:
b = find_bounds(xml, "请填写手机号码")
if not b:
return False
tap(*b); time.sleep(0.4); input_text("13800138000")
xml = ui_dump()
b = find_bounds(xml, "密码")
if b:
tap(*b); time.sleep(0.4); input_text("test12345678")
time.sleep(0.6)
xml = ui_dump()
b = find_bounds(xml, "立即登录")
if not b:
return False
tap(*b); time.sleep(2)
# 协议弹窗
xml = ui_dump()
b = find_bounds(xml, "同意并继续")
if b:
tap(*b); time.sleep(4)
return True
def captcha_status():
xml = ui_dump()
nodes = xml.replace("></", ">\n</")
texts = [m for m in nodes.splitlines() if 'text="' in m]
joined = " | ".join(t for t in texts if 'text="' in t)
has_slider = "滑块" in joined or "拼图" in joined or "安全验证" in joined
has_webview = "android.webkit.WebView" in joined
# 统计关键文本
keys = [k for k in ("安全验证", "滑块", "拼图", "验证码", "网络异常", "加载失败", "请稍后") if k in joined]
return {"ok": has_slider and has_webview, "webview": has_webview, "slider": has_slider,
"keys": keys, "texts": [l.strip() for l in nodes.splitlines() if 'text="' in l and l.strip().startswith('<node')][:12]}
def attach_trio(d, pid):
session = d.attach(pid)
print(f"[*] attach pid={pid}", flush=True)
loaded = []
for js_path, name in TRIO:
try:
sc = session.create_script(js_path.read_text())
sc.on('message', lambda m, dd, n=name: print(f" [{n}] {m.get('payload') if m.get('type')=='send' else m}", flush=True))
sc.load()
loaded.append(name)
time.sleep(0.15)
except Exception as e:
print(f" [!] {name} 加载失败: {e}", flush=True)
print(f"[*] 三件套已加载: {loaded}", flush=True)
return session
def main():
results = {"baseline": None, "with_frida": None, "captcha_baseline": None, "captcha_with_frida": None}
d = frida.get_device_manager().add_remote_device(REMOTE)
# ---- 基线: 无 frida 走登录 ----
print("===== 基线: 无 frida =====", flush=True)
force_stop(); launch()
pid = wait_pid()
results["baseline"] = pid
print(f"[*] app pid={pid}", flush=True)
if pid and go_login_page() and fill_and_submit():
time.sleep(5)
results["captcha_baseline"] = captcha_status()
print(f"[*] 基线验证码: {json.dumps(results['captcha_baseline'], ensure_ascii=False)}", flush=True)
# ---- 对照: attach 三件套 ----
print("===== 对照: attach + 三件套 =====", flush=True)
pid = wait_pid()
if not pid:
print("[!] app 未运行", flush=True)
return
session = attach_trio(d, pid)
results["with_frida"] = pid
# 观察存活
alive = True
t0 = time.time()
while time.time() - t0 < 40:
time.sleep(2)
try:
cur = [p for p in d.enumerate_processes() if p.pid == pid]
except Exception:
cur = []
if not cur:
alive = False
print(f"[!] attach 后 App 死亡于 +{time.time()-t0:.0f}s", flush=True)
session = None
break
results["frida_survival_s"] = int(time.time() - t0) if alive else int(time.time() - t0)
print(f"[*] attach 后存活: {'YES' if alive else 'NO'} ({results['frida_survival_s']}s)", flush=True)
if alive:
# 回到登录页重新走
if go_login_page() and fill_and_submit():
time.sleep(5)
results["captcha_with_frida"] = captcha_status()
print(f"[*] frida 下验证码: {json.dumps(results['captcha_with_frida'], ensure_ascii=False)}", flush=True)
else:
results["captcha_with_frida"] = "app died"
OUT.write_text(json.dumps(results, ensure_ascii=False, indent=1))
print(f"[*] 结果写入 {OUT}", flush=True)
try:
if session: session.detach()
except Exception:
pass
if __name__ == "__main__":
main()
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""完整链路验证: 正常启动App → attach + SSL_write hook → 存活观察 + 抓dfpReport.
基于最新发现: attach 不再必然被杀 (E0-E7 状态依赖)。
流程:
1. monkey 正常启动 App (无 frida)
2. 等 8s (App 完全起来)
3. attach 主进程 + SSL_write/SSL_read hook (零 bypass)
4. 用 /proc 精确监控主进程存活; logcat crash 同步
5. 观察到 dfpReport + actionV 即停
用法: python3 emu_attach_capture_v2.py [--waits N] [--observe S]
"""
from pathlib import Path
import frida, time, json, re, subprocess, sys, argparse
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
MAIN_JS = r"""
'use strict';
send({type:'armed', t:Date.now()});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
var n=0;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++; send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,t:Date.now(),hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked', t:Date.now()});
}catch(e){send({type:'err',e:String(e)});}
try{
var r2=new ApiResolver('module');
r2.enumerateMatchesSync('exports:*!SSL_read').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){this.buf=a[1];},
onLeave:function(ret){
var nn=ret.toInt32();
if(nn<=0||nn>4000)return;
send({type:'resp',len:nn,hex:hexb(this.buf,nn),t:Date.now()});
}});
});
send({type:'readhooked'});
}catch(e){send({type:'readerr',e:String(e)});}
"""
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--wait", type=float, default=8.0)
ap.add_argument("--observe", type=int, default=40)
ap.add_argument("--out", default="/Users/yml/codes/douyu_login_py/evidence/emu_attach_capture_v2.json")
args = ap.parse_args()
d = frida.get_device_manager().add_remote_device(REMOTE)
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
adb("shell", "logcat", "-c")
adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1")
time.sleep(args.wait)
pid = main_pid()
print(f"[*] App 启动 {args.wait}s, 主进程 pid={pid}", flush=True)
if pid is None:
print("[!] 无主进程, 退出"); return
t0 = time.time()
try:
session = d.attach(pid)
print(f"[*] attach ok at +{(time.time()-t0)*1000:.0f}ms", flush=True)
except Exception as e:
print(f"[!] attach err {e}", flush=True); return
events = []
got_dfp = [False]
actionVs = []
def on_main(m, dta):
if m.get('type') == 'error':
print(" [JS-ERR]", str(m)[:120], flush=True)
return
p = m.get('payload') or {}
t = p.get('type')
if t == 'hooked':
print("[*] SSL_write hooked", flush=True)
elif 'cls' in p:
print(f"[*] {p['cls']} len={p['len']}", flush=True)
events.append(p)
if p['cls'] == 'dfpReport': got_dfp[0] = True
elif t == 'resp':
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p.get('hex') or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
if av: actionVs.append(av.group(1).decode())
if av: print(f"[*] RESP actionV {av.group(1).decode()}", flush=True)
events.append(p)
def on_detach(reason, detail):
print(f"[*] DETACHED reason={reason} detail={str(detail)[:100]}", flush=True)
session.on('detached', on_detach)
try:
sc = session.create_script(MAIN_JS)
sc.on('message', on_main)
sc.load()
except Exception as e:
print(f"[!] script err {e}", flush=True)
# 监控: 每2s 主进程存活
died_at = None
while time.time() - t0 < args.observe:
time.sleep(2)
mp = main_pid()
if mp is None:
died_at = time.time() - t0
print(f"[*] 主进程死 at +{died_at:.0f}s", flush=True)
break
if mp != pid:
print(f"[*] pid变化 {pid}->{mp} (KeepAlive?) at +{time.time()-t0:.0f}s", flush=True)
died_at = time.time() - t0
break
cr = adb("shell", "logcat", "-d", "-b", "crash", "-t", "10").stdout
print(f"[*] 结果: 存活={died_at is None} 存活时长={died_at or args.observe}s EGL崩={'EGL' in cr} 事件={len(events)} actionV={actionVs[:2]}", flush=True)
Path(args.out).write_text(json.dumps(events, ensure_ascii=False, indent=1))
try: session.detach()
except Exception: pass
print(f"[*] done -> {args.out}", flush=True)
if __name__ == "__main__":
main()
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""复现实验: attach + 只读 Interceptor 是否让 msaoaidsec 检测失效 (App 存活).
假设: 空 attach (E0) 2s 静默死; attach+trace的只读 Interceptor 装到 msaoaidsec 内
(+0x1bfac 线程名检测器入口 / +0x1c0e4,0x1c0f8 strstr callsite) 后 App 存活.
实验设计 (每轮):
1. spawn → attach → 加载 trace_msaoaid_thread_strstr_plt.js (只读, 不patch不mask)
2. resume → 用 /proc 精确监控主进程 20s
3. 记录: 存活? 死亡时刻? 是否EGL崩? 收到的strstr事件?
控制组: 相同流程但不加载任何脚本 (= E0 复现)
"""
from pathlib import Path
import frida, time, json, subprocess, sys, re
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def run_round(d, rnd, script_name=None, observe=20):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.2)
adb("shell", "logcat", "-c")
pid = d.spawn([PACKAGE])
session = d.attach(pid)
t0 = time.time()
events = []
def on(m, dta):
if m.get('type') != 'send': return
p = m.get('payload') or {}
events.append(p)
ev = p.get('event', '?')
if ev in ('msaoaid-thread-strstr',):
print(f"[r{rnd} +{time.time()-t0:5.2f}s] STRSTR needle={p.get('needle')} haystack={p.get('haystack','')[:30]!r}", flush=True)
elif ev == 'msaoaid-thread-strstr-return':
print(f"[r{rnd} +{time.time()-t0:5.2f}s] STRSTR-RET needle={p.get('needle')} hit={p.get('returnedNonNull')}", flush=True)
if script_name:
sc = session.create_script((RE / "evidence/scripts" / script_name).read_text())
sc.on('message', on)
sc.load()
d.resume(pid)
print(f"[r{rnd}] resume at +{(time.time()-t0)*1000:.0f}ms script={script_name}", flush=True)
# 监控
died_at = None
while time.time() - t0 < observe:
time.sleep(2)
if main_pid() is None:
died_at = time.time() - t0
break
cr = adb("shell", "logcat", "-d", "-b", "crash", "-t", "8").stdout
status = "存活" if died_at is None else f"死@+{died_at:.1f}s"
egl = "EGL" in cr or "libEGL" in cr
print(f"[r{rnd}] 结果: {status} EGL崩={egl} events={len(events)}", flush=True)
try: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1)
return {"round": rnd, "script": script_name, "died_at": died_at, "egl": egl, "n_events": len(events)}
def main():
scripts = sys.argv[1:] or ["None", "trace_msaoaid_thread_strstr_plt.js"]
d = frida.get_device_manager().add_remote_device(REMOTE)
results = []
for script in scripts:
for rnd in (1, 2):
r = run_round(d, rnd, None if script == "None" else script)
results.append(r)
print(f"{r}", flush=True)
out = Path("/tmp/attach_trace_survival.json")
out.write_text(json.dumps(results, indent=1, ensure_ascii=False))
print(f"[*] -> {out}", flush=True)
if __name__ == "__main__":
main()
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""模拟器抓包 v11: B组优先三策略 (基于 diag E0-E7 实验结论).
结论回顾 (GC3VE/Android12/frida15.2.2):
- 纯 spawn→resume (不attach) = 存活, 但无 hook 能力 (B组/E3, 多次3/3存活)
- 任何 attach (挂起中/延迟, 有无脚本) = 被杀: 空attach静默_exit / attach+bypass=EGL崩
- 历史上 spawn+挂起attach+bypass 曾抓到 dfpReport+actionV, 但 App 随后必崩 (抢窗口)
本脚本提供三种模式:
zero (默认) 纯B组: spawn→立即resume→存活观察 (无hook, 验证App真能活/验证码可用)
race B组+延迟attach: resume后2s attach+bypass+SSLhook, 抢dfp窗口 (接受闪退)
stale 旧法对照: 挂起中加载 art_callsite+mask 再resume (复现历史成功路径)
用法: python3 emu_hook_zero_suspend.py [--mode zero|race|stale] [--rounds N] [--out path]
"""
from pathlib import Path
import frida, time, json, re, subprocess, sys, argparse
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
MAIN_JS = r"""
'use strict';
send({type:'armed', t:Date.now()});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
var n=0;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++; send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,t:Date.now(),hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked', t:Date.now()});
}catch(e){send({type:'err',e:String(e)});}
try{
var r2=new ApiResolver('module');
r2.enumerateMatchesSync('exports:*!SSL_read').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){this.buf=a[1];},
onLeave:function(ret){
var nn=ret.toInt32();
if(nn<=0||nn>4000)return;
send({type:'resp',len:nn,hex:hexb(this.buf,nn),t:Date.now()});
}});
});
send({type:'readhooked'});
}catch(e){send({type:'readerr',e:String(e)});}
"""
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def detach_cb(reason, detail, tag=""):
print(f" [detached:{tag}] reason={reason} detail={str(detail)[:120]}", flush=True)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--mode", default="zero", choices=["zero", "race", "stale"])
ap.add_argument("--out", default="/Users/yml/codes/douyu_login_py/evidence/emu_hook_v11.json")
ap.add_argument("--rounds", type=int, default=2)
args = ap.parse_args()
out_path = Path(args.out)
d = frida.get_device_manager().add_remote_device(REMOTE)
print(f"[*] mode={args.mode} rounds={args.rounds} OUT={out_path}", flush=True)
all_events = []
for rnd in range(1, args.rounds + 1):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.2)
adb("shell", "logcat", "-c")
pid = None
session = None
try:
pid = d.spawn([PACKAGE])
t_spawn = time.time()
if args.mode == "stale":
# 旧法: 挂起中加载2个脚本再resume
session = d.attach(pid)
session.on('detached', lambda r, dd, t=f"[r{rnd}]": detach_cb(r, dd, t))
for name in ("bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js"):
sc = session.create_script((RE/"evidence/scripts"/name).read_text()); sc.load()
suspend_ms = (time.time() - t_spawn) * 1000
d.resume(pid)
print(f"[r{rnd}] stale: spawn pid={pid} 挂起{suspend_ms:.0f}ms(2脚本) resumed", flush=True)
time.sleep(0.5)
else:
# zero/race: 先立即 resume (B组存活)
d.resume(pid)
print(f"[r{rnd}] spawn pid={pid} 立即resumed (B组存活)", flush=True)
if args.mode == "race":
time.sleep(2.0)
t_att = time.time()
session = d.attach(pid)
session.on('detached', lambda r, dd, t=f"[r{rnd}]": detach_cb(r, dd, t))
for name in ("bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js"):
sc = session.create_script((RE/"evidence/scripts"/name).read_text()); sc.load()
print(f"[r{rnd}] race: attach at +{(time.time()-t_spawn)*1000:.0f}ms +2脚本 +{(time.time()-t_att)*1000:.0f}ms", flush=True)
events = []
got_dfp = [False]
actionVs = []
def on_main(m, dta):
if m.get('type') == 'error':
return
p = m.get('payload') or {}
t = p.get('type')
if t == 'hooked':
print(f"[r{rnd}] SSL_write hooked", flush=True)
elif 'cls' in p:
print(f"[r{rnd}] {p['cls']} len={p['len']}", flush=True)
events.append(p); all_events.append(p)
if p['cls'] == 'dfpReport': got_dfp[0] = True
elif t == 'resp':
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p.get('hex') or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
if av: actionVs.append(av.group(1).decode())
print(f"[r{rnd}] RESP len={p['len']}{mark}", flush=True)
events.append(p); all_events.append(p)
if session:
sc = session.create_script(MAIN_JS)
sc.on('message', on_main)
sc.load()
# 存活监控 (死因判定: crash buffer 有记录=EGL崩, 无=静默_exit)
t0 = time.time()
died_at = None
while time.time() - t0 < 30:
time.sleep(2)
mp = main_pid()
if mp is None:
died_at = time.time() - t0
break
if mp != pid:
print(f"[r{rnd}] pid变化 {pid}->{mp} (KeepAlive重启)", flush=True)
died_at = time.time() - t0
break
if got_dfp[0] and time.time() - t0 > 15:
break
cr = adb("shell", "logcat", "-d", "-b", "crash", "-t", "20").stdout
crashed = bool(re.search(r"Fatal|Abort message|signal \d", cr))
print(f"[r{rnd}] 结束: 存活={died_at is None} 死后至+{round(died_at,1) if died_at else '-'} 崩={crashed} 事件={len(events)} actionV={actionVs[:2]}", flush=True)
try:
if session: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
except Exception as e:
print(f"[r{rnd}] ERR {repr(e)[:140]}", flush=True)
try:
if session: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1.5)
out_path.write_text(json.dumps(all_events, ensure_ascii=False, indent=1))
print(f"[*] 结束 事件={len(all_events)} -> {out_path}", flush=True)
if __name__ == "__main__":
main()
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""msaoaidsec 检测面精确归因 v2 (修正误判版).
上次 attach_trace_survival 用 main_pid() is None 判断死亡 —— 错!
KeepAlive 重启会换 pid, main_pid() 返回新 pid, 误报"存活"
本版: 死亡判定 = pid 变化 或 无主进程 (均算死), 并采集:
1. trace_msaoaid_thread_strstr_plt.js — 线程名检测器 strstr 命中 (needle=?)
2. trace_msaoaid_exit_sources.js (API兼容修复) — _exit PLT slot 观察, backtrace 帧
每组 spawn+attach (挂起中加载脚本, 立即resume), 观察 12s。
"""
from pathlib import Path
import frida, time, json, subprocess, sys
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
# frida 15 兼容修复: getGlobalExportByName -> findExportByName
def fix_script(text):
return text.replace("Module.getGlobalExportByName(", "Module.findExportByName(null, ")
def run_case(d, rnd, scripts):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.2)
adb("shell", "logcat", "-c")
pid = d.spawn([PACKAGE])
session = d.attach(pid)
t0 = time.time()
events = []
def on(m, dta):
if m.get('type') != 'send':
if m.get('type') == 'error':
print(f" [JS-ERR] {str(m)[:100]}", flush=True)
return
p = m.get('payload') or {}
ev = p.get('event', '?')
line = f"[r{rnd} +{time.time()-t0:6.2f}s] {ev}"
if ev == 'msaoaid-thread-strstr':
line += f" needle={p.get('needle')} hay={p.get('haystack','')[:30]!r}"
elif ev == 'msaoaid-thread-strstr-return':
line += f" needle={p.get('needle')} hit={p.get('returnedNonNull')} ret={p.get('returnPointer')}"
elif ev == 'msaoaid-exit-source':
line += f" status={p.get('status')} tid={p.get('tid')} frames={p.get('frames')}"
elif ev in ('msaoaid-exit-got-thread-exit', 'msaoaid-exit-got-thread-exit-installed'):
line += f" data={p.get('status') or p.get('slot')}"
print(line, flush=True)
events.append(p)
for name in scripts:
try:
text = (RE / "evidence/scripts" / name).read_text()
sc = session.create_script(fix_script(text))
sc.on('message', on)
sc.load()
print(f"[r{rnd}] loaded {name} +{(time.time()-t0)*1000:.0f}ms", flush=True)
except Exception as e:
print(f"[r{rnd}] load-err {name}: {e}", flush=True)
d.resume(pid)
print(f"[r{rnd}] resumed +{(time.time()-t0)*1000:.0f}ms", flush=True)
# 精确死亡判定: pid变化或无主进程
died = None
change = None
while time.time() - t0 < 12:
time.sleep(1.5)
mp = main_pid()
if mp is None:
died = time.time() - t0
print(f"[r{rnd}] DEAD(no main) at +{died:.1f}s", flush=True)
break
if mp != pid:
change = (mp, round(time.time() - t0, 1))
print(f"[r{rnd}] PID CHANGED {pid}->{mp} at +{change[1]}s (主进程死/重启)", flush=True)
break
if died is None and change is None:
print(f"[r{rnd}] 存活满12s (pid={pid} 未变)", flush=True)
cr = adb("shell", "logcat", "-d", "-b", "crash", "-t", "6").stdout
egl = "EGL" in cr
print(f"[r{rnd}] EGL崩={egl}", flush=True)
try: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1)
return {"round": rnd, "scripts": scripts, "died_at": died, "pid_change": change, "egl": egl, "events": events}
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
# 组1: 只读线程名探针 (不patch不mask)
r1 = run_case(d, 1, ["trace_msaoaid_thread_strstr_plt.js"])
# 组2: 只读线程名探针 + exit源观察
r2 = run_case(d, 2, ["trace_msaoaid_thread_strstr_plt.js", "trace_msaoaid_exit_sources.js"])
# 组3: 只读 exit源观察 (无线程名探针, 减少干扰)
r3 = run_case(d, 3, ["trace_msaoaid_exit_sources.js"])
out = Path("/tmp/msaoaid_precise.json")
out.write_text(json.dumps([r1, r2, r3], ensure_ascii=False, indent=1))
print(f"[*] -> {out}", flush=True)
if __name__ == "__main__":
main()
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""frida 存活期间验证码加载对照实验 (模拟器, spawn+bypass 三件套).
复用 hook_emu_stable.py 的加载顺序 (art_callsite 版 bypass → 立即 resume → patch_guard),
主 hook 只挂轻量 SSL hook (不干扰 UI), 然后用 adb UI 自动化走到登录→验证码,
验证: spawn+bypass 存活时, 极验验证码能否正常加载。
"""
from pathlib import Path
import frida, time, json, subprocess, re, sys
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/tmp/emu_spawn_captcha.json")
def sh(*args): return subprocess.run(args, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
MAIN_JS = """
'use strict';
send({type:'armed'});
/* 轻量: 只投递存活心跳, 不碰 UI 线程逻辑 */
setInterval(function(){ send({type:'alive', t:Date.now()}); }, 5000);
"""
def ui_dump():
adb("shell", "uiautomator", "dump", "/data/local/tmp/ui.xml")
return adb("shell", "cat", "/data/local/tmp/ui.xml").stdout
def find_bounds(xml, text):
m = re.search(r'<node[^>]*text="' + re.escape(text) + r'"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', xml)
if not m: return None
x1, y1, x2, y2 = map(int, m.groups())
return (x1 + x2) // 2, (y1 + y2) // 2
def tap(x, y): adb("shell", "input", "tap", str(x), str(y))
def type_text(s): adb("shell", "input", "text", s)
def go_login(force=False):
xml = ui_dump()
b = find_bounds(xml, "我的")
if not b: return False
tap(*b); time.sleep(2.5)
xml = ui_dump()
b = find_bounds(xml, "立即登录")
if not b: return False
tap(*b); time.sleep(3)
xml = ui_dump()
b = find_bounds(xml, "账号密码登录")
if b:
tap(*b); time.sleep(2)
return True
def fill_submit():
xml = ui_dump()
b = find_bounds(xml, "手机号/虎牙号") or find_bounds(xml, "请填写手机号码")
if not b: return False
tap(*b); time.sleep(0.4); type_text("13800138000")
xml = ui_dump()
b = find_bounds(xml, "密码")
if b:
tap(*b); time.sleep(0.4); type_text("test12345678")
time.sleep(0.6)
xml = ui_dump()
b = find_bounds(xml, "立即登录")
if not b: return False
tap(*b); time.sleep(2)
xml = ui_dump()
b = find_bounds(xml, "同意并继续")
if b:
tap(*b); time.sleep(4)
return True
def captcha_status():
xml = ui_dump()
joined = xml
webview = "android.webkit.WebView" in joined
slider = any(k in joined for k in ("滑块", "拼图", "安全验证"))
return {"webview": webview, "slider": slider,
"focus": adb("shell", "dumpsys window").stdout.count("OakVerifyActivity") > 0}
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
result = {}
# ===== 阶段1: 无 frida 基线 =====
print("== 阶段1: 无frida 基线 ==", flush=True)
adb("shell", "am", "force-stop", PACKAGE); time.sleep(1.2)
adb("shell", "monkey", "-p", PACKAGE, "-c", "android.intent.category.LAUNCHER", "1")
time.sleep(8)
if go_login() and fill_submit():
time.sleep(5)
result["baseline_no_frida"] = captcha_status()
print(" 验证码:", result["baseline_no_frida"], flush=True)
else:
print(" 基线流程失败", flush=True)
# ===== 阶段2: spawn+bypass 存活时 =====
print("== 阶段2: spawn+bypass ==", flush=True)
adb("shell", "am", "force-stop", PACKAGE); time.sleep(1.2)
pid = d.spawn([PACKAGE])
print(f" spawn pid={pid}", flush=True)
session = d.attach(pid)
def fast_load(path):
try:
s = session.create_script(path.read_text()); s.load(); return True
except Exception as e:
print(f" [load-err] {e}", flush=True); return False
fast_load(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js")
fast_load(RE / "evidence/scripts/mask_frida_maps_only.js")
d.resume(pid)
print(" resumed", flush=True)
try:
sg = session.create_script((RE/"evidence/scripts/patch_guard_block_termination.js").read_text()); sg.load()
except Exception as e:
print(f" [guard-err] {e}", flush=True)
alive_events = []
def on_main(m, dta):
if m.get('type') == 'send':
p = m.get('payload') or {}
if p.get('type') == 'alive':
alive_events.append(p)
elif m.get('type') == 'error':
print(" [JS-ERR]", str(m)[:120], flush=True)
sc = session.create_script(MAIN_JS)
sc.on('message', on_main)
sc.load()
# 等 UI 起来
time.sleep(8)
alive = [p for p in d.enumerate_processes() if p.pid == pid]
result["spawn_alive_at_8s"] = bool(alive)
print(f" 8s 存活: {bool(alive)}", flush=True)
if alive and go_login() and fill_submit():
time.sleep(5)
result["captcha_with_frida_alive"] = captcha_status()
print(" frida存活时验证码:", result["captcha_with_frida_alive"], flush=True)
alive2 = [p for p in d.enumerate_processes() if p.pid == pid]
result["spawn_alive_at_end"] = bool(alive2)
result["alive_heartbeats"] = len(alive_events)
print(f" 最终存活: {bool(alive2)}, 心跳数: {len(alive_events)}", flush=True)
try: d.kill(pid)
except: pass
OUT.write_text(json.dumps(result, ensure_ascii=False, indent=1))
print("[*] done ->", OUT, flush=True)
if __name__ == "__main__":
main()
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""hook BusinessCfg::setSafeDeviceId + getHdid, 抓真实 hdid/safeDeviceId 生成值 + 调用栈.
setSafeDeviceId(a1+1008=safeDeviceId, a1+1088=hdid), a2=argv1(safeDeviceId str), a3=argv2(hdid str)
同时 hook createWupDeviceInfo 观察 DeviceInfo 组装.
"""
from pathlib import Path
import frida, time, subprocess, json
REMOTE="127.0.0.1:31878"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
JS=r"""
'use strict';
function rcs(p,n){try{return p.readCString(n)||'';}catch(e){return '';}}
function st7(p){ // std::string (libc++ short/long form)
try{ var s=rcs(p,64); return s; }catch(e){return '<?>';}
}
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
// setSafeDeviceId: a2=std::string*a=safeDeviceId, a3=std::string*a2=hdid
function tryInstall(){
var md=Process.findModuleByName('libudbauthunify.so');
if(!md){ send({type:'waiting'}); return false; }
send({type:'mod',base:md.base.toString()});
Interceptor.attach(md.base.add(0x26A2E0),{
onEnter:function(a){
var sd=st7(a[1]); var hd=st7(a[2]);
send({type:'setSd',sd:sd.slice(0,64),hd:hd.slice(0,64),tid:Process.getCurrentThreadId()});
var bt=Thread.backtrace(this.context,Backtracer.ACCURATE).slice(0,14).map(function(x){return x.toString();});
send({type:'bt',bt:bt});
}
});
send({type:'setSd_hooked'});
Interceptor.attach(md.base.add(0x26A484),{
onEnter:function(){ this.out=this.context.x8; },
onLeave:function(retval){
try{
var out=this.out;
if(out.isNull()){ send({type:'getHdid',str:'NULL'}); return; }
// std::string: 字节0的低位=SSO标志
var first=out.readU8();
if((first & 1)===0){
// short string: len = first>>1; data in [out+1..]
var len=first>>1; var s=out.add(1).readCString(Math.max(0,Math.min(len,80)));
send({type:'getHdid',str:''+s,ln:len});
} else {
var p=out.readPointer(); var len=out.add(8).readU64(); var cap=out.add(16).readU64();
var s=p.readCString(Math.max(0,Math.min(len,80)));
send({type:'getHdid',str:''+s,ln:len,heap:true});
}
}catch(e){ send({type:'getHdid',err:String(e)}); }
}
});
send({type:'getHdid_hooked'});
return true;
}
var installed=false;
function poll(){
if(!installed){ installed=tryInstall(); }
if(!installed){ setTimeout(poll,300); }
}
poll();
"""
def main():
d=frida.get_device_manager().add_remote_device(REMOTE)
for a in range(1,4):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True); time.sleep(1)
pid=d.spawn([PACKAGE]); s=d.attach(pid)
try:
b=s.create_script((RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text()); b.load()
except: pass
d.resume(pid)
def on(m,dd):
if m.get('type')=='error': print(f" JSErr {str(m)[:150]}",flush=True); return
p=m.get('payload') or {}; t=p.get('type')
if t in ('setSd','getHdid'):
print(f"[att{a}] {t}: {p}",flush=True)
elif t=='bt': print(" backtrace:",flush=True);
elif t=='err' or t=='err2': print(f" {t}: {p}",flush=True)
sc=s.create_script(JS); sc.on('message',on); sc.load()
time.sleep(16)
try: d.kill(pid)
except: pass
time.sleep(1)
print("done",flush=True)
if __name__=="__main__": main()
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""模拟器 attach 版 dfpReport 捕获 (轮询重启 attach, 覆盖冷启动窗口)。
App 在模拟器上 spawn 会 EGL 崩, 所以用 attach。但 attach 只能抓 attach 之后的请求,
而 dfpReport 在冷启动早期就发。解法: 脚本后台反复 attach, 若 App 重启则重新 attach,
每次保住最长的 hook 窗口。配合外部 force-stop 重启 App 来完整覆盖。
"""
from __future__ import annotations
import frida, time, json
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/emu_attach_dfp.json")
JS = r"""
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
var n=0;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h='';try{h=a[1].readCString(Math.min(len,1500));}catch(e){return;}
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++;
send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,t:Date.now(),hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
"""
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
events = []
seen_sessions = set()
print(f"[*] attach 循环开始 (OUT={OUT})", flush=True)
try:
while True:
# 找 kiwi 进程
pid = None
try:
for p in d.enumerate_processes():
if 'kiwi' in p.name or 'duowan' in p.name:
pid = p.pid
break
except Exception:
pass
if pid and pid not in seen_sessions:
try:
s = d.attach(pid)
seen_sessions.add(pid)
print(f"[*] attached pid={pid}", flush=True)
sc = s.create_script(JS)
sc.on('message', lambda m, data: _on(m, events))
sc.load()
print("[*] hooked", flush=True)
# 保留这个会话
_keep[pid] = (s, sc)
except Exception as e:
print(f"[*] attach {pid} err {e}", flush=True)
time.sleep(3)
except KeyboardInterrupt:
pass
OUT.write_text(json.dumps(events))
print(f"[*] saved {len(events)} -> {OUT}", flush=True)
_keep = {}
def _on(message, events):
if message.get('type') != 'send':
if message.get('type') == 'error':
print("[JS-ERR]", str(message)[:150], flush=True)
return
p = message.get('payload') or {}
t = p.get('type')
if t == 'armed':
return
elif t == 'hooked':
print("[*] ssl hooked on this attach", flush=True)
elif t == 'err':
print("[*] err", p.get('e'), flush=True)
elif 'cls' in p:
import re
print(f"[{p.get('t')}] {p.get('cls')} len={p.get('len')}", flush=True)
events.append(p)
OUT.write_text(json.dumps(events))
if __name__ == "__main__":
main()
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""模拟器版 dfpReport 捕获: 验证全新模拟器是否为"新设备身份"
连接 127.0.0.1:31878 (模拟器 frida 15.2.2), spawn 方式。
只 hook SSL_write, 抓 dfpReport 请求 + 对应 606B 响应(内含 actionV/真或新 hdid)。
若 attach/反调试, 用 bypass 三件套 (可选开关)。
"""
from __future__ import annotations
import json, time
from pathlib import Path
import frida
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/emu_dfp_" + time.strftime("%H%M%S") + ".json")
REMOTE = "127.0.0.1:31878"
USE_BYPASS = True # 若 spawn 后闪退, 设为 False 再试
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var t0 = Date.now();
function hexb(p, n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(function(b){return ('0'+b.toString(16)).slice(-2)}).join(''); }catch(e){ return ''; } }
function head(p, n){ try{ return p.readCString(n); }catch(e){ return ''; } }
var n=0;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(!h)return;
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++;
send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,
t:Date.now()-t0,hex:hexb(a[1],len)});
}
}});
});
send({type:'sslwrite_hooked'});
}catch(e){ send({type:'sslwrite_err',e:String(e)}); }
try{
var r2=new ApiResolver('module');
r2.enumerateMatchesSync('exports:*!SSL_read').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){this.buf=a[1];},
onLeave:function(ret){
var n2=ret.toInt32();
if(n2<=0||n2>3200)return;
send({type:'resp',len:n2,hex:hexb(this.buf,n2),t:Date.now()-t0});
}});
});
send({type:'sslread_hooked'});
}catch(e){ send({type:'sslread_err',e:String(e)}); }
"""
def load(session, js, n=5, wait=2):
for i in range(n):
try:
s = session.create_script(js); s.load(); return s
except Exception as e:
print(f"[retry {i}] {e}", flush=True); time.sleep(wait)
return None
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
from subprocess import run
try:
pid = d.spawn([PACKAGE])
print(f"[*] spawn {PACKAGE} pid={pid}", flush=True)
except Exception as e:
# fallback attach
pid = [p for p in d.enumerate_processes() if 'kiwi' in p.name or 'duowan' in p.name]
pid = pid[0].pid if pid else None
print(f"[*] spawn failed, attach pid={pid}", flush=True)
session = d.attach(pid)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'armed':
print("[*] armed", flush=True)
elif t == 'sslwrite_hooked':
print("[*] SSL_write hooked", flush=True)
elif t == 'sslread_hooked':
print("[*] SSL_read hooked", flush=True)
elif t == 'req':
print(f"[{p.get('t')}ms] [{p.get('n')}] {p.get('cls')} len={p.get('len')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
elif t == 'resp':
import re
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p.get('hex') or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
print(f"[{p.get('t')}ms] [resp] len={p.get('len')}{mark}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
elif t in ('sslwrite_err','sslread_err'):
print(f"[*] {t}: {p.get('e')}", flush=True)
if USE_BYPASS:
print("[*] loading bypass", flush=True)
load(session, (RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text(), wait=1)
load(session, (RE / "evidence/scripts/mask_frida_maps_only.js").read_text(), wait=1)
d.resume(pid)
time.sleep(9)
load(session, (RE / "evidence/scripts/patch_guard_block_termination.js").read_text(), wait=1)
print("[*] loading 主 JS", flush=True)
script = load(session, JS)
else:
d.resume(pid)
time.sleep(6)
script = load(session, JS)
if script:
script.on('message', on_message)
print(f"[*] running. OUT={OUT}", flush=True)
try:
while True:
time.sleep(4)
except KeyboardInterrupt:
pass
print(f"[*] done -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""attach 模式抓模拟器自生成 32hex hdid (对比真机 ed0db8...).
核心: hook libudbauthunify.so:
- BusinessCfg::setSafeDeviceId(0x26A2E0): a1+1008=sd, a1+1088=hdid (两者都写)
- BusinessCfg::getHdid(0x26A484): this+1088 是 std::string 对象
- createWupDeviceInfo(0x2746A0): 返回结构 +152 = hdid std::string
App 正常启动后 attach, 不做 spawn (spawn 挂起会 EGL 闪退)。
"""
import sys, time, json
import frida
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
// short string: len = first>>1
var len = first >> 1;
return p.add(1).readUtf8String(len > 0 && len < 128 ? len : 0) || '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
if (len > 0 && len < 512) return data.readUtf8String(len) || '';
return null;
}
}catch(e){ return null; }
}
function log(t, m){ send({t:t, m:m}); }
function tryInstall(){
var md = Process.findModuleByName('libudbauthunify.so');
if (!md){ return false; }
log('mod', md.base.toString() + ' size=' + md.size);
// setSafeDeviceId: a2=sd, a3=hdid (std::string*)
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter: function(a){
this.sd = readStdString(a[1]);
this.hd = readStdString(a[2]);
},
onLeave: function(){
log('setSafeDeviceId', JSON.stringify({sd: (this.sd||'').slice(0,48), hd: (this.hd||'').slice(0,48)}));
}
});
// getHdid: this+1088 = std::string
Interceptor.attach(md.base.add(0x26A484), {
onEnter: function(a){ this.thiz = a[0]; },
onLeave: function(){
try{
var s = readStdString(this.thiz.add(1088));
log('getHdid', s ? ''+s : 'EMPTY');
}catch(e){ log('getHdid', 'ERR ' + e); }
}
});
// createWupDeviceInfo 输出 +152 hdid
Interceptor.attach(md.base.add(0x2746A0), {
onLeave: function(ret){
try{
var s = readStdString(ret.add(152));
log('wupDeviceInfo.hdid', s ? ''+s : 'EMPTY');
}catch(e){}
}
});
log('hooked', 'all');
return true;
}
var installed = false;
function poll(){
if (!installed){ installed = tryInstall(); }
if (!installed){ setTimeout(poll, 300); }
}
poll();
"""
def main():
dev = frida.get_device_manager().add_remote_device(REMOTE)
# 找 App 进程 attach
pid = None
for p in dev.enumerate_processes():
if p.name and PACKAGE in p.name:
pid = p.pid
break
if not pid:
print("App 未运行, 先启动:", PACKAGE)
import subprocess
subprocess.run(["adb", "-s", "127.0.0.1:5555", "shell",
"monkey -p %s -c android.intent.category.LAUNCHER 1" % PACKAGE],
capture_output=True)
time.sleep(12) # 等 lib 加载
for p in dev.enumerate_processes():
if p.name and PACKAGE in p.name:
pid = p.pid
break
if not pid:
print("仍找不到进程"); return
print("attach pid =", pid, flush=True)
session = dev.attach(pid)
def on_message(msg, data):
if msg.get("type") == "send":
pl = msg["payload"]
print("[%s] %s" % (pl["t"], pl["m"]), flush=True)
elif msg.get("type") == "error":
print("JSErr:", str(msg)[:200], flush=True)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("等待 hook 事件 (60s)...", flush=True)
time.sleep(60)
print("done", flush=True)
if __name__ == "__main__":
main()
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""守护式抓取模拟器 32hex hdid: 持续 attach kiwi 主进程, hook getHdid/setSafeDeviceId.
App 闪退/重启也不怕 —— 循环枚举进程, 遇新主进程立即 hook。
找到 hdid(≠ed0db8 或任意32hex)后写入 /tmp/emu_hdid_found.txt 并退出。
"""
import time, json, sys
import frida
REMOTE = "127.0.0.1:31878"
FOUND = "/tmp/emu_hdid_found.txt"
JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function emit(t,m){ send({t:t,m:m}); }
function hex32(s){ return /^[0-9a-fA-F]{32}$/.test(s||''); }
var hooked=false;
function tryHook(){
if(hooked) return true;
var md = Process.findModuleByName('libudbauthunify.so');
if(!md) return false;
emit('mod','base='+md.base);
try{
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); },
onLeave:function(){
var hd=(this.hd||''); var sd=(this.sd||'');
emit('SETSD', 'hdid='+hd+' sd='+sd.slice(0,32));
if(hex32(hd)) emit('FOUND_HDID', hd);
}
});
}catch(e){ emit('warn','setSd hook fail: '+e); }
try{
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){ this.thiz=a[0]; },
onLeave:function(){
try{
var s=readStdString(this.thiz.add(1088));
if(s) emit('GETHDID', ''+s);
if(hex32(s)) emit('FOUND_HDID', ''+s);
}catch(e){}
}
});
}catch(e){ emit('warn','getHdid hook fail: '+e); }
emit('ready','hooked');
hooked=true;
return true;
}
var tries=0;
function poll(){
tries++;
tryHook();
if(!hooked && tries<200){ setTimeout(poll, 250); }
}
poll();
"""
def main():
dev = frida.get_device_manager().add_remote_device(REMOTE)
seen = set()
found_hdid = None
start = time.time()
while time.time() - start < 600:
procs = []
try:
procs = dev.enumerate_processes()
except Exception:
time.sleep(2); continue
for p in procs:
if not p.name or 'kiwi' not in p.name:
continue
if ':cloudpatch' in p.name or ':logcat' in p.name:
continue
if p.pid in seen:
continue
seen.add(p.pid)
print(f"[{time.strftime('%H:%M:%S')}] 新主进程 pid={p.pid} {p.name}attach中...", flush=True)
try:
session = dev.attach(p.pid)
except Exception as e:
print(f" attach失败: {e}", flush=True)
continue
def on_msg(msg, data, pid=p.pid):
global found_hdid
if msg.get('type') == 'send':
pl = msg['payload']
t, m = pl['t'], pl['m']
if t == 'FOUND_HDID':
print(f"\n★★★★★★ 模拟器 hdid = {m} ★★★★★★", flush=True)
with open(FOUND, 'w') as f:
f.write(m)
return
print(f"[pid {pid}][{t}] {m}", flush=True)
elif msg.get('type') == 'error':
print(f"[pid {pid}] JSErr: {str(msg)[:150]}", flush=True)
try:
sc = session.create_script(JS)
sc.on('message', on_msg)
sc.load()
print(f" hook 已装 (pid {pid})", flush=True)
except Exception as e:
print(f" script失败: {e}", flush=True)
time.sleep(1.5)
print("10分钟超时结束", flush=True)
if __name__ == "__main__":
main()
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""模拟器 32hex hdid 抓取 v3: poll 等待模块 + hook createWupDeviceInfo(ret+152=hdid).
createWupDeviceInfo(0x2746A0): 每次 wup 打包必调用 (udb 上报/登录帧都走它),
返回 DeviceInfo 结构 +152 = hdid std::string (文档十九节真机验证过偏移)。
同时 hook setSafeDeviceId(0x26A2E0) / getHdid(0x26A484) 双保险。
"""
import frida, time, re, subprocess, json
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/tmp/emu_hdid_v3.json")
HDID_JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function emit(tag, val){ send({type:'hdid', tag:tag, val:''+(val||''), ts:Date.now()}); }
function install(){
var md = Process.findModuleByName('libudbauthunify.so');
if (!md) return false;
emit('mod-base', ''+md.base);
// 1) createWupDeviceInfo: 返回结构 +152 = hdid
try{
Interceptor.attach(md.base.add(0x2746A0), {
onLeave:function(ret){
try{
var s = readStdString(ret.add(152));
if (s) emit('WUPDEV_HDID', ''+s);
}catch(e){ emit('WUPDEV_ERR', ''+e); }
}
});
emit('createWupDev-hooked','');
}catch(e){ emit('createWupDev-err', ''+e); }
// 2) setSafeDeviceId: a2=sd, a3=hdid
try{
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){
this.sd = readStdString(a[1]);
this.hd = readStdString(a[2]);
},
onLeave:function(){
emit('SETSD', (this.hd||'') + ' | sd=' + (this.sd||'').slice(0,24));
}
});
emit('setSd-hooked','');
}catch(e){ emit('setSd-err', ''+e); }
// 3) getHdid: this+1088 (x8)
try{
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){
try{ this.thiz = this.context.x8; }catch(e){ this.thiz = null; }
},
onLeave:function(){
if (!this.thiz) return;
try{
var s = readStdString(this.thiz.add(1088));
emit('GETHDID', ''+s);
}catch(e){}
}
});
emit('getHdid-hooked','');
}catch(e){ emit('getHdid-err', ''+e); }
emit('ready','');
return true;
}
var installed = false;
function poll(){
if (!installed) installed = install();
if (!installed) setTimeout(poll, 200);
}
poll();
"""
MAIN_JS = r"""
'use strict';
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,800));
if(h.indexOf('hypasswordLogin')>=0||h.indexOf('huyaudbwebui')>=0){
send({type:'wup', len:len, hex:hexb(a[1],len), t:Date.now()});
}
}});
});
}catch(e){ send({type:'err', e:String(e)}); }
"""
def load(session, js, wait=0.2):
try:
s = session.create_script(js); s.load(); return s
except Exception as e:
print("[load-err]", str(e)[:150], flush=True)
return None
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],
capture_output=True)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
load(session, (RE/"evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text())
load(session, (RE/"evidence/scripts/mask_frida_maps_only.js").read_text())
hdid_sc = load(session, HDID_JS)
print("[*] HDID v3 loaded (poll-wait)", flush=True)
d.resume(pid)
print("[*] resumed", flush=True)
load(session, (RE/"evidence/scripts/patch_guard_block_termination.js").read_text(), wait=0.1)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'hdid':
print(f"[HDID {p.get('tag')}] {p.get('val')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'wup':
b = bytes.fromhex(p.get('hex',''))
if b'hidden' in b[:50]:
print(f"[WUP] len={p.get('len')}", flush=True)
else:
h = re.search(rb'hdid.{0,120}', b)
print(f"[WUP] len={p.get('len')} ctx={h.group(0)[:120] if h else b[:60]}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'err':
print("[err]", p.get('e'), flush=True)
hdid_sc.on('message', on_message)
main_sc = load(session, MAIN_JS)
if main_sc:
main_sc.on('message', on_message)
print("[*] running 90s ...", flush=True)
t0 = time.time()
try:
while time.time() - t0 < 90:
time.sleep(4)
except KeyboardInterrupt:
pass
print(f"[*] done, {len(events)} events", flush=True)
if __name__ == "__main__":
main()
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""模拟器 32hex hdid 抓取 v7 (严格按约定 doc)。
时序(严格复刻成功案例 hook_emu_spawn_bypass.py):
1. spawn(挂起)
2. load bypass_msaoaid_maps_art_callsite.js (挂起)
3. load mask_frida_maps_only.js (挂起)
4. resume
5. sleep 11
6. load patch_guard_block_termination.js
7. load 主 hook JS: 轮询等模块 -> hook setSafeDeviceId/getHdid/createWupDeviceInfo + SSL_write
命中即写 /tmp/emu_hdid_v7.json, 实时打印。
"""
import frida, time, re, subprocess, json
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/tmp/emu_hdid_v7.json")
MAIN_JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
function emit(tag,val){ send({type:tag, tag:tag, val:''+(val||''), ts:Date.now()}); }
var hooked=false, sslDone=false;
function hookUdb(){
if (hooked) return;
var md = Process.findModuleByName('libudbauthunify.so');
if (!md) return; // 未加载, 等下一轮
emit('mod-base', ''+md.base);
try{
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); },
onLeave:function(){
var hd=(this.hd||''); emit('SETSD', hd+' | sd='+(this.sd||'').slice(0,24));
if(/^[0-9a-f]{32}$/.test(hd)) emit('FOUND_HDID_32', hd);
}
});
}catch(e){ emit('setSd-err',''+e); }
try{
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){ try{ this.thiz=this.context.x8; }catch(e){ this.thiz=null; } },
onLeave:function(){
if(!this.thiz) return;
try{
var s=readStdString(this.thiz.add(1088));
if(s){ emit('GETHDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); }
}catch(e){}
}
});
}catch(e){ emit('getHdid-err',''+e); }
try{
Interceptor.attach(md.base.add(0x2746A0), {
onLeave:function(ret){
try{
var s=readStdString(ret.add(152));
if(s){ emit('WUPDEV_HDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); }
}catch(e){ emit('wupdev-err',''+e); }
}
});
}catch(e){ emit('wupdev-err2',''+e); }
emit('ready','all hooked');
hooked=true;
}
function hookSSL(){
if (sslDone) return;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,600));
if(h.indexOf('hypasswordLogin')>=0||h.indexOf('huyaudbwebui')>=0){
emit('WUP_FRAME', len+'B '+h.slice(0,80));
}
}});
});
send({type:'sslwrite-hooked', tag:'ssl'});
}catch(e){}
sslDone=true;
}
setInterval(function(){ hookUdb(); }, 300);
setInterval(function(){ hookSSL(); }, 1000);
"""
def load(session, js, wait=0.2):
try:
s = session.create_script(js); s.load(); return s
except Exception as e:
print("[load-err]", str(e)[:160], flush=True)
return None
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],
capture_output=True)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
# 挂起阶段: 只加载两个 bypass (复刻成功案例)
load(session, (RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text())
print("[*] bypass_msaoaid loaded", flush=True)
load(session, (RE/"evidence/scripts/mask_frida_maps_only.js").read_text())
print("[*] mask_frida loaded", flush=True)
d.resume(pid)
print("[*] resumed", flush=True)
# 立即加载 patch_guard (不 sleep 11! bash-118 成功案例 resume 后立即加载, sleep 期间 App 被杀)
load(session, (RE/"evidence/scripts/patch_guard_block_termination.js").read_text(), wait=0.1)
print("[*] patch_guard loaded", flush=True)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t in ('SETSD','GETHDID','WUPDEV_HDID','FOUND_HDID_32','mod-base','ready',
'setSd-err','getHdid-err','wupdev-err','wupdev-err2'):
print(f"[{t}] {p.get('val')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'WUP_FRAME':
print(f"[WUP_FRAME] {p.get('val')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'sslwrite-hooked':
print("[*] SSL_write hooked", flush=True)
main_sc = load(session, MAIN_JS)
if main_sc:
main_sc.on('message', on_message)
print("[*] running 150s", flush=True)
t0 = time.time()
while time.time() - t0 < 150:
time.sleep(3)
if any(e.get('tag') in ('FOUND_HDID_32','SETSD','GETHDID','WUPDEV_HDID') for e in events):
print("[*] 命中 hdid, 提前停止", flush=True)
break
print(f"[*] done, {len(events)} events", flush=True)
if __name__ == "__main__":
main()
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""模拟器 hdid v8: spawn + 三件套 + 内存扫描 32hex (App 启动即扫, 无需触发登录).
时序(成功验证): spawn → bypass×2(挂起) → resume → 立即 patch_guard → 主JS:
1. 轮询 hook setSafeDeviceId/getHdid/createWupDeviceInfo (保险)
2. 轮询: mod-base 出现后 3s, 扫描进程内存找 [0-9a-f]{32} ASCII 字符串并去重
3. SSL_write 抓 hypasswordLogin (万一触发登录)
"""
import frida, time, re, subprocess, json
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/tmp/emu_hdid_v8.json")
MAIN_JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
function emit(tag,val){ send({type:tag, tag:tag, val:''+(val||''), ts:Date.now()}); }
var hooked=false, scanned=false, sslDone=false;
function hookUdb(){
if (hooked) return;
var md = Process.findModuleByName('libudbauthunify.so');
if (!md) return;
emit('mod-base', ''+md.base);
try{
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); },
onLeave:function(){
var hd=(this.hd||''); emit('SETSD', hd+' | sd='+(this.sd||'').slice(0,24));
if(/^[0-9a-f]{32}$/.test(hd)) emit('FOUND_HDID_32', hd);
}
});
}catch(e){}
try{
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){ try{ this.thiz=this.context.x8; }catch(e){ this.thiz=null; } },
onLeave:function(){
if(!this.thiz) return;
try{ var s=readStdString(this.thiz.add(1088)); if(s){ emit('GETHDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); } }catch(e){}
}
});
}catch(e){}
try{
Interceptor.attach(md.base.add(0x2746A0), {
onLeave:function(ret){
try{ var s=readStdString(ret.add(152)); if(s){ emit('WUPDEV_HDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); } }catch(e){}
}
});
}catch(e){}
emit('ready','all hooked');
hooked = true;
}
function scanMem(){
// 扫描所有可读内存, 找 32hex ASCII 字符串 (含大小写hex)
var found = {};
var re32 = /[0-9a-f]{32}/i;
var ranges = Process.enumerateRanges({protection:'r--', coalesce:true});
ranges.forEach(function(r){
try{
var size = Math.min(r.size, 24*1024*1024);
var buf = Memory.readByteArray(r.base, size);
if (!buf) return;
var s = String.fromCharCode.apply(null, new Uint8Array(buf));
var m;
var re = /[0-9a-f]{32,40}/gi;
while ((m = re.exec(s)) !== null){
var v = m[0].toLowerCase();
if (!/^[0-9a-f]{32}$/.test(v)) continue;
if (found[v]) { found[v]++; continue; }
found[v] = 1;
}
}catch(e){}
});
var keys = Object.keys(found);
emit('scan-result', 'found32=' + keys.length + ' | ' + keys.slice(0,40).join(' '));
keys.forEach(function(k){ if(found[k]>=1) emit('H32CAND', k); });
scanned = true;
}
function hookSSL(){
if (sslDone) return;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,600));
if(h.indexOf('hypasswordLogin')>=0){
emit('WUP_LOGIN', len+'B '+hexb(a[1],len));
}
}});
});
send({type:'sslwrite-hooked', tag:'ssl'});
}catch(e){}
sslDone=true;
}
setInterval(hookUdb, 100);
setInterval(hookSSL, 1000);
// 模块出现后 3s 启动内存扫描
var scanLaunched = false;
setInterval(function(){
if (!scanLaunched && hooked && !scanned){
scanLaunched = true;
setTimeout(scanMem, 3000);
}
}, 500);
"""
def load(session, js, wait=0.2):
try:
s = session.create_script(js); s.load(); return s
except Exception as e:
print("[load-err]", str(e)[:160], flush=True)
return None
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],
capture_output=True)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
load(session, (RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text())
print("[*] bypass_msaoaid loaded", flush=True)
load(session, (RE/"evidence/scripts/mask_frida_maps_only.js").read_text())
print("[*] mask_frida loaded", flush=True)
d.resume(pid)
print("[*] resumed", flush=True)
load(session, (RE/"evidence/scripts/patch_guard_block_termination.js").read_text(), wait=0.1)
print("[*] patch_guard loaded", flush=True)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t in ('mod-base','ready','setSd-err','getHdid-err','wupdev-err'):
print(f"[{t}] {p.get('val')}", flush=True)
elif t == 'SETSD' or t == 'GETHDID' or t == 'WUPDEV_HDID':
print(f"[{t}] {p.get('val')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'FOUND_HDID_32':
print(f"\n★★★ FOUND 32HEX HDID = {p.get('val')} ★★★", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'scan-result':
print(f"[SCAN] {p.get('val')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'H32CAND':
print(f"[H32CAND] {p.get('val')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'WUP_LOGIN':
print(f"[WUP_LOGIN] {p.get('val')[:80]}", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
elif t == 'sslwrite-hooked':
print("[*] SSL_write hooked", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
main_sc = load(session, MAIN_JS)
if main_sc:
main_sc.on('message', on_message)
print("[*] running 160s", flush=True)
t0 = time.time()
while time.time() - t0 < 160:
time.sleep(3)
if any(e.get('tag')=='FOUND_HDID_32' for e in events):
print("[*] 命中, 提前停止", flush=True)
break
print(f"[*] done, {len(events)} events", flush=True)
if __name__ == "__main__":
main()
+203
View File
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""模拟器 hdid v9: spawn + 三件套 + 稳健内存扫描 32hex + hook 三函数 + SSL_write 抓登录帧.
v8 失败点: String.fromCharCode.apply 大数组崩 → 改分块扫描(chunk 256KB), 累计计数。
"""
import frida, time, re, subprocess, json
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/tmp/emu_hdid_v9.json")
MAIN_JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
function emit(tag,val){ send({type:tag, tag:tag, val:''+(val||''), ts:Date.now()}); }
var hooked=false, scanned=false, sslDone=false;
function hookUdb(){
if (hooked) return;
var md = Process.findModuleByName('libudbauthunify.so');
if (!md) return;
emit('mod-base', ''+md.base);
try{
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); },
onLeave:function(){
var hd=(this.hd||''); emit('SETSD', hd+' | sd='+(this.sd||'').slice(0,24));
if(/^[0-9a-f]{32}$/.test(hd)) emit('FOUND_HDID_32', hd);
}
});
}catch(e){}
try{
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){ try{ this.thiz=this.context.x8; }catch(e){ this.thiz=null; } },
onLeave:function(){
if(!this.thiz) return;
try{ var s=readStdString(this.thiz.add(1088)); if(s){ emit('GETHDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); } }catch(e){}
}
});
}catch(e){}
try{
Interceptor.attach(md.base.add(0x2746A0), {
onLeave:function(ret){
try{ var s=readStdString(ret.add(152)); if(s){ emit('WUPDEV_HDID',''+s); if(/^[0-9a-f]{32}$/.test(s)) emit('FOUND_HDID_32',''+s); } }catch(e){}
}
});
}catch(e){}
emit('ready','all hooked');
hooked = true;
}
// 稳健内存扫描: 分块读取, 用正则找 32hex ASCII
function scanMem(){
var found = {};
try{
var ranges = Process.enumerateRanges({protection:'r--', coalesce:true});
var total = ranges.length, scannedB = 0;
ranges.forEach(function(r, ri){
try{
var size = r.size;
if (size <= 0) return;
var step = 256*1024;
var off = 0;
while (off < size){
var n = Math.min(step, size - off);
var buf = Memory.readByteArray(r.base.add(off), n);
if (buf){
var sbuf = new Uint8Array(buf);
var s = '';
for (var i=0;i<sbuf.length;i++) s += String.fromCharCode(sbuf[i]);
var re = /[0-9a-f]{32,40}/gi;
var m;
while ((m = re.exec(s)) !== null){
var v = m[0].toLowerCase();
if (!/^[0-9a-f]{32}$/.test(v)) continue;
found[v] = (found[v]||0) + 1;
}
scannedB += n;
}
off += step;
}
}catch(e){}
});
}catch(e){ emit('scan-err',''+e); }
var keys = Object.keys(found);
emit('scan-result', 'ranges='+total+' scannedB='+scannedB+' found32+='+keys.length);
keys.sort().forEach(function(k, i){
if (i < 50) emit('H32CAND', k+' x'+found[k]);
// 过滤: 排除常见噪音
if (/^(00000000|ffffffff|0123456789)/.test(k)) return;
emit('H32CAND_NZ', k+' x'+found[k]);
});
scanned = true;
}
function hookSSL(){
if (sslDone) return;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,600));
if(h.indexOf('hypasswordLogin')>=0){
emit('WUP_LOGIN_FULL', len+'B '+hexb(a[1],len));
}
}});
});
send({type:'sslwrite-hooked', tag:'ssl'});
}catch(e){}
sslDone=true;
}
setInterval(hookUdb, 100);
setInterval(hookSSL, 1000);
var scanLaunched = false;
setInterval(function(){
if (!scanLaunched && hooked && !scanned){
scanLaunched = true;
setTimeout(scanMem, 2500);
}
}, 500);
"""
def load(session, js, wait=0.2):
try:
s = session.create_script(js); s.load(); return s
except Exception as e:
print("[load-err]", str(e)[:160], flush=True)
return None
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],
capture_output=True)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
load(session, (RE/"evidence/scripts/bypass_msaoaid_maps_skip_cleanup.js").read_text())
load(session, (RE/"evidence/scripts/mask_frida_maps_only.js").read_text())
print("[*] bypass x2 loaded", flush=True)
d.resume(pid)
print("[*] resumed", flush=True)
load(session, (RE/"evidence/scripts/patch_guard_block_termination.js").read_text(), wait=0.1)
print("[*] patch_guard loaded", flush=True)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t in ('mod-base','ready','scan-err'):
print(f"[{t}] {p.get('val')}", flush=True)
elif t in ('SETSD','GETHDID','WUPDEV_HDID'):
print(f"[{t}] {p.get('val')}", flush=True)
elif t == 'FOUND_HDID_32':
print(f"\n★★★ FOUND 32HEX HDID = {p.get('val')} ★★★", flush=True)
elif t == 'scan-result':
print(f"[SCAN] {p.get('val')}", flush=True)
elif t in ('H32CAND','H32CAND_NZ'):
print(f"[{t}] {p.get('val')}", flush=True)
elif t == 'WUP_LOGIN_FULL':
b = bytes.fromhex(p.get('val','').split('B ')[-1]) if 'B ' in p.get('val','') else b''
h = re.search(rb'hdid.{0,100}', b)
print(f"[WUP_LOGIN] len={p.get('val','').split('B')[0]} ctx={(h.group(0)[:110] if h else b[:60])}", flush=True)
elif t == 'sslwrite-hooked':
print("[*] SSL_write hooked", flush=True)
events.append(p); OUT.write_text(json.dumps(events, indent=1))
main_sc = load(session, MAIN_JS)
if main_sc:
main_sc.on('message', on_message)
print("[*] running 170s", flush=True)
t0 = time.time()
while time.time() - t0 < 170:
time.sleep(3)
if any(e.get('tag')=='FOUND_HDID_32' for e in events):
print("[*] 命中, 提前停止", flush=True)
break
print(f"[*] done, {len(events)} events", flush=True)
if __name__ == "__main__":
main()
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""抓模拟器自生成 32hex hdid — attach 主进程版 (PID=10200).
hook libudbauthunify.so:
- BusinessCfg::setSafeDeviceId(0x26A2E0): a2=sd, a3=hdid
- BusinessCfg::getHdid(0x26A484): this+1088 = std::string
- createWupDeviceInfo(0x2746A0): 返回 +152 = hdid
"""
import time, frida
REMOTE = "127.0.0.1:31878"
PID = 10200
JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function log(t,m){ send({t:t,m:m}); }
function install(){
var md = Process.findModuleByName('libudbauthunify.so');
if(!md){ log('err','no module'); return; }
log('mod','base='+md.base+' size='+md.size);
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); },
onLeave:function(){ log('setSafeDeviceId', JSON.stringify({sd:(this.sd||'').slice(0,48), hd:(this.hd||'').slice(0,48)})); }
});
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){ this.thiz=a[0]; },
onLeave:function(){
try{
var s=readStdString(this.thiz.add(1088));
log('getHdid', (s!==null&&s!==undefined?'':s)||'EMPTY');
}catch(e){ log('getHdid','ERR '+e); }
}
});
Interceptor.attach(md.base.add(0x2746A0), {
onLeave:function(ret){
try{
var s=readStdString(ret.add(152));
if (s) log('wupDevInfo.hdid', ''+s);
}catch(e){}
}
});
// 也 hook libhydeviceid 数据区常见字符串? 先看 getHdid 输出
log('hooked','all OK');
}
install();
"""
def main():
dev = frida.get_device_manager().add_remote_device(REMOTE)
pid = PID
try:
s = dev.attach(pid)
except Exception as e:
print("attach失败:", e); return
print("attached", pid, flush=True)
sc = s.create_script(JS)
def on_msg(m,d):
if m.get('type')=='send':
p=m['payload']
print("[%s] %s" % (p['t'], p['m']), flush=True)
elif m.get('type')=='error':
print("JSErr:", str(m)[:200], flush=True)
sc.on('message', on_msg)
sc.load()
print("等待 90s ...", flush=True)
time.sleep(90)
print("done", flush=True)
if __name__ == "__main__":
main()
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""attach 模式抓模拟器 32hex hdid v4: hook createWupDeviceInfo(ret+152=hdid).
直接 attach 正在运行的主进程(不 spawn, 无 EGL 崩溃风险)。
createWupDeviceInfo 每次 udb 上报/wup 打包都调用 → 上报即触发, 无需登录UI。
"""
import frida, time, re, json, sys
REMOTE = "127.0.0.1:31878"
OUT = Path_ = "/tmp/emu_hdid_v4.json"
JS = r"""
'use strict';
function readStdString(p){
if (p.isNull()) return null;
try{
var first = p.readU8();
if ((first & 1) === 0) {
var len = first >> 1;
return (len>0 && len<256) ? p.add(1).readUtf8String(len) : '';
} else {
var data = p.readPointer();
var len = p.add(8).readU64();
return (len>0 && len<1024) ? data.readUtf8String(len) : null;
}
}catch(e){ return null; }
}
function emit(tag, val){ send({type:'hdid', tag:tag, val:''+(val||''), ts:Date.now()}); }
function install(){
var md = Process.findModuleByName('libudbauthunify.so');
if (!md){ emit('state','NO_MOD'); return; }
emit('state','mod='+md.base);
try{
Interceptor.attach(md.base.add(0x2746A0), {
onEnter:function(){ emit('tick','createWupDeviceInfo #' + (++window.__n||(window.__n=1))); },
onLeave:function(ret){
try{
var s = readStdString(ret.add(152));
emit('WUPDEV_HDID', ''+s);
}catch(e){ emit('WUPDEV_ERR', ''+e); }
}
});
emit('state','createWupDev hooked');
}catch(e){ emit('state','createWupDev ERR '+e); }
try{
Interceptor.attach(md.base.add(0x26A2E0), {
onEnter:function(a){ this.sd=readStdString(a[1]); this.hd=readStdString(a[2]); },
onLeave:function(){ emit('SETSD', (this.hd||'')+' | sd='+(this.sd||'').slice(0,24)); }
});
}catch(e){}
try{
Interceptor.attach(md.base.add(0x26A484), {
onEnter:function(a){ try{ this.thiz=this.context.x8; }catch(e){ this.thiz=null; } },
onLeave:function(){
if(!this.thiz) return;
try{ var s=readStdString(this.thiz.add(1088)); if(s) emit('GETHDID',''+s); }catch(e){}
}
});
}catch(e){}
emit('state','all hooked, waiting events');
}
install();
"""
def main():
dev = frida.get_device_manager().add_remote_device(REMOTE)
pid = None
for p in dev.enumerate_processes():
if p.name and p.name == 'com.duowan.kiwi':
pid = p.pid; break
if not pid:
print("找不到主进程"); return
print("attach pid =", pid, flush=True)
s = dev.attach(pid)
events = []
def on_msg(m, d):
if m.get('type') == 'send':
p = m['payload']
print(f"[{p.get('tag')}] {p.get('val')}", flush=True)
events.append(p); open(OUT,'w').write(json.dumps(events, indent=1))
elif m.get('type') == 'error':
print("JSErr:", str(m)[:150], flush=True)
sc = s.create_script(JS)
sc.on('message', on_msg)
sc.load()
print("运行 100s (App 上报会自动触发 createWupDeviceInfo)...", flush=True)
time.sleep(100)
print("done,", len(events), "events", flush=True)
if __name__ == "__main__":
main()
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""模拟器 spawn+立即resume 版 dfpReport 捕获。
关键: spawn 后【立即】resume(App 避免 EGL 崩溃), 然后立即 attach+load 主 hook。
dfpReport 冷启动约 1s 后自动发, attach 足够快能赶上。
"""
from __future__ import annotations
import frida, time, json, re
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/emu_spawn_dfp.json")
JS = r"""
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
var n=0;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++; send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,t:Date.now(),hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
"""
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
# 立即 resume (防 EGL 崩)
d.resume(pid)
time.sleep(0.4)
session = d.attach(pid)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'armed':
print("[*] armed", flush=True)
elif t == 'hooked':
print("[*] SSL_write hooked", flush=True)
elif t == 'err':
print("[*] err", p.get('e'), flush=True)
elif 'cls' in p:
print(f"[{p.get('t')}] {p.get('cls')} len={p.get('len')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
sc = session.create_script(JS)
sc.on('message', on_message)
sc.load()
print(f"[*] loaded. running (OUT={OUT})", flush=True)
t0 = time.time()
try:
while time.time() - t0 < 60:
time.sleep(4)
except KeyboardInterrupt:
pass
print(f"[*] done, {len(events)} dfpRevent(s)", flush=True)
if __name__ == "__main__":
main()
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""模拟器 spawn + frida 绕过 版 dfpReport 捕获。
时序(模拟器需快速 resume 防 EGL 崩, 但反调试需在 resume 前 hook):
spawn(挂起) → 快速 load bypass_msaoaid + mask_frida (~1s内)
→ resume → patch_guard → 主 hook (抓 dfpReport+响应 actionV)
bypass 脚本用动态定位(Process.findModuleByName), 偏移为 lib 内偏移,
同一 apk(13.4.22)下与真机一致, 直接复用。
"""
from __future__ import annotations
import frida, time, json, re, subprocess
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/emu_spawn_bypass_dfp.json")
JS = r"""
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
var n=0;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++; send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,t:Date.now(),hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
try{
var r2=new ApiResolver('module');
r2.enumerateMatchesSync('exports:*!SSL_read').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){this.buf=a[1];},
onLeave:function(ret){
var nn=ret.toInt32();
if(nn<=0||nn>3200)return;
send({type:'resp',len:nn,hex:hexb(this.buf,nn),t:Date.now()});
}});
});
send({type:'readhooked'});
}catch(e){send({type:'readerr',e:String(e)});}
"""
def load(session, js, wait=0.2):
try:
s = session.create_script(js); s.load(); return s
except Exception as e:
print("[load-err]", str(e)[:150], flush=True); return None
def main():
subprocess.run(["adb", "-s", "127.0.0.1:5555", "shell", "am", "force-stop", PACKAGE], capture_output=True)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawn pid={pid}", flush=True)
session = d.attach(pid)
# 1) 挂起时快速加载两个 bypass
print("[*] load bypass_msaoaid (suspended)", flush=True)
load(session, (RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text())
print("[*] load mask_frida (suspended)", flush=True)
load(session, (RE / "evidence/scripts/mask_frida_maps_only.js").read_text())
# 2) resume (防 EGL 崩), 快速
d.resume(pid)
print("[*] resumed", flush=True)
# 3) patch_guard + 主 hook
load(session, (RE / "evidence/scripts/patch_guard_block_termination.js").read_text(), wait=0.1)
print("[*] load 主 hook JS", flush=True)
events = []
def on_message(m, data):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'armed': print("[*] armed", flush=True)
elif t == 'hooked': print("[*] SSL_write hooked", flush=True)
elif t == 'readhooked': print("[*] SSL_read hooked", flush=True)
elif t in ('err','readerr'): print("[*]", t, p.get('e'), flush=True)
elif t == 'event': print("[bypass]", p.get('event'), flush=True)
elif 'cls' in p:
print(f"[{p.get('t')}] {p.get('cls')} len={p.get('len')}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
elif 'resp' in t or t == 'resp':
import re
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p.get('hex') or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
print(f"[{p.get('t')}] RESP len={p.get('len')}{mark}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
sc = load(session, JS)
if sc:
sc.on('message', on_message)
print(f"[*] running (OUT={OUT})", flush=True)
t0 = time.time()
try:
while time.time() - t0 < 70:
time.sleep(4)
except KeyboardInterrupt:
pass
print(f"[*] done, {len(events)} events", flush=True)
if __name__ == "__main__":
main()
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""模拟器 稳定版 dfpReport 抓取: spawn→挂起快速加载2个bypass→立即resume→patch_guard+主hook。
内置自动重试(EGL 偶发崩 + 反调试偶发), 一旦抓到 dfpReport+actionV 或达上限即停。
"""
from __future__ import annotations
import frida, time, json, subprocess
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/emu_stable_dfp.json")
MAIN_JS = """
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
var n=0;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
n++; send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',n:n,len:len,t:Date.now(),hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
try{
var r2=new ApiResolver('module');
r2.enumerateMatchesSync('exports:*!SSL_read').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){this.buf=a[1];},
onLeave:function(ret){
var nn=ret.toInt32();
if(nn<=0||nn>4000)return;
send({type:'resp',len:nn,hex:hexb(this.buf,nn),t:Date.now()});
}});
});
send({type:'readhooked'});
}catch(e){send({type:'readerr',e:String(e)});}
"""
def run_once(d, events, attempt):
"""一次尝试, 成功(抓到 actionV 或 dfp) 返回 True."""
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1.2)
pid = d.spawn([PACKAGE])
print(f"[att{attempt}] spawn pid={pid}", flush=True)
session = d.attach(pid)
# 挂起时快速加载两个 bypass (极快, 不 sleep)
def fast_load(path):
try:
s = session.create_script(path.read_text()); s.load(); return True
except Exception as e:
print(f" [bypass-load-err] {e}", flush=True); return False
fast_load(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js")
fast_load(RE / "evidence/scripts/mask_frida_maps_only.js")
# 立即 resume
d.resume(pid)
print(f"[att{attempt}] resumed", flush=True)
got_dfp = False
def on_main(m, dta):
nonlocal got_dfp
if m.get('type') == 'error':
return
p = m.get('payload') or {}
t = p.get('type')
if t == 'hooked':
print(f"[att{attempt}] SSL_write hooked", flush=True)
elif 'cls' in p:
print(f"[att{attempt}] {p['cls']} len={p['len']}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
if p['cls'] == 'dfpReport':
got_dfp = True
elif t == 'resp':
import re
av = re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(p.get('hex') or ''))
mark = f" actionV={av.group(1).decode()}" if av else ""
print(f"[att{attempt}] RESP len={p['len']}{mark}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
# patch_guard + 主 hook (resume 后)
try:
sg = session.create_script((RE/"evidence/scripts/patch_guard_block_termination.js").read_text()); sg.load()
except Exception as e:
print(f" [guard-err] {e}", flush=True)
sc = session.create_script(MAIN_JS)
sc.on('message', on_main)
sc.load()
# 观察 ~35s, 抓到 dfp 即提前停
for _ in range(8):
time.sleep(4)
if got_dfp:
# 再等 actionV 响应
time.sleep(4)
print(f"[att{attempt}] dfp captured, stopping", flush=True)
break
try: d.kill(pid)
except: pass
return got_dfp
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
events = []
for attempt in range(1, 6): # 最多 5 次
try:
if run_once(d, events, attempt):
print(f"[*] SUCCESS on attempt {attempt}", flush=True)
break
except Exception as e:
print(f"[att{attempt}] ERROR {repr(e)}", flush=True)
time.sleep(2)
print(f"[*] done, {len(events)} events, saved {OUT}", flush=True)
if __name__ == "__main__":
main()
+78
View File
@@ -0,0 +1,78 @@
"""用 frida attach 到运行中 App, hook __system_property_get,
记录虎牙派生读取的系统属性, 并测试替换某些属性后 hdid 是否变化.
"""
import frida, time, subprocess, sys, json, re, ssl, socket
PACKAGE="com.duowan.kiwi"
REMOTE="127.0.0.1:31878"
JS = r"""
'use strict';
send({type:'armed'});
function rcsv(p){try{return p.readCString(256)||'';}catch(e){return '';}}
var interest = ['ro.serialno','ro.product.model','ro.product.device',
'ro.boot.serialno','ro.hardware','ro.build.fingerprint','ro.build.id',
'ro.product.board','ro.product.manufacturer','ro.ril.miui.imei0',
'persist.sys.imei','gsm.imei','ro.boot.image','ro.boot.product',
'ro.kernel.qemu','init.svc.adbd','ro.product.cpu.abi','ro.build.version.sdk',
'ro.build.version.release','ro.product.brand','ro.product.name'];
function hook(){
var m = Process.findModuleByName('libc.so');
// dl_ prefixed variants first
var syms = ['__system_property_get','free'];
var target = Module.findExportByName('libc.so','__system_property_get');
if(!target){send({type:'info',k:'no-propget'});return;}
send({type:'info',k:'found',a:target.toString()});
Interceptor.attach(target,{
onEnter:function(a){
this.name = rcsv(a[0]);
},
onLeave:function(ret){
var ov = rcsv(this.ctx.x1); // __system_property_get 第二参数 value buffer (x1)
var sig = this.name+'='+ov;
// 只记录感兴趣的; 若想起记则全记
if(interest.indexOf(this.name)>=0 || 1){
send({type:'prop',name:this.name,val:ov.slice(0,80)});
}
// 可选替换
// if(this.name=='ro.serialno'){ this.ctx.x1.writeUtf8String('99999999999'); }
}
});
}
hook();
"""
def get_pid(d):
for _ in range(10):
r=subprocess.run(["adb","-s","127.0.0.1:5555","shell","pidof",PACKAGE],capture_output=True,text=True)
if r.stdout.strip(): return int(r.stdout.strip().split()[0])
time.sleep(1)
return None
def main():
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1)
subprocess.run(["adb","-s","127.0.0.1:5555","shell","monkey","-p",PACKAGE,"-c","android.intent.category.LAUNCHER","1"],capture_output=True)
# attach 前等 app 起来但不触发派生(login 触发)
time.sleep(6)
d=frida.get_device_manager().add_remote_device(REMOTE)
pid=get_pid(d)
print("attaching pid",pid,flush=True)
if not pid:
print("no pid"); return
s=d.attach(pid)
props=[]
def on(m,dd):
if m.get('type')!='send':return
p=m.get('payload') or {}
if p.get('type')=='prop':
props.append((p['name'],p['val']))
print(f" {p['name']} = {p['val']}",flush=True)
sc=s.create_script(JS); sc.on('message',on); sc.load()
print("[*] hooked, 触发登录/活动 观察派生属性 20s...",flush=True)
# 触发动作让 app 读取属性
time.sleep(20)
print(f"[*] 共捕获 {len(props)} 次属性读取",flush=True)
try: s.detach()
except: pass
if __name__=="__main__": main()
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""请求链捕获器: 冷启动 → 完整登录链。
不再只盯 dfpReport。hook SSL_write + SSL_read:
- SSL_write: 读取 body 前 2000B, 按文本标记分类 (dfpReport/hy*Credlogin/hylogout/dckey/PrtReq 等),
记录完整 hex + len + t + 分类。
- SSL_read: 记录响应 hex + len + t, 按时间与请求配对, 看身份链 (t1/t5/actionV 的建立)。
用途: 搞清楚 dfpReport 在登录链中的角色, 设备身份(t1/t5)到底由哪个接口发放。
约定: 后台启动 + tail 实时看 + 命中即杀 (docs/Hook脚本运行与捕获约定.md)。
spawn 顺序: bypass x2 -> resume+11s -> patch_guard -> 最后主 hook JS。
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import frida
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/reqchain_" + time.strftime("%H%M%S") + ".json")
# 请求分类关键字 (wup payload 名 / http 头)
MARKERS = [
("dfpReport", "dfpReport"),
("hylogout", "hylogout"),
("hyCredlogin", "hyCredlogin"),
("hyanonymousCredlogin", "hyanonymousCredlogin"),
("dckey", "dckey"),
("PrtReq", "PrtReq"),
("CheckUidIsBind", "CheckUidIsBind"),
("GetUidByGameZone", "GetUidByGameZone"),
("get3", "get3"),
("verify3", "verify3"),
]
def classify(head: bytes) -> str:
for marker, name in MARKERS:
if marker.encode() in head:
return name
return "other"
JS = r"""
'use strict';
Process.setExceptionHandler(function(d){ return true; });
send({type:'armed'});
var t0 = Date.now();
function hexb(p, n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(function(b){return ('0'+b.toString(16)).slice(-2)}).join(''); }catch(e){ return ''; } }
function head(p, n){ try{ return p.readCString(n); }catch(e){ return ''; } }
function classify(head){
var ms = [['dfpReport','dfpReport'],['hylogout','hylogut'],['hyanonymousCredlogin','hyanonym'],
['hyCredlogin','hyCredlogin'],['dckey','dckey'],['PrtReq','PrtReq'],['get3','get3'],
['verify3','verify3']];
for (var i=0;i<ms.length;i++){ if (head.indexOf(ms[i][0]) >= 0) return ms[i][1]; }
return 'other';
}
var n = 0;
try{
var r = new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address, {
onEnter: function(a){
var len = a[2].toInt32();
if (len < 40 || len > 50000) return;
var h = head(a[1], Math.min(len, 2000));
if (!h) return;
var name = classify(h);
// 过滤明显无关 (cert请求等), 保留虎牙业务
if (name === 'other' && h.indexOf('POST /') < 0 && h.indexOf(' 1@') < 0 && h.indexOf('PUT /') < 0) return;
n++;
send({type:'req', n:n, cls:name, len:len, head:h.slice(0,80),
hex:hexb(a[1], len), t:Date.now()-t0});
}
});
});
send({type:'sslwrite_hooked'});
}catch(e){ send({type:'sslwrite_err', e:String(e)}); }
try{
var r2 = new ApiResolver('module');
r2.enumerateMatchesSync('exports:*!SSL_read').forEach(function(m){
Interceptor.attach(m.address, {
onEnter: function(a){ this.buf=a[1]; this.cap=a[2].toInt32(); },
onLeave: function(ret){
var n2 = ret.toInt32();
if (n2 <= 0 || n2 > 3200) return;
send({type:'resp', len:n2, hex:hexb(this.buf, n2), t:Date.now()-t0});
}
});
});
send({type:'sslread_hooked'});
}catch(e){ send({type:'sslread_err', e:String(e)}); }
"""
def load_with_retry(session, js, n=5):
last = None
for i in range(n):
try:
s = session.create_script(js)
s.load()
return s
except Exception as e:
last = e
print(f"[retry {i}] {e}", flush=True)
time.sleep(3)
raise last
def main():
# 清旧实例 (多进程坑)
import subprocess
subprocess.run(["adb", "shell", "am", "force-stop", PACKAGE], capture_output=True)
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
pid = device.spawn([PACKAGE])
print(f"[*] spawned {PACKAGE} pid={pid}", flush=True)
session = device.attach(pid)
events = []
def on_message(message, data):
if message.get("type") == "error":
print("[JS-ERR]", str(message)[:300], flush=True)
return
if message.get("type") != "send":
return
p = message.get("payload") or {}
t = p.get("type")
if t == "armed":
print("[*] JS armed", flush=True)
elif t == "sslwrite_hooked":
print("[*] SSL_write hooked", flush=True)
elif t == "sslread_hooked":
print("[*] SSL_read hooked", flush=True)
elif t == "req":
print(f"[{p.get('t')}ms] [{p.get('n')}] {p.get('cls'):12s} len={p.get('len')}", flush=True)
events.append({"type": "req", **p})
OUT.write_text(json.dumps(events))
elif t == "resp":
h = p.get('hex', '')
import re as _re
av = _re.search(rb'actionV\(([0-9a-f]{40})', bytes.fromhex(h))
mark = f" actionV={av.group(1).decode()}" if av else ""
print(f"[{p.get('t')}ms] [resp] len={p.get('len')}{mark}", flush=True)
events.append({"type": "resp", **p})
OUT.write_text(json.dumps(events))
elif t in ("sslwrite_err", "sslread_err"):
print(f"[*] {t}: {p.get('e')}", flush=True)
# 严格约定顺序
print("[*] loading bypass", flush=True)
load_with_retry(session, (RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text())
load_with_retry(session, (RE / "evidence/scripts/mask_frida_maps_only.js").read_text())
device.resume(pid)
time.sleep(11)
load_with_retry(session, (RE / "evidence/scripts/patch_guard_block_termination.js").read_text())
print("[*] loading 主 hook JS (最后)", flush=True)
script = load_with_retry(session, JS)
script.on("message", on_message)
print("[*] 已 resume。捕获取请求链 (冷启动自动触发 + 登录触发)", flush=True)
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
print(f"[*] 结束 -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""从 dfpReport wire 恢复明文 JSON + 三元组(hdid/deviceId/appkey)。
原理: 明文 = '{"appId":"5008"...' 的已知前缀 XOR keystream 恢复 ks 前段;
再用"完整JSON结构模板"+ 已知 deviceName/systemVer(每设备不同但可从 DeviceInfo 或 build 推算)
重建 JSON, 校验 ks 前缀一致性, 提取各 unique 字段.
已知: 所有帧的 JSON 结构一致 (字段顺序固定), 仅 4 个 unique 值不同:
appkey(?), channel, deviceId(40hex), hdid(40hex), deviceName(变长), systemVer(变长)
"""
from __future__ import annotations
import json, binascii, re
MAGIC = bytes.fromhex('571882cf664bb39401ee')
# 已知明文 JSON 前缀 (所有设备相同)
PREFIX = b'{"appId":"5008","appVer":"13.4.22"'
def extract_body(wire):
bs = wire.find(b'\r\n\r\n') + 4
body = wire[bs:]
mi = body.find(MAGIC)
return bytes(body[mi+10:])
def recover_plaintext(cw, device_model='M2102J2SC', sysver_maxlen=40):
"""用 PREFIX 恢复 ks 前段, 再用 JSON 模板重建明文. 返回 (plaintext_bytes, json_str)"""
ks = bytes(cw[i]^PREFIX[i] for i in range(len(PREFIX)))
# 模板: 全部字段, unique 值用已知结构 + 已知固定值
# 由于 hdid/deviceId/appkey 值未知但都是 hex, deviceName 已知,
# 我们分两段恢复:
# 段1: 从 prefix 到 "appkey":"... 之前(结构固定), 逐字节可读 ASCII 推进, appkey值=as hex guess
# 暂时只做前缀验证 + 结构重建
return ks
def try_template(cw, json_str):
Jb=json_str.encode();
if len(Jb)>len(cw): return None
ok=all(cw[i]^Jb[i]== (cw[i]^PREFIX[i] if False else cw[i]^Jb[i]) for i in range(0))
# 校验核心: 用已知前缀比较 ks
ks_t=cw[:len(PREFIX)]
ks_t=bytes(cw[i]^Jb[i] for i in range(len(PREFIX)))
# 与 PREFIX 派生的 ks 应相等(如果Jb在prefix处正确)
match=all((cw[i]^Jb[i])==(cw[i]^PREFIX[i]) for i in range(len(PREFIX)))
return match
def process_file(path):
d=json.load(open(path))
wire=binascii.unhexlify(d.get('dfp_wire',d.get('dfp','')))
cw=extract_body(wire)
return cw
if __name__=='__main__':
import glob
print("对已知帧应用恢复(验证JSON结构):")
frames = glob.glob('evidence/frame_*.json')+glob.glob('evidence/identity_*.json')
for f in sorted(frames):
d=json.load(open(f))
w=binascii.unhexlify(d.get('dfp_wire',''))
if not w: continue
cw=extract_body(w)
print(f" {f}: cw_len={len(cw)}", end='')
# 校验 prefix 匹配
ok=all(cw[i]== (w[0]^w[0]) for i in range(0))
if len(cw)>=len(PREFIX):
# 若 cw[0:len]== cw[j:j+len] 无意义; 校验PREFIX是否在明文位置: ks=cw^PREFIX 应任意
print(" prefix_ok")
else:
print()
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""模拟器 spawn + 真机G2-0055组合bypass(bypass_msaoaid_maps_skip_cleanup.js) 存活测试。
对照真机50s存活结论, 应在模拟器上也长时存活。
"""
from pathlib import Path
import frida, time, subprocess, json
REMOTE="127.0.0.1:31878"; PACKAGE="com.duowan.kiwi"
RE=Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
BYPASS="bypass_msaoaid_maps_skip_cleanup.js"
def one_round(d,r):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE],capture_output=True)
time.sleep(1.2)
pid=d.spawn([PACKAGE])
s=d.attach(pid)
# 挂起时加载 G2-0055 组合
sc=s.create_script((RE/"evidence/scripts"/BYPASS).read_text())
def on(m,dd):
if m.get('type')=='send': print(f" r{r} {m.get('payload')}",flush=True)
elif m.get('type')=='error': print(f" r{r} ERR {str(m)[:100]}",flush=True)
sc.on('message',on); sc.load()
d.resume(pid)
print(f"r{r} spawned+bypass, resume, 观察存活...",flush=True)
t0=time.time(); died=None; last=True
while time.time()-t0<90:
time.sleep(3)
try: alive=[p for p in d.enumerate_processes() if p.pid==pid]
except: break
if not alive:
died=time.time()-t0
print(f" r{r} DEAD at +{died:.0f}s",flush=True); break
last=(time.time()-t0)
if not died:
print(f" r{r} 存活超过 90s! (仍在最后观测点 {last:.0f}s)",flush=True)
try: d.kill(pid)
except: pass
return died if died else 90
def main():
d=frida.get_device_manager().add_remote_device(REMOTE)
for r in range(1,4):
try:
surv=one_round(d,r)
print(f"=> r{r} 存活={surv:.0f}s",flush=True)
except Exception as e:
print(f"r{r} ERR {e}",flush=True)
time.sleep(2)
if __name__=="__main__": main()
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""spawn + 快速加载 bypass → resume 的【存活时长】多轮测试。
验证: spawn 路径带 bypass 后 App 能活多久, 是否每个 spawn 都稳定抓到 dfpReport。
流程每轮:
force-stop → spawn → attach → 挂起内快速加载 bypass_msaoaid + mask_frida
→ resume → 加载 patch_guard + 主hook(SSL_write抓dfp)
→ 持续观察存活, 记录死亡时间 或 抓满 N 秒
统计多轮: 平均存活时长, dfpReport 捕获成功率, actionV。
"""
from __future__ import annotations
import frida, time, subprocess, json, re
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/spawn_survival.json")
MAIN_JS = """
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',len:len,t:Date.now()});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
"""
ROUNDS = 4
MAX_SURVIVE = 120 # 每轮最多观察秒数
def one_round(d, r):
subprocess.run(["adb","-s","127.0.0.1:5555","shell","am","force-stop",PACKAGE], capture_output=True)
time.sleep(1.2)
pid = d.spawn([PACKAGE])
s = d.attach(pid)
# 挂起快速加载 2 bypass
for name in ["bypass_msaoaid_maps_art_callsite.js", "mask_frida_maps_only.js"]:
try:
sc = s.create_script((RE/"evidence/scripts"/name).read_text()); sc.load()
except Exception as e:
print(f" r{r} bp {name} err {e}", flush=True)
d.resume(pid)
dfp = []
def on_main(m, dd):
if m.get('type') == 'error': return
p = m.get('payload') or {}
if p.get('type') == 'hooked':
print(f" r{r} hook installed", flush=True)
elif 'cls' in p:
print(f" r{r} {p['cls']} len={p['len']}", flush=True)
dfp.append(p)
# patch_guard + 主 hook
try:
sg = s.create_script((RE/"evidence/scripts/patch_guard_block_termination.js").read_text()); sg.load()
except Exception as e:
print(f" r{r} guard err {e}", flush=True)
sc = s.create_script(MAIN_JS)
sc.on('message', on_main)
sc.load()
# 观察存活
t0 = time.time()
died_at = None
last_alive = True
while time.time() - t0 < MAX_SURVIVE:
time.sleep(3)
try:
alive = [p for p in d.enumerate_processes() if p.pid == pid]
except Exception:
break
if not alive:
died_at = time.time() - t0
break
survival = died_at if died_at else MAX_SURVIVE
got_dfp = any(e.get('cls')=='dfpReport' for e in dfp)
res = {"round": r, "survival_s": round(survival), "dfp_count": len([e for e in dfp if e.get('cls')=='dfpReport']), "got_dfp": got_dfp, "died": died_at is not None}
print(f"=> r{r}: 存活={survival:.0f}s dfpReport={res['dfp_count']} {'DIED' if died_at else 'alive'} ", flush=True)
try: d.kill(pid)
except: pass
return res
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
all_res = []
for r in range(1, ROUNDS+1):
try:
all_res.append(one_round(d, r))
except Exception as e:
print(f"r{r} ERROR {e}", flush=True)
all_res.append({"round": r, "error": str(e)})
time.sleep(2)
print("\n=== 汇总 ===", flush=True)
sur = [x.get('survival_s') for x in all_res if 'survival_s' in x]
dfp = [x for x in all_res if x.get('got_dfp')]
print(f"存活时长: {sur}", flush=True)
print(f"dfpReport 捕获成功率: {len(dfp)}/{ROUNDS}", flush=True)
json.dump(all_res, open(OUT, 'w'), indent=2)
print(f"saved {OUT}", flush=True)
if __name__ == "__main__":
main()
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""T0: 基础对照实验 (2026-08-27 重做, 不用旧结论).
T1: spawn + attach(空session) + resume → 观察 15s, 死因=完整logcat
T2: spawn + resume (不attach) → 对照
每轮输出: pid 时间线 + 死亡时 logcat(main/crash/events + kernel) 关键行。
判定死亡: 主进程不存在 或 pid 变化 (KeepAlive 重启也算死)。
"""
from pathlib import Path
import frida, time, json, subprocess, sys, re
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
OUT = Path("/tmp/t0_basic.json")
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def run_case(d, kind, rnd):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
adb("shell", "logcat", "-c")
adb("shell", "logcat", "-c", "-b", "crash")
pid = d.spawn([PACKAGE])
t0 = time.time()
session = None
if kind == "attach":
session = d.attach(pid)
d.resume(pid)
print(f"[T1.r{rnd}] spawn pid={pid} {'attach+resume' if kind=='attach' else '纯resume'} +{(time.time()-t0)*1000:.0f}ms", flush=True)
# 观察 15s
timeline = []
died_at = None
while time.time() - t0 < 15:
time.sleep(1.5)
mp = main_pid()
now = round(time.time() - t0, 1)
if mp is None:
died_at = now
timeline.append({"t": now, "main": None})
print(f" +{now}s 主进程消失 (死)", flush=True)
break
if mp != pid:
died_at = now
timeline.append({"t": now, "main": mp, "note": "pid变化"})
print(f" +{now}s pid变化 {pid}->{mp} (主进程死/重启)", flush=True)
break
timeline.append({"t": now, "main": mp})
# 抓日志
main_log = adb("shell", "logcat", "-d", "-t", "300").stdout
crash_log = adb("shell", "logcat", "-d", "-b", "crash", "-t", "100").stdout
# 关键筛选
keys = []
for line in (main_log.splitlines() + crash_log.splitlines()):
if re.search(r"msaoaid|nsdt|kist|guard|exit|kill|died|died|FATAL|Fatal|signal|Abort|SIGSEGV|SIGABRT|frida|avc:|SELinux|scudo|Suspicious|property|magisk|denied", line, re.I):
keys.append(line[:220])
print(f" 死亡={'是@'+str(died_at) if died_at else '否(存活)'} 关键日志行数={len(keys)}", flush=True)
for l in keys[-18:]:
print(f" L: {l}", flush=True)
try:
if session: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1)
return {"kind": kind, "round": rnd, "died_at": died_at, "timeline": timeline, "log_lines": keys[-40:]}
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
results = []
for rnd in (1, 2):
results.append(run_case(d, "attach", rnd))
results.append(run_case(d, "pure", 1))
OUT.write_text(json.dumps(results, ensure_ascii=False, indent=1))
print(f"[*] -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""T1: frida attach 痕迹快照 (自己的实验).
spawn -> attach(挂起) -> 加载【痕迹采集脚本】(只读, 不改任何东西) -> resume
采集(每~350ms):
- 进程线程名列表 (/proc/self/task/*/comm)
- /proc/self/maps 里含 frida/agent/re.linker/gum 的行
- /proc/self/fd 的 symlink (linjector/pipe/socket)
- 已加载模块含 frida/agent 的名字
目标: attach(不任何hook) 时进程内哪些痕迹可被 msaoaidsec 看到。
"""
from pathlib import Path
import frida, time, json, subprocess, sys
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
OUT = Path("/tmp/t1_traces.json")
TRACE_JS = r"""
'use strict';
function snap() {
const s = { t: Date.now(), threads: [], maps_marks: [], fds: [], modules: [] };
try {
for (const t of Process.enumerateThreads()) {
s.threads.push({ id: t.id, name: t.state == null ? '?' : t.name });
}
} catch (_) {}
try {
const maps = Process.enumerateMappings ? null : null;
} catch (_) {}
try {
for (const m of Process.enumerateModules()) {
if (/frida|agent|gum|re\.linker|linjector/i.test(m.name + m.path)) {
s.modules.push({ name: m.name, path: m.path, base: m.base.toString() });
}
}
} catch (_) {}
try {
const MEM = Process.findModuleByName('libc.so');
} catch (_) {}
// fd 列表: 用 openat 枚举 /proc/self/fd
try {
const dir = new File('/proc/self/fd', 'r');
const names = dir.read().toString().split(/\s+/);
for (const n of names) {
if (!/^\d+$/.test(n)) continue;
const link = new File('/proc/self/fd/' + n, 'r');
// can't readlink via File; use readlink via NativeFunction
}
} catch (_) {}
send(s);
}
try {
const readlink = new NativeFunction(Module.findExportByName('libc.so', 'readlink'), 'long', ['pointer', 'pointer', 'ulong']);
setInterval(() => {
const s = { t: Date.now(), threads: [], fds: [], modules: [], links: [] };
try {
for (const t of Process.enumerateThreads()) s.threads.push({ id: t.id, name: t.name });
} catch (_) {}
try {
for (const m of Process.enumerateModules()) {
if (/frida|agent|gum|re\.linker|linjector/i.test(m.name + m.path)) {
s.modules.push({ name: m.name, path: m.path });
}
}
} catch (_) {}
// fd symlinks
try {
const fp = new File('/proc/self/fd', 'r');
const buf = fp.read().toString();
for (const n of buf.split(/\s+/)) {
if (!/^\d+$/.test(n)) continue;
try {
const p = Memory.alloc(512);
const r = readlink('/proc/self/fd/' + n, p, 512);
if (r > 0) {
const link = p.readCString();
if (/frida|agent|linjector|pipe|socket|memfd/i.test(link)) s.links.push(n + ' -> ' + link);
}
} catch (_) {}
}
} catch (_) {}
send(s);
}, 350);
} catch (e) { send({ err: String(e) }); }
send({ boot: true });
"""
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
for rnd in (1, 2):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
pid = d.spawn([PACKAGE])
session = d.attach(pid)
t0 = time.time()
snaps = []
def on(m, dta):
if m.get('type') != 'send': return
p = m.get('payload') or {}
p['elapsed'] = round(time.time() - t0, 2)
if 'threads' in p or 'err' in p or 'boot' in p:
snaps.append(p)
sc = session.create_script(TRACE_JS)
sc.on('message', on)
sc.load()
d.resume(pid)
print(f"[T1.r{rnd}] spawn pid={pid} attach+采集脚本 resume +{(time.time()-t0)*1000:.0f}ms", flush=True)
# 观察直到死(最多15s)
died = None
while time.time() - t0 < 15:
time.sleep(1)
mp = main_pid()
if mp is None or mp != pid:
died = time.time() - t0
break
print(f"[T1.r{rnd}] 死@+{died if died else 15}s 快照数={len(snaps)}", flush=True)
if dies := (died or 15):
print(f" 最后2个快照: {json.dumps(snaps[-2:], ensure_ascii=False)[:900]}", flush=True)
try: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1)
OUT.write_text(json.dumps(snaps, ensure_ascii=False, indent=1))
print(f"[*] -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""T2: 拦截 msaoaidsec 的退出动作 (找到检测点 + 让App活下来).
spawn -> attach(挂起) -> 加载【exit拦截脚本】:
- Interceptor.replace libc _exit/exit/exit_group/_Exit/abort
- 若调用者 backtrace 含 libmsaoaidsec.so → 吞掉该退出(不真正退出), 记录 backtrace
- 其他来源(Java System.exit等) → 放行原函数
resume 后观察 App 是否存活 (若存活并能看到 msaoaidsec backtrace = 检测点找到+绕过成功)
"""
from pathlib import Path
import frida, time, json, subprocess, sys
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
OUT = Path("/tmp/t2_exit_block.json")
INTERCEPT_JS = r"""
'use strict';
const PACK = 'libmsaoaidsec.so';
let swallowed = 0, passthrough = 0;
const log = [];
function bt() {
const src = [];
try {
for (const a of Thread.backtrace(this.context, Backtracer.ACCURATE)) {
const m = Process.findModuleByAddress(a);
src.push(m ? (m.name === PACK ? a.sub(m.base).toString() : m.name) : a.toString());
}
} catch (_) {}
return src;
}
function make(name, retType, argTypes) {
try {
const addr = Module.findExportByName('libc.so', name);
if (addr === null) return;
const orig = new NativeFunction(addr, retType, argTypes);
Interceptor.replace(addr, new NativeCallback(function () {
const src = bt();
const fromMsao = src.some(s => typeof s === 'string' && s.startsWith('0x'));
if (fromMsao) {
swallowed++;
log.push({ fn: name, msao: true, src: src.slice(0, 8) });
send({ sw: name, n: swallowed });
return; // 吞掉 msaoaidsec 的退出
}
passthrough++;
send({ pass: name, src: src.slice(0, 6) });
return orig.apply(null, arguments);
}, retType, argTypes));
send({ installed: name });
} catch (e) { send({ err: name, e: String(e) }); }
}
make('_exit', 'void', ['int']);
make('_Exit', 'void', ['int']);
make('exit', 'void', ['int']);
make('exit_group', 'void', ['int']);
make('abort', 'void', []);
// 线程退出也要看 (pthread_exit 常用于静默死)
try {
const pe = Module.findExportByName('libc.so', 'pthread_exit');
const origPe = new NativeFunction(pe, 'void', ['pointer']);
Interceptor.replace(pe, new NativeCallback(function (retval) {
const src = bt();
if (src.some(s => typeof s === 'string' && s.startsWith('0x'))) {
swallowed++;
log.push({ fn: 'pthread_exit', msao: true, src: src.slice(0, 8) });
send({ sw: 'pthread_exit', n: swallowed });
return; // 吞掉
}
return origPe(retval);
}, 'void', ['pointer']));
send({ installed: 'pthread_exit' });
} catch (e) { send({ err: 'pthread_exit', e: String(e) }); }
send({ ready: true });
"""
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
for rnd in (1, 2, 3):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
adb("shell", "logcat", "-c")
pid = d.spawn([PACKAGE])
session = d.attach(pid)
t0 = time.time()
events = []
def on(m, dta):
if m.get('type') != 'send': return
p = m.get('payload') or {}
p['elapsed'] = round(time.time() - t0, 2)
events.append(p)
if 'sw' in p:
print(f" [+{p['elapsed']}s] 吞掉 msaoaidsec {p['sw']} (#{p['n']})", flush=True)
elif 'pass' in p:
print(f" [+{p['elapsed']}s] 放行 {p['pass']} src={p['src'][:4]}", flush=True)
elif 'installed' in p:
print(f" [+{p['elapsed']}s] hook {p['installed']}", flush=True)
sc = session.create_script(INTERCEPT_JS)
sc.on('message', on)
sc.load()
d.resume(pid)
print(f"[T2.r{rnd}] spawn pid={pid} + exit拦截, resume +{(time.time()-t0)*1000:.0f}ms", flush=True)
died = None
while time.time() - t0 < 20:
time.sleep(1.5)
mp = main_pid()
if mp is None or mp != pid:
died = time.time() - t0
break
# 抓 backtrace 详情
swallows = [e for e in events if 'sw' in e]
print(f"[T2.r{rnd}] 结果: {'死@+'+str(round(died,1))+'s' if died else '存活20s'} 吞掉={len(swallows)} 放行={sum(1 for e in events if 'pass' in e)}", flush=True)
# 打印最后几次吞掉的 backtrace (从 py 侧拿不到 log, 用 events 里的 sw 计数即可)
try: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1)
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
print(f"[*] -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""T3: 抓 msaoaidsec 的真实退出途径 (raw syscall / tgkill / kill).
T2 proved libc _exit/exit/abort/pthread_exit NOT used (0 calls, app still dies).
This experiment hooks:
- libc 'syscall' (可变参数) → 若 msaoaidsec 用 syscall(exit_group)
- libc 'tgkill' / 'tkill' / 'kill' → 若自杀信号
- libc 'raise'
- libc '_exit' _Exit exit exit_group (再拦一次确认)
每次命中打印 backtrace (库内偏移)。不做任何返回改写 (只读观察) —— 纯归因。
"""
from pathlib import Path
import frida, time, json, subprocess, sys
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
OUT = Path("/tmp/t3_exit_trace.json")
JS = r"""
'use strict';
const PACK = 'libmsaoaidsec.so';
function bt() {
const out = [];
try {
for (const a of Thread.backtrace(this.context, Backtracer.ACCURATE)) {
const m = Process.findModuleByAddress(a);
out.push(m ? (m.name === PACK ? 'MS:' + a.sub(m.base).toString() : m.name) : a.toString());
}
} catch (_) {}
return out.slice(0, 10);
}
function watch(name, nargs) {
try {
const addr = Module.findExportByName('libc.so', name);
if (addr === null) { send({ skip: name }); return; }
Interceptor.attach(addr, {
onEnter(args) {
const src = bt();
const ms = src.some(s => typeof s === 'string' && s.startsWith('MS:'));
const argv = [];
for (let i = 0; i < nargs; i++) { try { argv.push(args[i].toString()); } catch (_) { argv.push('?'); } }
send({ fn: name, ms, src, argv });
}
});
send({ hooked: name });
} catch (e) { send({ err: name, e: String(e) }); }
}
watch('syscall', 8);
watch('tgkill', 3);
watch('tkill', 2);
watch('kill', 2);
watch('raise', 1);
watch('_exit', 1);
watch('_Exit', 1);
watch('exit', 1);
watch('exit_group', 1);
watch('abort', 0);
send({ ready: true });
"""
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
for rnd in (1, 2, 3):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
adb("shell", "logcat", "-c")
pid = d.spawn([PACKAGE])
session = d.attach(pid)
t0 = time.time()
hits = []
def on(m, dta):
if m.get('type') != 'send': return
p = m.get('payload') or {}
p['elapsed'] = round(time.time() - t0, 3)
if 'fn' in p:
hits.append(p)
mark = 'MSAO!!' if p.get('ms') else ' '
print(f" [{mark} +{p['elapsed']:6.2f}s] {p['fn']} argv={p.get('argv')}", flush=True)
print(f" bt={p.get('src')}", flush=True)
elif 'hooked' in p:
pass
elif 'skip' in p:
print(f" [skip] {p['skip']}", flush=True)
sc = session.create_script(JS)
sc.on('message', on)
sc.load()
d.resume(pid)
print(f"[T3.r{rnd}] pid={pid} resume +{(time.time()-t0)*1000:.0f}ms", flush=True)
died = None
while time.time() - t0 < 15:
time.sleep(1)
mp = main_pid()
if mp is None or mp != pid:
died = time.time() - t0
break
print(f"[T3.r{rnd}] {'死@+'+str(round(died,1))+'s' if died else '存活'} 命中={len(hits)}", flush=True)
try: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1)
OUT.write_text(json.dumps(hits, ensure_ascii=False, indent=1))
print(f"[*] -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""T4 终局验证: 全吞退出函数 replace成no-op, 读LR抓调用者, 观察App是否存活.
T3 已证 _exit(0)/_Exit(0) 被调且 backtrace 空 (内联/清栈调用)。
T4: Interceptor.replace _exit/_Exit/exit/exit_group/abort 为 no-op (不真正退出)
同时 attach libc 'syscall' 打印 SYS_exit_group(94)/SYS_tgkill(131)/SYS_kill(129) 调用
并读 LR (return address) 找 msaoaidsec 偏移。
若 App 存活 >10s → 挡住退出=绕过成功; 若仍死 → 是 syscall/tgkill 自杀。
"""
from pathlib import Path
import frida, time, json, subprocess, sys
REMOTE = "127.0.0.1:31878"
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
OUT = Path("/tmp/t4_all_block.json")
JS = r"""
'use strict';
const PACK = 'libmsaoaidsec.so';
let nExit = 0, nSyscall = 0;
function where(addr) {
try {
const m = Process.findModuleByAddress(addr);
if (m === null) return addr.toString();
return m.name === PACK ? 'MS:' + addr.sub(m.base).toString() : m.name;
} catch (_) { return '?'; }
}
// 1) 全吞退出函数
for (const name of ['_exit', '_Exit', 'exit', 'exit_group', 'abort']) {
try {
const addr = Module.findExportByName('libc.so', name);
if (addr === null) { send({ skip: name }); continue; }
Interceptor.replace(addr, new NativeCallback(function () {
nExit++;
send({ exit: name, n: nExit, lr: where(this.context.lr) });
return; // no-op: 吞掉退出
}, 'void', name === 'abort' ? [] : ['int']));
send({ blocked: name });
} catch (e) { send({ err: name, e: String(e) }); }
}
// 2) 观察 syscall 的 exit_group/tgkill/kill
try {
const sc = Module.findExportByName('libc.so', 'syscall');
Interceptor.attach(sc, {
onEnter(args) {
const nr = args[0].toInt32() & 0xffffffff;
if ([94, 93, 131, 129].includes(nr)) {
nSyscall++;
send({ sc: nr, n: nSyscall, lr: where(this.context.lr), arg1: args[1].toString() });
}
}
});
send({ watch_syscall: true });
} catch (e) { send({ err: 'syscall', e: String(e) }); }
// 3) 观察 tgkill/tkill/kill/raise
for (const name of ['tgkill', 'tkill', 'kill', 'raise']) {
try {
const addr = Module.findExportByName('libc.so', name);
if (addr) Interceptor.attach(addr, { onEnter(args) { send({ kill: name, a0: args[0].toString(), a1: args[1].toString(), lr: where(this.context.lr) }); } });
} catch (_) {}
}
send({ ready: true });
"""
def sh(*a): return subprocess.run(a, capture_output=True, text=True)
def adb(*a): return sh(*ADB, *a)
def main_pid():
r = adb("shell", "pidof", PACKAGE)
for p in r.stdout.strip().split():
rr = adb("shell", "cat", f"/proc/{p}/cmdline")
if rr.stdout.strip("\x00").strip() == PACKAGE:
return int(p)
return None
def main():
d = frida.get_device_manager().add_remote_device(REMOTE)
for rnd in (1, 2, 3):
adb("shell", "am", "force-stop", PACKAGE)
time.sleep(1.5)
adb("shell", "logcat", "-c")
pid = d.spawn([PACKAGE])
session = d.attach(pid)
t0 = time.time()
events = []
def on(m, dta):
if m.get('type') != 'send': return
p = m.get('payload') or {}
p['elapsed'] = round(time.time() - t0, 3)
events.append(p)
if 'exit' in p or 'sc' in p or 'kill' in p:
line = f" [+{p['elapsed']:6.2f}s]"
if 'exit' in p: line += f" 退出被吞 {p['exit']}(#{p['n']}) lr={p['lr']}"
if 'sc' in p: line += f" syscall#{p['sc']} lr={p['lr']}"
if 'kill' in p: line += f" kill {p['kill']}({p['a0']},{p['a1']}) lr={p['lr']}"
print(line, flush=True)
sc = session.create_script(JS)
sc.on('message', on)
sc.load()
d.resume(pid)
print(f"[T4.r{rnd}] pid={pid} resume +{(time.time()-t0)*1000:.0f}ms", flush=True)
died = None
while time.time() - t0 < 15:
time.sleep(1)
mp = main_pid()
if mp is None or mp != pid:
died = time.time() - t0
break
print(f"[T4.r{rnd}] {'死@+'+str(round(died,1))+'s' if died else '存活15s'} exit吞={sum(1 for e in events if 'exit' in e)} syscall命中={sum(1 for e in events if 'sc' in e)}", flush=True)
try: session.detach()
except Exception: pass
try: d.kill(pid)
except Exception: pass
time.sleep(1)
OUT.write_text(json.dumps(events, ensure_ascii=False, indent=1))
print(f"[*] -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""干净验证: 模拟器 spawn + 挂起加载 bypass → resume → 确认存活并抓到 dfpReport。
打印每个 bypass 脚本的 event, 确认安装成功。
"""
from __future__ import annotations
import frida, time, json, subprocess
from pathlib import Path
REMOTE = "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/emu_clean_verify.json")
MAIN_JS = """
'use strict';
send({type:'armed'});
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function head(p,n){try{return p.readCString(n);}catch(e){return '';}}
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=head(a[1],Math.min(len,1500));
if(h.indexOf('dfpReport')>=0||h.indexOf('dckey')>=0){
send({cls:h.indexOf('dfpReport')>=0?'dfpReport':'dckey',len:len,hex:hexb(a[1],len)});
}
}});
});
send({type:'hooked'});
}catch(e){send({type:'err',e:String(e)});}
"""
def bypass_events(session, js_path, label):
"""加载 bypass 并捕获其 send(event) 消息, 返回是否成功安装."""
events = []
def on_msg(m, d):
if m.get('type') == 'send':
events.append(m.get('payload'))
elif m.get('type') == 'error':
events.append({'scripterr': str(m)[:120]})
try:
sc = session.create_script(js_path.read_text())
sc.on('message', on_msg)
sc.load()
time.sleep(0.15)
print(f" [{label}] events: {events}", flush=True)
return events
except Exception as e:
print(f" [{label}] load ERR: {e}", flush=True)
return [{'loaderr': str(e)[:120]}]
def main():
subprocess.run(["adb", "-s", "127.0.0.1:5555", "shell", "am", "force-stop", PACKAGE], capture_output=True)
time.sleep(1.5)
d = frida.get_device_manager().add_remote_device(REMOTE)
pid = d.spawn([PACKAGE])
print(f"[*] spawned pid={pid}", flush=True)
session = d.attach(pid)
print("[*] suspended, loading bypass (fast)...", flush=True)
e1 = bypass_events(session, RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js", "bypass_msaoaid")
e2 = bypass_events(session, RE / "evidence/scripts/mask_frida_maps_only.js", "mask_frida")
print("[*] resuming...", flush=True)
d.resume(pid)
ok = []
# patch_guard 在 resume 后加载
e3 = bypass_events(session, RE / "evidence/scripts/patch_guard_block_termination.js", "patch_guard")
# 主 hook
events = []
def on_main(m, dta):
if m.get('type') == 'error':
print("[JS-ERR]", str(m)[:120], flush=True); return
p = m.get('payload') or {}
t = p.get('type')
if t == 'armed': print("[*] armed", flush=True)
elif t == 'hooked': print("[*] SSL_write hooked", flush=True)
elif 'cls' in p:
print(f"[*] {p['cls']} len={p['len']}", flush=True)
events.append(p); OUT.write_text(json.dumps(events))
sc = session.create_script(MAIN_JS)
sc.on('message', on_main)
sc.load()
# 观察 25s: 存活 + 是否有 dfpReport
for t in [2, 4, 6, 8, 10, 15, 20, 25]:
time.sleep(t if t == 2 else t - prev)
prev = t
alive = [p for p in d.enumerate_processes() if p.pid == pid]
print(f" +{t}s alive={bool(alive)}", flush=True)
if not alive:
print("[*] *** App DEAD at +%ds ***" % t, flush=True); break
print(f"[*] final alive check: {bool([p for p in d.enumerate_processes() if p.pid==pid])}", flush=True)
print(f"[*] dfpReport captured: {len([e for e in events if e.get('cls')=='dfpReport'])}", flush=True)
try: d.kill(pid)
except: pass
if __name__ == "__main__":
main()
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""可靠进程存活监控: 以 adb pidof + /proc/<pid> 为唯一事实来源.
不用 frida enumerate_processes 判断存活 (有延迟/漏报, 且 spawn 后枚举不稳定)。
用法: python3 watch_proc.py <seconds> [label] -- 对当前 com.duowan.kiwi 进程做轮询
输出: 每 2s 一行 pid/状态; 结束时报告存活与否与死亡精确时长。
"""
import subprocess, sys, time
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
def adb(*a):
return subprocess.run(ADB + list(a), capture_output=True, text=True)
def pidof():
r = adb("shell", "pidof", PACKAGE)
out = r.stdout.strip()
return [int(x) for x in out.split()] if out else []
def proc_ok(pid):
r = adb("shell", "ls", f"/proc/{pid}/status")
return r.returncode == 0
def main():
seconds = int(sys.argv[1]) if len(sys.argv) > 1 else 60
label = sys.argv[2] if len(sys.argv) > 2 else ""
print(f"[watch:{label}] {seconds}s, 起点 pids={pidof()}", flush=True)
t0 = time.time()
seen = {}
last_alive = t0
last_dead = None
while time.time() - t0 < seconds:
pids = pidof()
if pids:
for p in pids:
seen.setdefault(p, time.time())
last_alive = time.time()
last_dead = None
print(f" +{time.time()-t0:5.1f}s ALIVE pids={pids}", flush=True)
else:
if last_dead is None:
last_dead = time.time()
print(f" +{time.time()-t0:5.1f}s DEAD (was alive={last_alive-t0:.1f}s)", flush=True)
time.sleep(2)
dur = time.time() - t0
final = pidof()
print(f"[watch:{label}] 结束: alive={bool(final)} final_pids={final} 存活时长={last_alive-t0:.1f}s 死亡时刻={last_dead-t0 if last_dead else '未死亡'} 总时长={dur:.1f}s", flush=True)
if __name__ == "__main__":
main()
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""可靠主进程监控 v2: 主进程 = cmdline 精确等于包名 (排除 :sub 子进程).
事实来源: adb /proc/<pid>/cmdline + pidof. 绝不使用 frida enumerate 判断存活.
用法: python3 watch_proc_v2.py <seconds> [label]
"""
import subprocess, sys, time
ADB = ["adb", "-s", "127.0.0.1:5555"]
PACKAGE = "com.duowan.kiwi"
def adb(*a):
return subprocess.run(ADB + list(a), capture_output=True, text=True)
def main_pid():
"""返回主进程 pid (cmdline==PACKAGE), 无则 None."""
r = adb("shell", "pidof", PACKAGE)
pids = r.stdout.strip().split()
for p in pids:
r2 = adb("shell", "cat", f"/proc/{p}/cmdline")
cmd = r2.stdout.strip("\x00").strip()
if cmd == PACKAGE:
return int(p)
return None
def main():
seconds = int(sys.argv[1]) if len(sys.argv) > 1 else 60
label = sys.argv[2] if len(sys.argv) > 2 else ""
t0 = time.time()
last_alive = 0.0
died_at = None
pid_history = {}
print(f"[watch:{label}] {seconds}s 起点 main_pid={main_pid()}", flush=True)
while time.time() - t0 < seconds:
p = main_pid()
now = time.time() - t0
if p is not None:
pid_history.setdefault(p, now)
last_alive = now
died_at = None
elif died_at is None:
died_at = now
print(f" +{now:5.1f}s DEAD (main) [存活到 +{last_alive:.1f}s]", flush=True)
time.sleep(1.5)
dur = time.time() - t0
p = main_pid()
print(f"[watch:{label}] END: alive={p is not None} pid={p} 最后存活时间=+{last_alive:.1f}s "
f"死亡时刻={round(died_at,1) if died_at else '未死'} pids_seen={list(pid_history.keys())} 总={dur:.1f}s", flush=True)
if __name__ == "__main__":
main()