docs(huya): 逆向定位 32hex hdid = NativeEntry.getGUID() + 真机注入通道证据

核心结论:
- 32hex hdid (WUP 登录帧 field1.tag0) = com.huya.security.hydeviceid.NativeEntry.getGUID()
- 金样本 ed0db8 为金样本设备 GUID; 当前真机 M2102J2SC getGUID()=0a7dfaa882938a6ab502511452142c57(稳定)
- NativeEntry 全字段: getGUID(32hex)/getCDID(40hex)/getHDID(40hex)/getMID(16hex)/getSDID(base64 safedeviceid)
- 单机不可铸造: 改 serialno/ANDROID_ID/删files/hydevice持久化 均不变(硬锚底层硬件)
- libhydeviceid.so 为 OLLVM+datadiv 壳; 用 dlopen->JNI_OnLoad->RegisterNatives(env表索引215) 绕开混淆

脚本: phone_probe_registernatives2(注册表)/phone_java_call_nativeentry(Java读getGUID)/phone_read_hdid(native hook)
      phone_dump_hydev(dump解密so)/phone_probe_login_frame(登录帧)/arm64_disasm(反汇编)
证据: hdid_regtable/java_nativeentry/hdid_read/login_frames/hdid_probe* + libhydeviceid_{original,dump}.so
This commit is contained in:
yml2213
2026-08-28 12:30:34 +08:00
parent 45b5e9128c
commit 0c6aff6a6d
23 changed files with 86238 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""通用 aarch64 反汇编工具: 分析内存 dump 的 libhydeviceid.so (解密后).
用法:
arm64_disasm.py <dump.so> <模块基址> <偏移hex> [长度字节]
例:
反汇编 JNI_OnLoad(0x22c4c8) 前 0x300 字节:
arm64_disasm.py evidence/diag_phone/libhydeviceid_dump.so 0x7814211000 0x22c4c8 0x300
打印所有 BL/BLR 分支目标到模块偏移.
"""
from __future__ import annotations
import sys
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_ARM
def load(fn):
with open(fn, "rb") as f:
return f.read()
def main():
dump_path = sys.argv[1]
base = int(sys.argv[2], 16)
start_off = int(sys.argv[3], 16) # 相对模块基址的偏移 (内存态文件里约等于文件偏移)
length = int(sys.argv[4], 16) if len(sys.argv) > 4 else 0x200
data = load(dump_path)
md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
md.detail = True
code = data[start_off:start_off + length]
print(f"# base=0x{base:x} start_off=0x{start_off:x} len=0x{length:x}")
for insn in md.disasm(code, base + start_off):
disp = ""
if insn.mnemonic.startswith(("b.", "b ")) or insn.mnemonic in ("bl", "br", "blr", "cbz", "cbnz"):
if insn.operands:
disp = f" ; -> 0x{insn.operands[0].imm:x}"
elif insn.mnemonic == "blr":
disp = " ; [indirect call]"
print(f" 0x{insn.address:x}\t{insn.mnemonic:8s} {insn.op_str}{disp}")
if __name__ == "__main__":
main()
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""真机 dump 解密后的 libhydeviceid.so (JNI_OnLoad 壳解密完成后).
用真机稳定注入通道 spawn+art_callsite, 等 libhydeviceid.so 加载且壳解密
(JNI_OnLoad 返回)后 Memory.dump 整个模块, 保存为本地 .so 供反汇编分析.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_dump_hydev.py [serial] [remote]
输出:
evidence/diag_phone/libhydeviceid_dump.so
"""
from __future__ import annotations
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "libhydeviceid_dump.so"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
JS = r"""
'use strict';
var dumped = false;
function tryDump(){
if(dumped) return;
var md = Process.findModuleByName('libhydeviceid.so');
if(!md) return;
dumped = true;
var ranges = md.enumerateRanges('r--');
send({type:'plan', count:ranges.length, modBase:''+md.base});
ranges.forEach(function(r, i){
try{
var buf = Memory.readByteArray(r.base, r.size);
send({type:'seg', i:i, n:ranges.length, off:''+r.base.sub(md.base), size:r.size}, buf);
}catch(e){ send({type:'seg-err', i:i, off:''+r.base.sub(md.base), e:String(e)}); }
});
send({type:'done', count:ranges.length});
}
setInterval(tryDump, 500);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
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)
session.on("detached", lambda r, dd: print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"got": 0, "expected": 0}
fh = OUT.open("wb")
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 == "plan":
result["expected"] = p["count"]
print(f"[*] 计划 dump {p['count']} 个 r-x 段", flush=True)
elif t == "seg":
off = int(p["off"], 16)
fh.seek(off)
fh.write(data)
result["got"] += 1
print(f"[*] seg {p['i']+1}/{p['n']} off=0x{p['off']} size={p['size']}", flush=True)
elif t == "done":
print(f"[*] dump done, got {result['got']}/{result['expected']}", flush=True)
elif t == "seg-err":
print(f"[seg-err] {p}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed, waiting dump...", flush=True)
t0 = time.time()
while (result["got"] < result["expected"] or result["expected"] == 0) and time.time() - t0 < 20:
time.sleep(0.5)
fh.close()
OUT.chmod(0o644)
print(f"[*] done got={result['got']}/{result['expected']} -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""真机: 主动 Java 调用 NativeEntry 的 getGUID/getMID/getCDID/getSDID/getHDID, 找 32hex hdid(field1.tag0).
hypasswordLogin 登录帧 field1.tag0 的 32hex hdid 未在 SSL 帧/其他 get* 中抓到,
最可能是 getGUID()/getMID()(之前未触发) 的返回值。本脚本在 Java VM 就绪后
用 Java.use 主动调用全部 get 方法并打印, 确定 32hex hdid 的来源方法。
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_java_call_nativeentry.py [serial] [remote]
输出:
evidence/diag_phone/java_nativeentry.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "java_nativeentry.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
JS = r"""
'use strict';
setTimeout(function(){
Java.perform(function(){
var out = {};
try{
var NE = Java.use('com.huya.security.hydeviceid.NativeEntry');
send({type:'class-ok', cls:'com.huya.security.hydeviceid.NativeEntry'});
['init','getGUID','getMID','getCDID','getSDID','getHDID'].forEach(function(m){
try{ out[m] = NE[m](); }catch(e){ out[m] = '<ERR:'+String(e).slice(0,50)+'>'; }
send({type:'val', method:m, value:out[m]});
});
send({type:'all', out:out});
}catch(e){
send({type:'use-err', e:String(e).slice(0,200)});
}
});
}, 6000);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
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)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"vals": {}, "detached": det}
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 == "val":
result["vals"][p["method"]] = p["value"]
print(f"[val] {p['method']} = {p['value']}", flush=True)
elif t == "class-ok":
print(f"[*] class found", flush=True)
elif t == "all":
print(f"[*] 全部读取完成", flush=True)
elif t in ("use-err",):
print(f"[!] use-err: {p.get('e')}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(20)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] saved {OUT}")
if __name__ == "__main__":
main()
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""真机探针: 枚举 libhydeviceid.so / libudbauthunify.so 导出符号, 定位 32hex hdid 生成入口.
用真机稳定注入通道 (spawn + art_callsite bypass + resume) 长时存活。
App 启动即加载这两个 so, 枚举 exports 拿函数名+基址偏移, 为下一步
针对性 hook 生成函数(抓输入)做准备。
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_hdid.py [serial] [remote]
输出:
evidence/diag_phone/hdid_probe_exports.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "hdid_probe_exports.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
PROBE_JS = r"""
'use strict';
function listExports(modName, tag){
try{
var md = Process.findModuleByName(modName);
if(!md){ send({type:'mod', name:modName, state:'not-loaded'}); return false; }
var ex = md.enumerateExports();
var sym = md.enumerateSymbols();
send({type:'mod', name:modName, base:''+md.base, size:md.size,
exports: ex.map(function(e){return {name:e.name, type:e.type, off:''+e.address.sub(md.base)};}),
symCount: sym.length,
syms: sym.map(function(s){return {name:s.name, off:''+s.address.sub(md.base)};})});
return true;
}catch(e){ send({type:'err', name:modName, e:String(e)}); return false; }
}
function hookPropGet(){
var t = Module.findExportByName('libc.so','__system_property_get');
if(!t) return;
Interceptor.attach(t,{
onEnter:function(a){ this.k = a[0].readCString()||''; },
onLeave:function(ret){
try{ send({type:'prop', k:this.k, v:(this.ctx.x1.readCString()||'')}); }catch(e){}
}
});
send({type:'propget-hooked'});
}
// App 启动早期就抓 libhydeviceid.so 加载后的符号 + 派生属性读取
setTimeout(function(){ listExports('libhydeviceid.so','hydev'); }, 2000);
setTimeout(function(){ listExports('libudbauthunify.so','udb'); }, 2000);
setTimeout(hookPropGet, 2000);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
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)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
d.resume(pid)
print("[*] bypass loaded + resumed", flush=True)
result = {"modules": {}, "props": []}
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 == "mod":
result["modules"][p["name"]] = p
print(f"[mod] {p['name']} base={p.get('base')} exports={len(p.get('exports') or [])} syms={p.get('symCount')}", flush=True)
elif t == "prop":
result["props"].append({"k": p.get("k"), "v": p.get("v")})
elif t == "propget-hooked":
print("[*] __system_property_get hooked", flush=True)
elif t == "err":
print(f"[err] {p.get('name')}: {p.get('e')}", flush=True)
sc = session.create_script(PROBE_JS)
sc.on("message", on_message)
sc.load()
print("[*] probe loaded, collecting 12s", flush=True)
time.sleep(12)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
# 打印关键: 各 so 的导出符号名(去重)
for name, m in result["modules"].items():
print(f"\n=== {name} exports ({len(m.get('exports') or [])}) ===")
for e in (m.get("exports") or [])[:60]:
print(f" 0x{e['off']} {e['name']}")
print("\n=== 派生属性 (去重) ===")
seen = {}
for pr in result["props"]:
seen[pr["k"]] = pr["v"]
for k, v in seen.items():
print(f" {k} = {v}")
print(f"\n[*] saved {OUT}")
if __name__ == "__main__":
main()
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""真机探针 v3: 等 so 加载后 hook, 抓 32hex hdid 生成/使用链路 + 登录触发.
修正 v1/v2: 挂起时 libhydeviceid.so 尚未加载 -> 用 setInterval 轮询,
so 一加载即 hook:
- libhydeviceid.so: 枚举非 runtime 导出 + hook JNI_OnLoad(0x22c4c8)
- libudbauthunify.so: hook BusinessCfg::getHdid(0x26a484) / setSafeDeviceId(0x26a2e0) 带调用栈
resume 后 am start GameSdkLoginActivity 触发设备上报/登录, 让 hdid 被读取/生成.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_hdid3.py [serial] [remote]
输出:
evidence/diag_phone/hdid_probe3.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
LOGIN_ACT = "com.duowan.kiwi/.loginui.impl.gamesdk.GameSdkLoginActivity"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "hdid_probe3.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
JS = r"""
'use strict';
function stdstr(p){
try{
if(p.isNull()) return '';
var first = p.readU8();
if((first&1)===0){ var l=first>>1; return l? p.add(1).readUtf8String(l):''; }
else { var d=p.readPointer(); var l=p.add(8).readU64(); return (l&&l<256)? d.readUtf8String(l):''; }
}catch(e){ return ''; }
}
var hydev=null, udb=null;
function hookHydev(){
if(hydev) return;
var md = Process.findModuleByName('libhydeviceid.so');
if(!md) return;
hydev = md;
send({type:'hydev', base:''+md.base, size:md.size});
var ex = md.enumerateExports();
var useful = ex.filter(function(e){ return !/^_Z/.test(e.name) && !/^__/.test(e.name) && e.name!==''; });
send({type:'hydev-exports', count:useful.length,
exports: useful.map(function(e){ return {off:''+e.address.sub(md.base), name:e.name}; })});
try{
Interceptor.attach(md.base.add(0x22c4c8), {
onEnter:function(){ send({type:'jni-onload-enter', tid:Process.getCurrentThreadId()}); },
onLeave:function(ret){ send({type:'jni-onload-leave', ret:''+ret}); }
});
send({type:'jni-onload-hooked'});
}catch(e){ send({type:'hydev-err', e:String(e)}); }
}
function hookUdb(){
if(udb) return;
var md = Process.findModuleByName('libudbauthunify.so');
if(!md) return;
udb = md;
send({type:'udb', base:''+md.base});
try{
Interceptor.attach(md.base.add(0x26a484), {
onLeave:function(ret){ send({type:'hdid', str:stdstr(ret), tid:Process.getCurrentThreadId()}); }
});
send({type:'hdid-hooked'});
}catch(e){ send({type:'udb-err', e:String(e)}); }
try{
Interceptor.attach(md.base.add(0x26a2e0), {
onEnter:function(a){
send({type:'setsd', sd:stdstr(a[1]), hd:stdstr(a[2]), tid:Process.getCurrentThreadId()});
try{
var bt=Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0,20).map(function(x){return x.toString();});
send({type:'bt', bt:bt});
}catch(e){}
}
});
send({type:'setsd-hooked'});
}catch(e){}
}
setInterval(hookHydev, 40);
setInterval(hookUdb, 40);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
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)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"events": [], "detached": det}
def on_message(m, data):
if m.get("type") == "error":
print("[JS-ERR]", str(m)[:200], flush=True); return
p = m.get("payload") or {}
result["events"].append(p)
t = p.get("type")
if t in ("hdid", "setsd"):
print(f"[{t}] {p.get('hd') or p.get('str')} tid={p.get('tid')}", flush=True)
elif t == "bt":
print(" bt:", " <- ".join(p["bt"][:8]), flush=True)
elif t == "hydev":
print(f"[*] libhydeviceid loaded base={p.get('base')}", flush=True)
elif t == "hydev-exports":
print(f"[*] hydev 非runtime导出 {p.get('count')}:", flush=True)
for e in (p.get("exports") or [])[:40]:
print(f" 0x{e['off']} {e['name']}")
elif t in ("udb","hdid-hooked","setsd-hooked","jni-onload-hooked","jni-onload-enter","jni-onload-leave"):
print(f"[*] {t}", p if t in ("jni-onload-leave",) else "", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(5)
print("[*] 触发登录 Activity...", flush=True)
adb("shell", "am", "start", "-n", LOGIN_ACT)
time.sleep(15)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] saved {OUT}")
if __name__ == "__main__":
main()
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""真机探针 v4: 抓 libhydeviceid.so 生成 hdid 的输入(系统属性/文件/随机源).
libhydeviceid.so 加壳, 生成函数不导出。改为抓它的"输入":
- hook libc __system_property_get (spawn 挂起时已加载) -> App 启动读的属性
- hook libhydeviceid 的 hy_fopen64/hy_read/hy_syscall (壳文件包装) -> 访问的文件
- hook getHdid(0x26a484) 时同时 dump x0 原始字节, 定位为何返回空
这能判断 hdid 依赖 ro.serialno / ANDROID_ID / /proc 信息 / 随机源等, 决定能否复现.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_hdid4.py [serial] [remote]
输出:
evidence/diag_phone/hdid_probe4.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
LOGIN_ACT = "com.duowan.kiwi/.loginui.impl.gamesdk.GameSdkLoginActivity"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "hdid_probe4.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
JS = r"""
'use strict';
function stdstr(p){
try{
if(p.isNull()) return '';
var first = p.readU8();
if((first&1)===0){ var l=first>>1; return l? p.add(1).readUtf8String(l):''; }
else { var d=p.readPointer(); var l=p.add(8).readU64(); return (l&&l<512)? d.readUtf8String(l):''; }
}catch(e){ return ''; }
}
// 1) spawn 挂起时 hook __system_property_get (libc 已加载)
(function(){
try{
var t = Module.findExportByName('libc.so','__system_property_get');
if(!t){ send({type:'no-propget'}); return; }
Interceptor.attach(t,{
onEnter:function(a){ this.k = a[0].readCString()||''; this.vp = a[1]; },
onLeave:function(ret){ try{ send({type:'prop', k:this.k, v:this.vp.readCString()||''}); }catch(e){ send({type:'prop', k:this.k, v:'<err>'}); } }
});
send({type:'propget-hooked'});
}catch(e){ send({type:'propget-err', e:String(e)}); }
})();
var hydev=null;
function hookHydev(){
if(hydev) return;
var md = Process.findModuleByName('libhydeviceid.so');
if(!md) return;
hydev = md;
send({type:'hydev', base:''+md.base});
// 文件操作壳包装: hy_fopen64(0xf1298), hy_read(0x30bef0), hy_syscall(0x30bf3c)
var targets = {0xf1298:'hy_fopen64', 0x30bef0:'hy_read', 0x30bf3c:'hy_syscall', 0x30bf00:'hy_write', 0xf3360:'hyftell'};
Object.keys(targets).forEach(function(off){
try{
Interceptor.attach(md.base.add(parseInt(off,16)), {
onEnter:function(a){
var s='';
try{ s = (a[0]&&!a[0].isNull()) ? a[0].readCString(256)||'' : ''; }catch(e){}
send({type:'hyio', fn:targets[off], arg0:s, tid:Process.getCurrentThreadId()});
}
});
}catch(e){}
});
send({type:'hyio-hooked'});
}
// getHdid 返回空排查: dump x0 原始字节
function hookUdb(){
var md = Process.findModuleByName('libudbauthunify.so');
if(!md) return;
try{
Interceptor.attach(md.base.add(0x26a484), {
onLeave:function(ret){
var s=stdstr(ret);
var hex='';
try{ hex = Array.from(new Uint8Array(ret.readByteArray(64))).map(function(b){return ('0'+b.toString(16)).slice(-2);}).join(''); }catch(e){}
send({type:'hdid', str:s, ret:''+ret, hex:hex, tid:Process.getCurrentThreadId()});
}
});
send({type:'hdid-hooked'});
}catch(e){ send({type:'udb-err', e:String(e)}); }
}
setInterval(hookHydev, 40);
setInterval(hookUdb, 40);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
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)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"props": [], "hyio": [], "hdid": [], "detached": det}
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 == "prop":
result["props"].append(p); print(f"[prop] {p.get('k')} = {p.get('v')}", flush=True)
elif t == "hyio":
result["hyio"].append(p); print(f"[hyio] {p.get('fn')} arg0={p.get('arg0')}", flush=True)
elif t == "hdid":
result["hdid"].append(p); print(f"[hdid] str={p.get('str')!r} ret={p.get('ret')} hex={p.get('hex')}", flush=True)
elif t in ("propget-hooked","hydev","hyio-hooked","hdid-hooked","no-propget"):
print(f"[*] {t}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(4)
print("[*] 触发登录 Activity...", flush=True)
adb("shell", "am", "start", "-n", LOGIN_ACT)
time.sleep(15)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] saved {OUT}")
if __name__ == "__main__":
main()
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""真机 v6: hook SSL_write 抓 hypasswordLogin 登录帧, 提取实际 hdid + 追来源.
目标: 确认登录帧 t1.t0 的 32hex hdid 实际值(应为金样本 ed0db8...),
并 hook 它进入 WUP 帧前被读取的调用链。同时 hook:
- libc strstr/strcpy 等 (登录帧组装时 hdid 字符串被拷贝处)
- BusinessCfg::getSafeDeviceId(0x26a3c4) / setDeviceInfo(0x26a894)
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_hdid6.py [serial] [remote]
输出:
evidence/diag_phone/hdid_probe6.json
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
LOGIN_ACT = "com.duowan.kiwi/.loginui.impl.gamesdk.GameSdkLoginActivity"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "hdid_probe6.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
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 stdstr(p){
try{
if(p.isNull()) return '';
var f = p.readU8();
if((f&1)===0){ var l=f>>1; return l? p.add(1).readUtf8String(l):''; }
else { var d=p.readPointer(); var l=p.add(8).readU64(); return (l&&l<512)? d.readUtf8String(l):''; }
}catch(e){ return ''; }
}
var sslDone=false, udbHooked=false;
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=''; try{ h=a[1].readCString(Math.min(len,600))||''; }catch(e){}
if(h.indexOf('hypasswordLogin')>=0){
send({type:'wup_login', len:len, hex:hexb(a[1],len)});
}
}});
});
send({type:'sslwrite-hooked'});
}catch(e){}
sslDone=true;
}
function hookUdb(){
if(udbHooked) return;
var md = Process.findModuleByName('libudbauthunify.so');
if(!md) return;
udbHooked=true;
send({type:'udb', base:''+md.base});
// 设备信息读取点
try{
Interceptor.attach(md.base.add(0x26a3c4), { // getSafeDeviceId
onLeave:function(ret){ send({type:'getsd', ret:''+ret}); }
});
}catch(e){}
try{
Interceptor.attach(md.base.add(0x26a894), { // setDeviceInfo(a1=this, a2..a5=std::string*)
onEnter:function(a){
send({type:'setdi', args:[stdstr(a[1]), stdstr(a[2]), stdstr(a[3]), stdstr(a[4])]});
}
});
}catch(e){}
send({type:'udb-hooked'});
}
// libhydeviceid 导出函数调用探测
var hyTried=false;
function hookHy(){
if(hyTried) return;
var md = Process.findModuleByName('libhydeviceid.so');
if(!md) return;
hyTried=true;
send({type:'hydev', base:''+md.base});
['hyfopen64','hy_read','hy_syscall','hy_write','hyftell'].forEach(function(n){
try{
var a = Module.findExportByName('libhydeviceid.so', n);
if(a) Interceptor.attach(a,{onEnter:function(args){
var s=''; try{ s=args[0].readCString(128)||''; }catch(e){}
send({type:'hycall', fn:n, a0:s, tid:Process.getCurrentThreadId()});
}});
}catch(e){}
});
send({type:'hydev-hooked'});
}
setInterval(hookSSL, 800);
setInterval(hookUdb, 50);
setInterval(hookHy, 50);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
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)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"frames": [], "events": [], "detached": det}
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")
result["events"].append(p)
if t == "wup_login":
result["frames"].append(p)
b = bytes.fromhex(p.get("hex", ""))
# 提取帧里的 32hex hdid
h32 = re.findall(rb"[0-9a-f]{32}", b)
print(f"[WUP_LOGIN] len={p.get('len')} hdid候选: {[h.decode() for h in h32[:3]]}", flush=True)
elif t in ("udb","udb-hooked","sslwrite-hooked","hydev","hydev-hooked"):
print(f"[*] {t}", p if t=="udb" else "", flush=True)
elif t == "setdi":
print(f"[setDeviceInfo] args={p.get('args')}", flush=True)
elif t == "getsd":
print(f"[getSafeDeviceId] ret={p.get('ret')}", flush=True)
elif t == "hycall":
print(f"[hycall] {p.get('fn')} a0={p.get('a0')}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(4)
print("[*] 触发登录...", flush=True)
adb("shell", "am", "start", "-n", LOGIN_ACT)
time.sleep(18)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] 登录帧数: {len(result['frames'])} saved {OUT}")
if __name__ == "__main__":
main()
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""真机 v9: 触发一键登录, hook SSL_write 抓 WUP 登录帧 + createWupDeviceInfo(0x2746a0),
从登录帧提取 32hex hdid (t1.t0) 和其来源.
触发: 点击 GameSdkLoginActivity 的"本机号码一键登录" (mBtnLogin, 中心 540,1287),
App 会自动发起登录 WUP 请求, 帧内经 JCE 编码含 t1.t0=hdid 字段.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_login_frame.py [serial] [remote]
输出:
evidence/diag_phone/login_frames.json
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
LOGIN_ACT = "com.duowan.kiwi/.loginui.impl.gamesdk.GameSdkLoginActivity"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "login_frames.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
JS = r"""
'use strict';
var sslDone=false, udbHooked=false, cdiHooked=false;
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<200||len>80000) return;
try{
var buf=a[1].readByteArray(len);
send({type:'sslfr', len:len, hex:Array.from(new Uint8Array(buf)).map(function(b){return ('0'+b.toString(16)).slice(-2);}).join('')});
}catch(e){}
}});
});
send({type:'ssl-hooked'});
}catch(e){ send({type:'ssl-err', e:String(e)}); }
sslDone=true;
}
function hookUdb(){
if(udbHooked) return;
var md=Process.findModuleByName('libudbauthunify.so');
if(!md) return;
udbHooked=true;
send({type:'udb', base:''+md.base});
try{ // BusinessCfg::getHdid
Interceptor.attach(md.base.add(0x26a484),{onLeave:function(ret){
var s='<n>';
try{ s = ret.isNull()? '<null>' : ret.readCString(64)||'<?>'; }catch(e){ s='<err>'; }
var bt='';
try{ bt = Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0,16).map(function(x){return x.toString();}).join(' <- '); }catch(e){}
send({type:'getHdid', ret:''+ret, str:s, bt:bt});
}});
}catch(e){}
try{ // createWupDeviceInfo(wup::DeviceInfo*)
Interceptor.attach(md.base.add(0x2746a0),{onEnter:function(a){ send({type:'createWupDev-enter', tid:Process.getCurrentThreadId()}); }});
}catch(e){}
send({type:'udb-hooked'});
}
setInterval(hookSSL, 500);
setInterval(hookUdb, 60);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def tap(x, y):
adb("shell", f"input tap {x} {y}")
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
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)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"frames": [], "events": [], "detached": det}
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")
result["events"].append(p)
if t == "sslfr":
result["frames"].append(p)
b = bytes.fromhex(p.get("hex", ""))
# WUP/JCE 帧特征: 找 t1.t0 的 32hex 候选
h32 = list(set(x.decode() for x in re.findall(rb"[0-9a-f]{32}", b)))
h40 = list(set(x.decode() for x in re.findall(rb"[0-9a-f]{40}", b)))
print(f"[ssl] len={p['len']} 32hex候选={h32[:4]} 40hex候选={h40[:2]}", flush=True)
elif t in ("udb","udb-hooked","ssl-hooked"):
print(f"[*] {t}", p if t=="udb" else "", flush=True)
elif t == "getHdid":
print(f"[getHdid] ret={p.get('ret')} str={p.get('str')[:60]!r}", flush=True)
if p.get("bt"):
print(f" bt: {p['bt']}", flush=True)
elif t == "createWupDev-enter":
print(f"[createWupDeviceInfo] enter tid={p.get('tid')}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(6)
# 打开登录页 (不自动点击, 交给用户手动切账号密码登录)
adb("shell", "am", "start", "-n", LOGIN_ACT)
time.sleep(3)
duration = int(sys.argv[3]) if len(sys.argv) > 3 else 240
print(f"[*] 探针持续抓帧 {duration}s, 请手动登录...", flush=True)
time.sleep(duration)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] 帧数: {len(result['frames'])} saved {OUT}")
if __name__ == "__main__":
main()
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""真机探针 v2: 抓 libhydeviceid.so 经 RegisterNatives 注册的 native 方法 + getHdid 调用栈.
libhydeviceid.so 没有 JNI/生成函数导出(核心方法经 JNI_OnLoad -> RegisterNatives 动态注册)。
本脚本在 spawn 挂起中 hook libart.so 的 RegisterNatives, App 启动早期注册会被捕获,
拿到 libhydeviceid.so 注册的 native 方法指针 -> 基址偏移, 定位 hdid 生成入口。
同时 hook libudbauthunify.so 的 BusinessCfg::getHdid(0x26a484) 抓 hdid 值+调用栈。
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_registernatives.py [serial] [remote]
输出:
evidence/diag_phone/hdid_registernatives.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "hdid_registernatives.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
JS = r"""
'use strict';
function stdstr(p){
try{
if(p.isNull()) return '';
var first = p.readU8();
if((first & 1)===0){ var l=first>>1; return l? p.add(1).readUtf8String(l):''; }
else { var d=p.readPointer(); var l=p.add(8).readU64(); return (l&&l<256)? d.readUtf8String(l):''; }
}catch(e){ return ''; }
}
function modOf(a){ try{ var m=Process.findModuleByAddress(a); return m? {n:m.name, off:''+a.sub(m.base)}: {n:'?', off:'?'}; }catch(e){ return {n:'?',off:'?'}; } }
// 1) hook libart RegisterNatives
(function(){
var RN = Module.findExportByName('libart.so','_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi');
if(!RN){ send({type:'rn-notfound'}); return; }
send({type:'rn-found', a:''+RN});
Interceptor.attach(RN,{
onEnter:function(a){
var count = a[3].toInt32();
if(count<=0||count>500) return;
var cls='';
try{ cls = Java.vm.tryGetEnv().getClassName(a[1])||''; }catch(e){ cls='?'; }
for(var i=0;i<count;i++){
try{
var base=a[2].add(i*24);
var name=base.readPointer().readCString()||'';
var sig =base.add(8).readPointer().readCString()||'';
var fn =base.add(16).readPointer();
var mo=modOf(fn);
send({type:'reg', cls:cls, name:name, sig:sig, mod:mo.n, off:mo.off});
}catch(e){}
}
}
});
send({type:'rn-hooked'});
})();
// 2) hook BusinessCfg::getHdid (libudbauthunify.so 0x26a484)
(function(){
var md = Process.findModuleByName('libudbauthunify.so');
if(!md){ send({type:'udb-not-loaded'}); return; }
try{
Interceptor.attach(md.base.add(0x26a484),{
onLeave:function(ret){
var s=stdstr(ret);
send({type:'hdid', str:s, tid:Process.getCurrentThreadId()});
}
});
Interceptor.attach(md.base.add(0x26a2e0),{ // setSafeDeviceId(a2=sd, a3=hdid)
onEnter:function(a){
send({type:'setsd', sd:stdstr(a[1]), hd:stdstr(a[2]), tid:Process.getCurrentThreadId()});
try{
var bt=Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0,24).map(function(x){return x.toString();});
send({type:'bt', bt:bt});
}catch(e){}
}
});
send({type:'udb-hooked'});
}catch(e){ send({type:'udb-err', e:String(e)}); }
})();
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
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)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
print("[*] art_callsite loaded", flush=True)
result = {"regs": [], "hdid": [], "detached": det}
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 == "reg":
result["regs"].append(p)
elif t in ("hdid", "setsd"):
result["hdid"].append(p)
print(f"[{t}] {p}", flush=True)
elif t == "bt":
print(" bt:", " <- ".join(p["bt"][:8]), flush=True)
elif t in ("rn-found","rn-hooked","udb-hooked"):
print(f"[*] {t}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed, collecting 18s", flush=True)
time.sleep(18)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
# 汇总 libhydeviceid.so 注册的方法
print("\n=== libhydeviceid.so 注册的 native 方法 ===")
seen = set()
for r in result["regs"]:
if r.get("mod") == "libhydeviceid.so" and (r["name"], r["off"]) not in seen:
seen.add((r["name"], r["off"]))
print(f" 0x{r['off']} {r['name']}{r['sig']} (class={r.get('cls')})")
print(f"\n=== 所有 so 注册统计 ===")
from collections import Counter
c = Counter(r.get("mod") for r in result["regs"])
for mod, n in c.most_common():
print(f" {mod}: {n}")
print(f"\n[*] saved {OUT}")
if __name__ == "__main__":
main()
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""真机 v7: dlopen 拦截 libhydeviceid -> attach JNI_OnLoad -> hook JNINativeInterface::RegisterNatives
拿到 libhydeviceid.so 动态注册的全部 native 方法(名字+签名+函数地址).
绕过 OLLVM 混淆: 不去静态反汇编 JNI_OnLoad, 而是运行时在 JNI_OnLoad 里用 env 的
JNINativeInterface 函数表定位 RegisterNatives 槽位并 hook, 抓 JNINativeMethod 数组.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_registernatives2.py [serial] [remote]
输出:
evidence/diag_phone/hdid_regtable.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "hdid_regtable.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
# JNINativeInterface 中 RegisterNatives 的索引 (JNI 1.6 规范, 含 4 个 reserved)
RN_INDEX = 215
def mod_of(a):
try:
m = Process.findModuleByAddress(a)
return (m.name, str(a.sub(m.base))) if m else ("?", "?")
except Exception:
return ("?", "?")
JS = r"""
'use strict';
function jmod(a){
try{ var m = Process.findModuleByAddress(a); return m ? {n:m.name, off:''+a.sub(m.base)} : {n:'?',off:'?'}; }catch(e){ return {n:'?',off:'?'}; }
}
var hooked = false;
function attachJniOnload(md){
try{
Interceptor.attach(md.base.add(0x22c4c8), {
onEnter:function(a){
send({type:'jni-onload-enter', tid:Process.getCurrentThreadId()});
try{
var env = Java.vm.tryGetEnv();
var fns = env.handle.readPointer(); // JNINativeInterface*
// 打印 200..230 槽位的所属模块, 验证表结构
var dump=[];
for(var i=200;i<=230;i++){
var p = fns.add(i*8).readPointer();
var m = jmod(p);
dump.push({i:i, a:''+p, mod:m.n, off:m.off});
}
send({type:'fns-scan', dump:dump});
var rn = fns.add(%d*8).readPointer();
send({type:'rn-addr', rn:''+rn, mod:jmod(rn)});
Interceptor.attach(rn, {
onEnter:function(args){
// args[1]=jclass clazz, args[2]=JNINativeMethod* methods, args[3]=jint n
var count = args[3].toInt32();
var methods = args[2];
var cls='';
try{ cls = Java.vm.tryGetEnv().getClassName(args[1]||Java.vm.tryGetEnv().getClass(args[1])); }catch(e){ cls='?'; }
send({type:'reg-call', cls:cls, count:count, tid:Process.getCurrentThreadId()});
for(var i=0;i<count;i++){
try{
var base = methods.add(i*24);
var name = base.readPointer().readCString()||'';
var sig = base.add(8).readPointer().readCString()||'';
var fn = base.add(16).readPointer();
var m = jmod(fn);
send({type:'reg', cls:cls, name:name, sig:sig, mod:m.n, off:m.off});
}catch(e){}
}
}
});
send({type:'rn-hooked'});
}catch(e){ send({type:'rn-err', e:String(e)}); }
}
});
send({type:'jni-onload-hooked'});
}catch(e){ send({type:'jni-err', e:String(e)}); }
}
function hookDlopen(){
if(hooked) return;
var dl = Module.findExportByName(null,'android_dlopen_ext');
var dlname = 'android_dlopen_ext';
if(!dl){ dl = Module.findExportByName(null,'dlopen'); dlname='dlopen'; }
if(!dl) return;
hooked = true;
send({type:'dlopen-hooked', fn:dlname});
Interceptor.attach(dl, {
onEnter:function(a){ try{ this.name = a[0].readCString()||''; }catch(e){ this.name=''; } },
onLeave:function(){
if(this.name && this.name.indexOf('libhydeviceid')>=0){
send({type:'dlopen-hydev', name:this.name});
var md = Process.findModuleByName('libhydeviceid.so');
if(md){ attachJniOnload(md); }
}
}
});
}
setInterval(hookDlopen, 40);
""" % RN_INDEX
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
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)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"regs": [], "fns_scan": [], "events": [], "detached": det}
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")
result["events"].append(p)
if t == "reg":
result["regs"].append(p)
print(f"[reg] {p.get('name')}{p.get('sig')} @ {p.get('mod')}+0x{p.get('off')} (class={p.get('cls')})", flush=True)
elif t == "reg-call":
print(f"[*] RegisterNatives called count={p.get('count')} cls={p.get('cls')}", flush=True)
elif t == "rn-addr":
print(f"[*] RegisterNatives addr 0x{p['rn']} mod={p.get('mod')}", flush=True)
elif t == "fns-scan":
result["fns_scan"].append(p)
print("[*] JNINativeInterface 槽位 200-230:", flush=True)
for e in p["dump"]:
print(f" [{e['i']}] {e.get('mod')}+0x{e.get('off')}", flush=True)
elif t in ("dlopen-hooked","jni-onload-hooked","rn-hooked","dlopen-hydev","jni-onload-enter"):
print(f"[*] {t}", p if t=="dlopen-hydev" else "", flush=True)
elif t in ("jni-err","rn-err"):
print(f"[!] {t}: {p.get('e')}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed, waiting RegisterNatives...", flush=True)
time.sleep(22)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] 注册方法数: {len(result['regs'])} saved {OUT}")
if __name__ == "__main__":
main()
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""真机 v8: 用 Java 层直接调用 com.huya.security.hydeviceid.NativeEntry 采集 hdid 族.
拿到 libhydeviceid 注册表后, 用 frida Java.use 调 NativeEntry 的 native 方法:
init() -> getHDID()/getSDID()/getCDID()/getGUID()/getMID()
直接可靠读取每台真机的 hdid(32hex) 及同族设备 ID —— 这是多机采集 hdid 池的前提.
同时 hook getHDID 的 native 入口(0x20f0c8)记录调用栈确认来源.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_read_hdid.py [serial] [remote]
输出:
evidence/diag_phone/hdid_read.json
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
import frida
SERIAL = sys.argv[1] if len(sys.argv) > 1 else "5dd8c93f"
REMOTE = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:31878"
PACKAGE = "com.duowan.kiwi"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "hdid_read.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
GETHDID_OFF = 0x20F0C8 # libhydeviceid.so 相对偏移
JS = r"""
'use strict';
var NE_CLASS = 'com.huya.security.hydeviceid.NativeEntry';
var nativeHooked = false;
var GETS = [
{n:'getCDID', off:0x20eb08},
{n:'getSDID', off:0x20ede8},
{n:'getHDID', off:0x20f0c8},
{n:'getGUID', off:0x20f8c8},
{n:'getMID', off:0x20fba8}
];
function readJstr(env, ret){
try{
var fns = env.readPointer(); // JNINativeInterface*
var g = new NativeFunction(fns.add(169*8).readPointer(), 'pointer', ['pointer','pointer','pointer']); // GetStringUTFChars
var rel = new NativeFunction(fns.add(170*8).readPointer(), 'void', ['pointer','pointer','pointer']); // ReleaseStringUTFChars
var cv = g(env, ret, ptr(0));
if(cv && !cv.isNull()){ var s=cv.readCString()||''; rel(env, ret, cv); return s; }
return '<null>';
}catch(e){ return '<err>'; }
}
function hookAll(){
if(nativeHooked) return;
var m = Process.findModuleByName('libhydeviceid.so');
if(!m) return;
nativeHooked = true;
GETS.forEach(function(g){
try{
Interceptor.attach(m.base.add(g.off), {
onEnter:function(a){ this.env = a[0]; this.name = g.n; },
onLeave:function(ret){ send({type:'getval', name:this.name, str:readJstr(this.env, ret)}); }
});
}catch(e){}
});
send({type:'all-hooked', mods:GETS.map(function(g){return g.n;})});
}
setInterval(hookAll, 300);
Java.perform(function(){
function tryRead(){
try{
var NE = Java.use('com.huya.security.hydeviceid.NativeEntry');
send({type:'class-found', cls:NE_CLASS});
// 先 init (可能构造指纹), 再逐个读
var out = {};
['init','getHDID','getSDID','getCDID','getGUID','getMID'].forEach(function(m){
try{ out[m] = NE[m](); }catch(e){ out[m] = '<ERR:'+e+'>'; }
});
send({type:'read', values:out});
}catch(e){ send({type:'java-err', e:String(e)}); }
}
// 类可能尚未注册(等 libhydeviceid 加载)
var n=0;
var timer = setInterval(function(){
n++;
try{
Java.use('com.huya.security.hydeviceid.NativeEntry');
clearInterval(timer);
tryRead();
}catch(e){ if(n>60){ clearInterval(timer); send({type:'cls-not-found'}); } }
}, 500);
});
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def main():
OUT.parent.mkdir(parents=True, exist_ok=True)
adb("shell", "am", "force-stop", PACKAGE)
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)
det = []
session.on("detached", lambda r, dd: det.append({"reason": r, "detail": str(dd)}) or print(f"[*] detached {r} {dd}", flush=True))
session.create_script(ART_CALLSITE.read_text()).load()
result = {"read": None, "nativetrap": [], "detached": det}
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 == "read":
result["read"] = p["values"]
print("[*] NativeEntry 读取:", flush=True)
for k, v in p["values"].items():
print(f" {k} = {v}", flush=True)
elif t == "getval":
print(f"[getval] {p.get('name')} = {p.get('str')}", flush=True)
# 存起来
result.setdefault("getvals", []).append(p)
elif t == "all-hooked":
print(f"[*] all get methods hooked: {p.get('mods')}", flush=True)
elif t in ("class-found","native-hooked"):
print(f"[*] {t}", flush=True)
elif t == "cls-not-found":
print("[!] 类未找到(可能未加载)", flush=True)
elif t == "java-err":
print(f"[java-err] {p.get('e')}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed, waiting for class + read...", flush=True)
time.sleep(35)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] saved {OUT}")
if __name__ == "__main__":
main()