- phone_stalk_getguid: Stalker 跟踪 getGUID(0x20f8c8) 执行, 揭示内部调用 (art::JNI::NewStringUTF 外层, libhydeviceid 内部函数+jsemalloc, 不读属性) - phone_getguid_ioctl: getGUID 窗口 ioctl/openat/property_get 追踪 -> ioctl=0, 确认是 getter 非生成器 - phone_init_ioctl: init()(0x20e6e4) 窗口追踪 -> 巨大 ioctl 噪音(App启动) + open fileshydckey(设备密钥) - getguid_stalk/ioctl/init 证据 结论: 32hex GUID = init 时用底层硬件指纹(SoC序列号等不可改)+hydckey经OLLVM保护算法生成并缓存; getGUID 只是读取。单机不可铸造(种子为不可改硬件ID)。
161 lines
5.7 KiB
Python
161 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""真机: Stalker 跟踪 NativeEntry.getGUID(0x20f8c8) 执行, 揭示 32hex 生成的硬件读取调用.
|
|
|
|
getGUID 被 OLLVM 平坦化, 静态难读。改用运行时 Stalker: hook getGUID,
|
|
onEnter Stalker.follow(当前线程) 记录所有 call, 同时 hook libc __system_property_get
|
|
+ libhydeviceid 的 hy_fopen64/hy_read 看它读的属性/文件。Java.use('...NativeEntry').getGUID()
|
|
主动触发。
|
|
|
|
用法:
|
|
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
|
|
scripts/phone_stalk_getguid.py [serial] [remote]
|
|
输出:
|
|
evidence/diag_phone/getguid_stalk.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" / "getguid_stalk.json"
|
|
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
|
|
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
|
|
GETGUID_OFF = 0x20F8C8
|
|
|
|
JS = r"""
|
|
'use strict';
|
|
var hooked=false, propHooked=false;
|
|
|
|
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:'?'}; }
|
|
}
|
|
|
|
// hook __system_property_get: 记录 getGUID 期间读的属性
|
|
function hookProp(){
|
|
if(propHooked) return;
|
|
try{
|
|
var t=Module.findExportByName('libc.so','__system_property_get');
|
|
if(!t) return;
|
|
propHooked=true;
|
|
Interceptor.attach(t,{
|
|
onEnter:function(a){ this.k=a[0].readCString()||''; this.vp=a[1]; },
|
|
onLeave:function(){ try{ send({type:'prop', k:this.k, v:this.vp.readCString()||''}); }catch(e){} }
|
|
});
|
|
}catch(e){}
|
|
}
|
|
|
|
function hookGetGUID(){
|
|
if(hooked) return;
|
|
var md=Process.findModuleByName('libhydeviceid.so');
|
|
if(!md) return;
|
|
hooked=true;
|
|
var g=md.base.add(0x20f8c8);
|
|
try{
|
|
Interceptor.attach(g,{
|
|
onEnter:function(){
|
|
this.tid=Process.getCurrentThreadId();
|
|
send({type:'guid-enter', tid:this.tid});
|
|
try{
|
|
Stalker.follow(this.tid, {
|
|
events:{call:true, ret:false, exec:false},
|
|
onReceive:function(events){
|
|
// 记录 call 事件的目标地址
|
|
var ev=events[0]||{};
|
|
var calls=[];
|
|
if(ev.type==='call'){ try{ calls=[{addr:''+ev.target}]; }catch(e){} }
|
|
if(calls.length){ send({type:'call-ev', calls:calls}); }
|
|
},
|
|
onCallSummary:function(sum){
|
|
var list=[];
|
|
for(var a in sum){ var p=ptr(a); list.push({addr:''+p, mod:jmod(p).n, off:jmod(p).off, cnt:sum[a]}); }
|
|
send({type:'call-sum', list:list});
|
|
}
|
|
});
|
|
}catch(e){ send({type:'stalk-err', e:String(e)}); }
|
|
},
|
|
onLeave:function(){ try{ Stalker.unfollow(Process.getCurrentThreadId()); }catch(e){} send({type:'guid-leave'}); }
|
|
});
|
|
send({type:'guid-hooked'});
|
|
}catch(e){ send({type:'hook-err', e:String(e)}); }
|
|
}
|
|
setInterval(hookGetGUID, 200);
|
|
setInterval(hookProp, 200);
|
|
|
|
// 触发: Java.use NativeEntry.getGUID()
|
|
setTimeout(function(){
|
|
Java.perform(function(){
|
|
try{
|
|
var NE=Java.use('com.huya.security.hydeviceid.NativeEntry');
|
|
send({type:'cls-ok'});
|
|
var v=NE['getGUID']();
|
|
send({type:'guid-value', v:v});
|
|
var c=NE['getCDID'](); send({type:'cdid', v:c});
|
|
}catch(e){ send({type:'java-err', e:String(e).slice(0,150)}); }
|
|
});
|
|
}, 8000);
|
|
"""
|
|
|
|
|
|
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": [], "calls": [], "values": {}, "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 == "call-sum":
|
|
result["calls"] = p["list"]
|
|
print(f"[call-sum] {len(p['list'])} 个调用:", flush=True)
|
|
for e in p["list"][:20]:
|
|
print(f" 0x{e['off']} x{e['cnt']} {e['mod']}", flush=True)
|
|
elif t == "prop":
|
|
result["props"].append(p)
|
|
print(f"[prop] {p.get('k')}={p.get('v')}", flush=True)
|
|
elif t == "guid-value":
|
|
result["values"]["getGUID"] = p["v"]
|
|
print(f"[*] getGUID = {p['v']}", flush=True)
|
|
elif t == "cdid":
|
|
result["values"]["getCDID"] = p["v"]
|
|
print(f"[*] getCDID = {p['v']}", flush=True)
|
|
elif t in ("guid-enter","guid-leave","guid-hooked","cls-ok"):
|
|
print(f"[*] {t}", flush=True)
|
|
elif t in ("stalk-err","hook-err","java-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", flush=True)
|
|
time.sleep(8) # 等类加载
|
|
print("[*] 触发 getGUID (Java.use)...", flush=True)
|
|
time.sleep(6)
|
|
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() |