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