- 输入图定案: 库内零硬件文件读, 种子=ANDROID_ID+服务端hydeviceid_config+MID, 属性层无关(X1/X2) - 真机全段解密 dump(phone_dump_hydev_full) + merge_decrypted 合入原文件 → unidbg 可直接加载 - unidbg(JDK21+patch) harness: JNI 桩(SharedPreferences日志化/Settings.Secure/NativeBridge) 金测试: androidId 加密态与 getGUID 全部与真机一致 - 遗留: NativeBridge.b(100) 原生归属库未定(非hydeviceid/udb/device-util), 当前桩替顶 - 修正结论: '单机不可铸造'判断错误, GUID=b(100)=f(androidId密, config, MID) 输入全可控
101 lines
3.5 KiB
Python
101 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""真机 FULL dump libhydeviceid.so (r-x + r-- + rw- 全部段, 含解密后的 .data/.got).
|
|
|
|
上次 phone_dump_hydev 只抓 r-- (代码+rodata), .data/.got/.bss 全零 —— unidbg 需要
|
|
完整解密镜像(密钥/GOT 都在 .data). 本脚本抓到全部可读内存.
|
|
|
|
用法:
|
|
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
|
|
scripts/phone_dump_hydev_full.py [serial] [remote]
|
|
输出: evidence/diag_phone/libhydeviceid_dump_full.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_full.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--').concat(md.enumerateRanges('rw-'));
|
|
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, prot:r.protection}, 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, "base": 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"]
|
|
result["base"] = int(p["modBase"], 16)
|
|
print(f"[*] 计划 dump {p['count']} 段, module base={p['modBase']}", 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']} prot={p.get('prot')}", flush=True)
|
|
elif t == "done":
|
|
print(f"[*] dump done {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)
|
|
t0 = time.time()
|
|
while (result["got"] < result["expected"] or result["expected"] == 0) and time.time() - t0 < 25:
|
|
time.sleep(0.5)
|
|
fh.close()
|
|
OUT.chmod(0o644)
|
|
print(f"[*] done got={result['got']}/{result['expected']} base=0x{result['base']:x} -> {OUT}", flush=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |