#!/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()