核心结论:
- 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
168 lines
6.4 KiB
Python
168 lines
6.4 KiB
Python
#!/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() |