docs(huya): 逆向定位 32hex hdid = NativeEntry.getGUID() + 真机注入通道证据

核心结论:
- 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
This commit is contained in:
yml2213
2026-08-28 12:30:34 +08:00
parent 45b5e9128c
commit 0c6aff6a6d
23 changed files with 86238 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
# 虎牙 32hex hdid 生成逆向研究(真机 M2102J2SC)
> 目标:搞清楚 WUP 登录帧 t1.t0 / field1.tag0 的 32hex hdid(金样本 `ed0db8334cadd236c00cadf7e11ab5a5`
> 如何生成、是否能在同一台设备上铸造新值(实现"每账号独立设备")。
> 时间:2026-08-28(真机稳定注入通道打通后进行)。**已定位 32hex = NativeEntry.getGUID()。**
## 一、最终结论(决定性)
**32hex hdidWUP 登录帧 field1.tag0= `com.huya.security.hydeviceid.NativeEntry.getGUID()` 的返回值。**
- 金样本 `ed0db8...` 是**金样本设备**的 getGUID() 值;
- **当前真机 M2102J2SC 的 getGUID() = `0a7dfaa882938a6ab502511452142c57`**32hex,交叉验证稳定:
在 5 个设备上报帧中出现 + Java 主动调用一致)。
**getGUID() 每台设备唯一且稳定(硬锚硬件,同 getCDID/getHDID 一样),可被 `NativeEntry`
native 方法直接可靠读取。**
因此**"生成 32hex hdid" 无需逆向算法**:每台真机天然由 libhydeviceid.so 生成并提供它独有的
32hex getGUID()。多真机设备池 = 每台调 getGUID() 采集各自唯一值。
### 单机不可铸造(3 个决定性实验)
`ro.serialno` / `ANDROID_ID` / 删 `files/hydevice/` 持久化后重启,设备标识(UUID 文件名 +
getGUID 等)**全部不变** —— 因 libhydeviceid.so 基于底层硬件(SoC/IMEI)稳定生成,刷不掉。
## 二、NativeEntry 全部字段(已可靠读取)
通过 dlopen 拦截 → JNI_OnLoad(0x22c4c8) → RegisterNatives(JNIEnv 函数表索引 215)dump 出
`com.huya.security.hydeviceid.NativeEntry` 的 19 个 native 方法,Java 主动调用实测值:
| NativeEntry 方法 | 真机实测值 | 对应 WUP 登录帧字段 |
|---|---|---|
| **getGUID()** | **`0a7dfaa882938a6ab502511452142c57`** | **field1.tag0 = hdid32hex** |
| getCDID() | `02df398797432eadefcc12767119ad5e80999389` | field2.tag2 = device_id40hex |
| getHDID() | `7c5387e0539c023c31c4ff0e807e7256117385ee` | field2.tag8 = 另一设备ID40hex |
| getMID() | `1e8bdf7d4f7a01d3` | MID16hex |
| getSDID() | `PQwemAN9...`base64 180B | safedeviceid |
- Java 类 `com.huya.security.hydeviceid.NativeEntry` 注册 method 地址(libhydeviceid.so 偏移):
getCDID@0x20eb08 / getSDID@0x20ede8 / **getHDID@0x20f0c8** / init@0x20e6e4 /
getGUID@0x20f8c8 / getMID@0x20fba8 / linkId@0x20fe88(其余 setPhoneInfo/setMotionEvent/上报类略)。
- 另有 `com.huyaudb.HuyaAuthCore` 注册 `init/unInit/sendMsg/receiveNet`WUP 消息收发)。
### hdid 名称混淆(已彻底澄清)
项目里"hdid"对应**多个不同值**
1. **32hexWUP 登录帧 field1.tag0= getGUID()** —— 设备 GUID,硬锚,每台唯一;
2. 40hex `7c5387` = getHDID() = huyaudb 上报里被命名为 "hdid"**同名不同值**);
3. 40hex `02df39` = getCDID() = device_id
4. 16hex `1e8bdf7d4f7a01d3` = getMID()
5. 凭据文件 `files/hydevice/<UUID>``64a33427...`)持久化加密凭据。
## 三、libhydeviceid.so 加壳(静态逆向极难,但已绕过)
- **OLLVM 混淆**`based on Obfuscator-LLVM 9.0.9svn`+ **datadiv 运行时壳**~70 个 `.datadiv_decode*`
解密函数 + `hy_fopen64/hy_read/hy_syscall/hy_write/hyftell` 文件 I/O 壳 + 读 TracerPid 反调试)。
-**dlopen→JNI_OnLoad→RegisterNatives** 动态拿到方法表,绕开混淆,无需反汇编。
- 已 dump 解密后代码:`evidence/diag_phone/libhydeviceid_dump.so`(内存态)+ 原始
`libhydeviceid_original.so`
## 四、可行方向(服务于"每账号独立设备")——**推荐,已具备采集能力**
**多真机采集设备池**:每台真机用真机注入通道(spawn+art_callsite bypass)读
`{getGUID(32hex), getCDID(40hex), getHDID(40hex), getMID(16hex), getSDID(base64)}`
这些值每台唯一、稳定、被服务端认可。采集多台建设备池轮换,即可实现每账号独立设备。
**无需逆向铸造算法**,直接读 NativeEntry 即可。
## 五、实验脚本(真机)
- `scripts/phone_probe_registernatives2.py`v7dlopen→JNI_OnLoad→RegisterNatives 注册表 + get* 地址)
- `scripts/phone_java_call_nativeentry.py`**Java 主动调 NativeEntry get* 全家桶,读取 32hex GUID**
- `scripts/phone_read_hdid.py`v8native hook get* 用原生 JNI GetStringUTFChars 解码)
- `scripts/phone_dump_hydev.py`dump 解密后的 libhydeviceid.so
- `scripts/phone_probe_hdid*.py`(早期符号 / 属性 / 文件探针,过程记录)
- 证据:`evidence/diag_phone/{hdid_regtable,hdid_read,java_nativeentry,login_frames}.json` /
`libhydeviceid_{original,dump}.so`
+366
View File
@@ -0,0 +1,366 @@
{
"events": [
{
"type": "hydev",
"base": "0x7813011000",
"size": 4124672
},
{
"type": "hydev-exports",
"count": 76,
"exports": [
{
"off": "0xff854",
"name": ".datadiv_decode13377393303060931696"
},
{
"off": "0x30bf3c",
"name": "hy_syscall"
},
{
"off": "0x2c671c",
"name": ".datadiv_decode8394649184240089179"
},
{
"off": "0xc452c",
"name": ".datadiv_decode12715880952929850959"
},
{
"off": "0x22c6b4",
"name": ".datadiv_decode17756744967214589151"
},
{
"off": "0x206530",
"name": ".datadiv_decode16142768320442664775"
},
{
"off": "0x10fe18",
"name": ".datadiv_decode13935584199325042899"
},
{
"off": "0x1c91dc",
"name": ".datadiv_decode4182082414496431548"
},
{
"off": "0x19b5f0",
"name": ".datadiv_decode14514111528646739931"
},
{
"off": "0x26d7b0",
"name": ".datadiv_decode3186372685042885857"
},
{
"off": "0x212b04",
"name": ".datadiv_decode13001096679939917798"
},
{
"off": "0xec484",
"name": ".datadiv_decode7272451282499632790"
},
{
"off": "0x2880fc",
"name": ".datadiv_decode5662830498276606612"
},
{
"off": "0x111490",
"name": ".datadiv_decode4441042267327943258"
},
{
"off": "0x27f610",
"name": ".datadiv_decode16021085630626535143"
},
{
"off": "0x30ae48",
"name": ".datadiv_decode17316226923494143690"
},
{
"off": "0x1d5f4c",
"name": ".datadiv_decode7531359686248698545"
},
{
"off": "0x247144",
"name": ".datadiv_decode17780086335141721104"
},
{
"off": "0x1adfd0",
"name": ".datadiv_decode16904853521236949932"
},
{
"off": "0xf013c",
"name": ".datadiv_decode3639556092286127407"
},
{
"off": "0x21f98c",
"name": ".datadiv_decode8476215815990549556"
},
{
"off": "0x30bf00",
"name": "hy_write"
},
{
"off": "0xea848",
"name": ".datadiv_decode5013095055348382358"
},
{
"off": "0x2bf894",
"name": ".datadiv_decode6552120278544488220"
},
{
"off": "0x245be0",
"name": ".datadiv_decode16449743662515666302"
},
{
"off": "0x11b930",
"name": ".datadiv_decode15631854437821104344"
},
{
"off": "0xe7a5c",
"name": ".datadiv_decode15220286527488363869"
},
{
"off": "0x189ed0",
"name": ".datadiv_decode971716066137475299"
},
{
"off": "0x235fd4",
"name": ".datadiv_decode2240554284615242758"
},
{
"off": "0xbbf90",
"name": ".datadiv_decode930153296716187306"
},
{
"off": "0x17f75c",
"name": ".datadiv_decode18193155687921697907"
},
{
"off": "0xef9c4",
"name": ".datadiv_decode7208817035916333614"
},
{
"off": "0x22c4c8",
"name": "JNI_OnLoad"
},
{
"off": "0xbdb4c",
"name": ".datadiv_decode9718679232209877219"
},
{
"off": "0x9f9d8",
"name": ".datadiv_decode2415276826024734045"
},
{
"off": "0xf3360",
"name": "hyftell"
},
{
"off": "0x242918",
"name": ".datadiv_decode1813700997626276425"
},
{
"off": "0x116020",
"name": ".datadiv_decode2127118722610622943"
},
{
"off": "0xf80c8",
"name": ".datadiv_decode6313363124510466600"
},
{
"off": "0x1d4e54",
"name": ".datadiv_decode16229027793617600142"
},
{
"off": "0x1d0580",
"name": ".datadiv_decode10115291355877817959"
},
{
"off": "0x30bae8",
"name": ".datadiv_decode10203504059392997873"
},
{
"off": "0xf1298",
"name": "hyfopen64"
},
{
"off": "0x2c321c",
"name": ".datadiv_decode15986889750731987093"
},
{
"off": "0x23e338",
"name": ".datadiv_decode11861954481925470588"
},
{
"off": "0x2fae08",
"name": ".datadiv_decode13370067802168070091"
},
{
"off": "0x2332d4",
"name": ".datadiv_decode5223443461034991902"
},
{
"off": "0x30bef0",
"name": "hy_read"
},
{
"off": "0x2ccc64",
"name": ".datadiv_decode8208694899043494479"
},
{
"off": "0x227b50",
"name": ".datadiv_decode18104751532030584008"
},
{
"off": "0x278c40",
"name": ".datadiv_decode8389934691053754807"
},
{
"off": "0x274bbc",
"name": ".datadiv_decode381523612705756801"
},
{
"off": "0x10923c",
"name": ".datadiv_decode11326397958007879543"
},
{
"off": "0x22ed90",
"name": ".datadiv_decode15748246807707687162"
},
{
"off": "0x307350",
"name": ".datadiv_decode14652163487098383068"
},
{
"off": "0x2823e0",
"name": ".datadiv_decode10168130846246250133"
},
{
"off": "0xfe1f4",
"name": ".datadiv_decode8274439441013259658"
},
{
"off": "0xb5a18",
"name": ".datadiv_decode2818691867109853035"
},
{
"off": "0x284a38",
"name": ".datadiv_decode17937343329166821664"
},
{
"off": "0x23755c",
"name": ".datadiv_decode8740858567082904038"
},
{
"off": "0xe1968",
"name": ".datadiv_decode15148178244621190089"
},
{
"off": "0x1094ec",
"name": ".datadiv_decode10096756759899255764"
},
{
"off": "0x2c2b28",
"name": ".datadiv_decode5687050107175613243"
},
{
"off": "0x1e4048",
"name": ".datadiv_decode10091502229593795971"
},
{
"off": "0x1107c8",
"name": ".datadiv_decode16105350321006291378"
},
{
"off": "0xf0078",
"name": ".datadiv_decode13805690131605262795"
},
{
"off": "0x308e64",
"name": ".datadiv_decode13481575639147657934"
},
{
"off": "0x25df2c",
"name": ".datadiv_decode5869377321246282402"
},
{
"off": "0xec6d8",
"name": ".datadiv_decode5215474294857803928"
},
{
"off": "0x23c534",
"name": ".datadiv_decode15714924883407080113"
},
{
"off": "0x27fc5c",
"name": ".datadiv_decode16454361570808326355"
},
{
"off": "0x27df10",
"name": ".datadiv_decode15924187094028544261"
},
{
"off": "0x23532c",
"name": ".datadiv_decode4448295952963935447"
},
{
"off": "0x141914",
"name": ".datadiv_decode5332962653989896249"
},
{
"off": "0x1106a8",
"name": ".datadiv_decode7627883646358325318"
},
{
"off": "0x2c80ac",
"name": ".datadiv_decode10053382265862873780"
}
]
},
{
"type": "jni-onload-hooked"
},
{
"type": "jni-onload-enter",
"tid": 27101
},
{
"type": "jni-onload-leave",
"ret": "0x10006"
},
{
"type": "udb",
"base": "0x77bf3d4000"
},
{
"type": "hdid-hooked"
},
{
"type": "setsd-hooked"
},
{
"type": "hdid",
"str": "",
"tid": 27267
},
{
"type": "hdid",
"str": "",
"tid": 27267
},
{
"type": "hdid",
"str": "",
"tid": 27267
},
{
"type": "hdid",
"str": "",
"tid": 27101
},
{
"type": "hdid",
"str": "",
"tid": 27101
}
],
"detached": []
}
+247
View File
@@ -0,0 +1,247 @@
{
"props": [
{
"type": "prop",
"k": "ro.carrier.name",
"v": ""
},
{
"type": "prop",
"k": "ro.carrier.name",
"v": ""
},
{
"type": "prop",
"k": "ro.carrier.name",
"v": ""
},
{
"type": "prop",
"k": "ro.carrier.name",
"v": ""
},
{
"type": "prop",
"k": "ro.product.mod_device",
"v": ""
},
{
"type": "prop",
"k": "ro.product.mod_device",
"v": ""
},
{
"type": "prop",
"k": "ro.miui.cts",
"v": ""
},
{
"type": "prop",
"k": "persist.sys.miui_optimization",
"v": ""
},
{
"type": "prop",
"k": "ro.cust.test",
"v": ""
},
{
"type": "prop",
"k": "ro.cust.test",
"v": ""
},
{
"type": "prop",
"k": "ro.cust.test",
"v": ""
},
{
"type": "prop",
"k": "persist.sys.func_limit_switch",
"v": ""
},
{
"type": "prop",
"k": "ro.config.low_ram.threshold_gb",
"v": ""
},
{
"type": "prop",
"k": "ro.product.mod_device",
"v": ""
},
{
"type": "prop",
"k": "ro.product.mod_device",
"v": ""
},
{
"type": "prop",
"k": "ro.build.characteristics",
"v": "nosdcard"
},
{
"type": "prop",
"k": "ro.miui.userdata_version",
"v": ""
},
{
"type": "prop",
"k": "ro.debuggable",
"v": "0"
},
{
"type": "prop",
"k": "ro.miui.has_cust_partition",
"v": "true"
},
{
"type": "prop",
"k": "ro.miui.cust_device",
"v": ""
},
{
"type": "prop",
"k": "ro.miui.ui.version.code",
"v": "11"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "debug.atrace.tags.enableflags",
"v": "0"
},
{
"type": "prop",
"k": "ro.debuggable",
"v": "0"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.miui.mcc",
"v": "9460"
},
{
"type": "prop",
"k": "ro.build.version.sdk",
"v": "30"
},
{
"type": "prop",
"k": "ro.vendor.redirect_socket_calls",
"v": ""
},
{
"type": "prop",
"k": "ro.arch",
"v": ""
},
{
"type": "prop",
"k": "ro.arch",
"v": ""
},
{
"type": "prop",
"k": "ro.boringcrypto.hwrand",
"v": ""
},
{
"type": "prop",
"k": "ro.build.version.sdk",
"v": "30"
},
{
"type": "prop",
"k": "persist.sys.dalvik.vm.lib",
"v": ""
},
{
"type": "prop",
"k": "persist.sys.dalvik.vm.lib.2",
"v": "libart.so"
},
{
"type": "prop",
"k": "ro.build.version.release_or_codename",
"v": "11"
},
{
"type": "prop",
"k": "ro.product.model",
"v": "M2102J2SC"
}
],
"hyio": [],
"hdid": [],
"detached": []
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
{
"read": null,
"nativetrap": [],
"detached": [
{
"reason": "process-terminated",
"detail": "None"
}
],
"getvals": [
{
"type": "getval",
"name": "getCDID",
"str": "02df398797432eadefcc12767119ad5e80999389"
},
{
"type": "getval",
"name": "getHDID",
"str": "7c5387e0539c023c31c4ff0e807e7256117385ee"
},
{
"type": "getval",
"name": "getSDID",
"str": "PQwemAN9NHkZKoMqJ1jkwhIypqMTaQEOrmXr37xQVhQZq7D8uCKpS0FntlUjmpcZ4PnIwGtXSr+w2NnEM5QpuwVPuPhSRxBjPEjt1XPta6E6IvWxsSculivOJlC9qSOAjmzk9EFJeJU+0K1drfzcBKL5v8hMLabmzr76aYoVFExaEgA7lzs="
},
{
"type": "getval",
"name": "getHDID",
"str": "7c5387e0539c023c31c4ff0e807e7256117385ee"
}
]
}
@@ -0,0 +1,5 @@
{
"regs": [],
"hdid": [],
"detached": []
}
+802
View File
@@ -0,0 +1,802 @@
{
"regs": [
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "getCDID",
"sig": "()Ljava/lang/String;",
"mod": "libhydeviceid.so",
"off": "0x20eb08"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "getSDID",
"sig": "()Ljava/lang/String;",
"mod": "libhydeviceid.so",
"off": "0x20ede8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "getHDID",
"sig": "()Ljava/lang/String;",
"mod": "libhydeviceid.so",
"off": "0x20f0c8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "init",
"sig": "()V",
"mod": "libhydeviceid.so",
"off": "0x20e6e4"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "getGUID",
"sig": "()Ljava/lang/String;",
"mod": "libhydeviceid.so",
"off": "0x20f8c8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "getMID",
"sig": "()Ljava/lang/String;",
"mod": "libhydeviceid.so",
"off": "0x20fba8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "linkId",
"sig": "()Z",
"mod": "libhydeviceid.so",
"off": "0x20fe88"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "setPrivacyMode",
"sig": "(Z)V",
"mod": "libhydeviceid.so",
"off": "0x20f3a8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "setUserAction",
"sig": "(Ljava/lang/String;)V",
"mod": "libhydeviceid.so",
"off": "0x20f3b4"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "setAppStatus",
"sig": "(I)V",
"mod": "libhydeviceid.so",
"off": "0x20f888"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "setPhoneInfo",
"sig": "(Ljava/lang/String;)Z",
"mod": "libhydeviceid.so",
"off": "0x2107f0"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "doReportAgain",
"sig": "(Ljava/lang/String;)V",
"mod": "libhydeviceid.so",
"off": "0x20fea0"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "delayCollect",
"sig": "(Ljava/lang/String;)V",
"mod": "libhydeviceid.so",
"off": "0x210348"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "setMotionEvent",
"sig": "(Ljava/lang/String;Ljava/lang/String;)Z",
"mod": "libhydeviceid.so",
"off": "0x210c20"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "uploadScreenRecord",
"sig": "()V",
"mod": "libhydeviceid.so",
"off": "0x2114f8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "uploadScreenShot",
"sig": "()V",
"mod": "libhydeviceid.so",
"off": "0x2114fc"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "updateDisplayInfo",
"sig": "(Ljava/lang/String;)V",
"mod": "libhydeviceid.so",
"off": "0x211500"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "addApplist",
"sig": "(Ljava/lang/String;Ljava/lang/String;)V",
"mod": "libhydeviceid.so",
"off": "0x2119a8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "requestConfig",
"sig": "()V",
"mod": "libhydeviceid.so",
"off": "0x212a5c"
},
{
"type": "reg",
"cls": "com.huyaudb.HuyaAuthCore",
"name": "init",
"sig": "()V",
"mod": "libudbauthunify.so",
"off": "0x2621d4"
},
{
"type": "reg",
"cls": "com.huyaudb.HuyaAuthCore",
"name": "unInit",
"sig": "()V",
"mod": "libudbauthunify.so",
"off": "0x26238c"
},
{
"type": "reg",
"cls": "com.huyaudb.HuyaAuthCore",
"name": "sendMsg",
"sig": "(J[B)[B",
"mod": "libudbauthunify.so",
"off": "0x26244c"
},
{
"type": "reg",
"cls": "com.huyaudb.HuyaAuthCore",
"name": "receiveNet",
"sig": "([BIII)V",
"mod": "libudbauthunify.so",
"off": "0x2626b0"
}
],
"fns_scan": [
{
"type": "fns-scan",
"dump": [
{
"i": 200,
"a": "0x7895f37840",
"mod": "libart.so",
"off": "0x3ee840"
},
{
"i": 201,
"a": "0x7895f38004",
"mod": "libart.so",
"off": "0x3ef004"
},
{
"i": 202,
"a": "0x7895f387c8",
"mod": "libart.so",
"off": "0x3ef7c8"
},
{
"i": 203,
"a": "0x7895f38f8c",
"mod": "libart.so",
"off": "0x3eff8c"
},
{
"i": 204,
"a": "0x7895f39750",
"mod": "libart.so",
"off": "0x3f0750"
},
{
"i": 205,
"a": "0x7895f39f14",
"mod": "libart.so",
"off": "0x3f0f14"
},
{
"i": 206,
"a": "0x7895f3a6d8",
"mod": "libart.so",
"off": "0x3f16d8"
},
{
"i": 207,
"a": "0x7895f3ae9c",
"mod": "libart.so",
"off": "0x3f1e9c"
},
{
"i": 208,
"a": "0x7895f3b660",
"mod": "libart.so",
"off": "0x3f2660"
},
{
"i": 209,
"a": "0x7895f3be24",
"mod": "libart.so",
"off": "0x3f2e24"
},
{
"i": 210,
"a": "0x7895f3c5e8",
"mod": "libart.so",
"off": "0x3f35e8"
},
{
"i": 211,
"a": "0x7895f3cdac",
"mod": "libart.so",
"off": "0x3f3dac"
},
{
"i": 212,
"a": "0x7895f3d570",
"mod": "libart.so",
"off": "0x3f4570"
},
{
"i": 213,
"a": "0x7895f3dd34",
"mod": "libart.so",
"off": "0x3f4d34"
},
{
"i": 214,
"a": "0x7895f3e4f8",
"mod": "libart.so",
"off": "0x3f54f8"
},
{
"i": 215,
"a": "0x7895f3ecbc",
"mod": "libart.so",
"off": "0x3f5cbc"
},
{
"i": 216,
"a": "0x7895f3fbb4",
"mod": "libart.so",
"off": "0x3f6bb4"
},
{
"i": 217,
"a": "0x7895f40488",
"mod": "libart.so",
"off": "0x3f7488"
},
{
"i": 218,
"a": "0x7895f40bc4",
"mod": "libart.so",
"off": "0x3f7bc4"
},
{
"i": 219,
"a": "0x7895f41300",
"mod": "libart.so",
"off": "0x3f8300"
},
{
"i": 220,
"a": "0x7895f41354",
"mod": "libart.so",
"off": "0x3f8354"
},
{
"i": 221,
"a": "0x7895f41b34",
"mod": "libart.so",
"off": "0x3f8b34"
},
{
"i": 222,
"a": "0x7895f4232c",
"mod": "libart.so",
"off": "0x3f932c"
},
{
"i": 223,
"a": "0x7895f42b54",
"mod": "libart.so",
"off": "0x3f9b54"
},
{
"i": 224,
"a": "0x7895f43320",
"mod": "libart.so",
"off": "0x3fa320"
},
{
"i": 225,
"a": "0x7895f43b08",
"mod": "libart.so",
"off": "0x3fab08"
},
{
"i": 226,
"a": "0x7895f44244",
"mod": "libart.so",
"off": "0x3fb244"
},
{
"i": 227,
"a": "0x7895f44934",
"mod": "libart.so",
"off": "0x3fb934"
},
{
"i": 228,
"a": "0x7895f44948",
"mod": "libart.so",
"off": "0x3fb948"
},
{
"i": 229,
"a": "0x7895f4495c",
"mod": "libart.so",
"off": "0x3fb95c"
},
{
"i": 230,
"a": "0x7895f44a14",
"mod": "libart.so",
"off": "0x3fba14"
}
]
}
],
"events": [
{
"type": "dlopen-hooked",
"fn": "android_dlopen_ext"
},
{
"type": "dlopen-hydev",
"name": "/data/app/~~8yCINTbsGdM-Ig8T11hF2Q==/com.duowan.kiwi-sandFFuGVgsZjumsNfkf1A==/lib/arm64/libhydeviceid.so"
},
{
"type": "jni-onload-hooked"
},
{
"type": "jni-onload-enter",
"tid": 30763
},
{
"type": "fns-scan",
"dump": [
{
"i": 200,
"a": "0x7895f37840",
"mod": "libart.so",
"off": "0x3ee840"
},
{
"i": 201,
"a": "0x7895f38004",
"mod": "libart.so",
"off": "0x3ef004"
},
{
"i": 202,
"a": "0x7895f387c8",
"mod": "libart.so",
"off": "0x3ef7c8"
},
{
"i": 203,
"a": "0x7895f38f8c",
"mod": "libart.so",
"off": "0x3eff8c"
},
{
"i": 204,
"a": "0x7895f39750",
"mod": "libart.so",
"off": "0x3f0750"
},
{
"i": 205,
"a": "0x7895f39f14",
"mod": "libart.so",
"off": "0x3f0f14"
},
{
"i": 206,
"a": "0x7895f3a6d8",
"mod": "libart.so",
"off": "0x3f16d8"
},
{
"i": 207,
"a": "0x7895f3ae9c",
"mod": "libart.so",
"off": "0x3f1e9c"
},
{
"i": 208,
"a": "0x7895f3b660",
"mod": "libart.so",
"off": "0x3f2660"
},
{
"i": 209,
"a": "0x7895f3be24",
"mod": "libart.so",
"off": "0x3f2e24"
},
{
"i": 210,
"a": "0x7895f3c5e8",
"mod": "libart.so",
"off": "0x3f35e8"
},
{
"i": 211,
"a": "0x7895f3cdac",
"mod": "libart.so",
"off": "0x3f3dac"
},
{
"i": 212,
"a": "0x7895f3d570",
"mod": "libart.so",
"off": "0x3f4570"
},
{
"i": 213,
"a": "0x7895f3dd34",
"mod": "libart.so",
"off": "0x3f4d34"
},
{
"i": 214,
"a": "0x7895f3e4f8",
"mod": "libart.so",
"off": "0x3f54f8"
},
{
"i": 215,
"a": "0x7895f3ecbc",
"mod": "libart.so",
"off": "0x3f5cbc"
},
{
"i": 216,
"a": "0x7895f3fbb4",
"mod": "libart.so",
"off": "0x3f6bb4"
},
{
"i": 217,
"a": "0x7895f40488",
"mod": "libart.so",
"off": "0x3f7488"
},
{
"i": 218,
"a": "0x7895f40bc4",
"mod": "libart.so",
"off": "0x3f7bc4"
},
{
"i": 219,
"a": "0x7895f41300",
"mod": "libart.so",
"off": "0x3f8300"
},
{
"i": 220,
"a": "0x7895f41354",
"mod": "libart.so",
"off": "0x3f8354"
},
{
"i": 221,
"a": "0x7895f41b34",
"mod": "libart.so",
"off": "0x3f8b34"
},
{
"i": 222,
"a": "0x7895f4232c",
"mod": "libart.so",
"off": "0x3f932c"
},
{
"i": 223,
"a": "0x7895f42b54",
"mod": "libart.so",
"off": "0x3f9b54"
},
{
"i": 224,
"a": "0x7895f43320",
"mod": "libart.so",
"off": "0x3fa320"
},
{
"i": 225,
"a": "0x7895f43b08",
"mod": "libart.so",
"off": "0x3fab08"
},
{
"i": 226,
"a": "0x7895f44244",
"mod": "libart.so",
"off": "0x3fb244"
},
{
"i": 227,
"a": "0x7895f44934",
"mod": "libart.so",
"off": "0x3fb934"
},
{
"i": 228,
"a": "0x7895f44948",
"mod": "libart.so",
"off": "0x3fb948"
},
{
"i": 229,
"a": "0x7895f4495c",
"mod": "libart.so",
"off": "0x3fb95c"
},
{
"i": 230,
"a": "0x7895f44a14",
"mod": "libart.so",
"off": "0x3fba14"
}
]
},
{
"type": "rn-addr",
"rn": "0x7895f3ecbc",
"mod": {
"n": "libart.so",
"off": "0x3f5cbc"
}
},
{
"type": "rn-hooked"
},
{
"type": "reg-call",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"count": 19,
"tid": 30763
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "getCDID",
"sig": "()Ljava/lang/String;",
"mod": "libhydeviceid.so",
"off": "0x20eb08"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "getSDID",
"sig": "()Ljava/lang/String;",
"mod": "libhydeviceid.so",
"off": "0x20ede8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "getHDID",
"sig": "()Ljava/lang/String;",
"mod": "libhydeviceid.so",
"off": "0x20f0c8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "init",
"sig": "()V",
"mod": "libhydeviceid.so",
"off": "0x20e6e4"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "getGUID",
"sig": "()Ljava/lang/String;",
"mod": "libhydeviceid.so",
"off": "0x20f8c8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "getMID",
"sig": "()Ljava/lang/String;",
"mod": "libhydeviceid.so",
"off": "0x20fba8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "linkId",
"sig": "()Z",
"mod": "libhydeviceid.so",
"off": "0x20fe88"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "setPrivacyMode",
"sig": "(Z)V",
"mod": "libhydeviceid.so",
"off": "0x20f3a8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "setUserAction",
"sig": "(Ljava/lang/String;)V",
"mod": "libhydeviceid.so",
"off": "0x20f3b4"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "setAppStatus",
"sig": "(I)V",
"mod": "libhydeviceid.so",
"off": "0x20f888"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "setPhoneInfo",
"sig": "(Ljava/lang/String;)Z",
"mod": "libhydeviceid.so",
"off": "0x2107f0"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "doReportAgain",
"sig": "(Ljava/lang/String;)V",
"mod": "libhydeviceid.so",
"off": "0x20fea0"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "delayCollect",
"sig": "(Ljava/lang/String;)V",
"mod": "libhydeviceid.so",
"off": "0x210348"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "setMotionEvent",
"sig": "(Ljava/lang/String;Ljava/lang/String;)Z",
"mod": "libhydeviceid.so",
"off": "0x210c20"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "uploadScreenRecord",
"sig": "()V",
"mod": "libhydeviceid.so",
"off": "0x2114f8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "uploadScreenShot",
"sig": "()V",
"mod": "libhydeviceid.so",
"off": "0x2114fc"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "updateDisplayInfo",
"sig": "(Ljava/lang/String;)V",
"mod": "libhydeviceid.so",
"off": "0x211500"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "addApplist",
"sig": "(Ljava/lang/String;Ljava/lang/String;)V",
"mod": "libhydeviceid.so",
"off": "0x2119a8"
},
{
"type": "reg",
"cls": "com.huya.security.hydeviceid.NativeEntry",
"name": "requestConfig",
"sig": "()V",
"mod": "libhydeviceid.so",
"off": "0x212a5c"
},
{
"type": "reg-call",
"cls": "com.huyaudb.HuyaAuthCore",
"count": 4,
"tid": 30763
},
{
"type": "reg",
"cls": "com.huyaudb.HuyaAuthCore",
"name": "init",
"sig": "()V",
"mod": "libudbauthunify.so",
"off": "0x2621d4"
},
{
"type": "reg",
"cls": "com.huyaudb.HuyaAuthCore",
"name": "unInit",
"sig": "()V",
"mod": "libudbauthunify.so",
"off": "0x26238c"
},
{
"type": "reg",
"cls": "com.huyaudb.HuyaAuthCore",
"name": "sendMsg",
"sig": "(J[B)[B",
"mod": "libudbauthunify.so",
"off": "0x26244c"
},
{
"type": "reg",
"cls": "com.huyaudb.HuyaAuthCore",
"name": "receiveNet",
"sig": "([BIII)V",
"mod": "libudbauthunify.so",
"off": "0x2626b0"
}
],
"detached": [
{
"reason": "process-terminated",
"detail": "None"
}
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"vals": {
"getGUID": "0a7dfaa882938a6ab502511452142c57",
"getMID": "1e8bdf7d4f7a01d3",
"getCDID": "02df398797432eadefcc12767119ad5e80999389",
"getSDID": "PQwemAN9NHkZKoMqJ1d4FRIypqMTaQEOrmXr37xQVhQZq7L4uCKpS01ntlUkhq1Axdiy9nVNa/2z36z0Vbos+DdHlZJMBBhNP1vehQ3eO5kFXfuv9zZShivBPUqhhwn0zEuX90cIWKYq7Z98gtyiWLHCsORZI73C6IjhfJRFSXZYH0p8ly0=",
"getHDID": "7c5387e0539c023c31c4ff0e807e7256117385ee"
},
"detached": []
}
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""通用 aarch64 反汇编工具: 分析内存 dump 的 libhydeviceid.so (解密后).
用法:
arm64_disasm.py <dump.so> <模块基址> <偏移hex> [长度字节]
例:
反汇编 JNI_OnLoad(0x22c4c8) 前 0x300 字节:
arm64_disasm.py evidence/diag_phone/libhydeviceid_dump.so 0x7814211000 0x22c4c8 0x300
打印所有 BL/BLR 分支目标到模块偏移.
"""
from __future__ import annotations
import sys
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_ARM
def load(fn):
with open(fn, "rb") as f:
return f.read()
def main():
dump_path = sys.argv[1]
base = int(sys.argv[2], 16)
start_off = int(sys.argv[3], 16) # 相对模块基址的偏移 (内存态文件里约等于文件偏移)
length = int(sys.argv[4], 16) if len(sys.argv) > 4 else 0x200
data = load(dump_path)
md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
md.detail = True
code = data[start_off:start_off + length]
print(f"# base=0x{base:x} start_off=0x{start_off:x} len=0x{length:x}")
for insn in md.disasm(code, base + start_off):
disp = ""
if insn.mnemonic.startswith(("b.", "b ")) or insn.mnemonic in ("bl", "br", "blr", "cbz", "cbnz"):
if insn.operands:
disp = f" ; -> 0x{insn.operands[0].imm:x}"
elif insn.mnemonic == "blr":
disp = " ; [indirect call]"
print(f" 0x{insn.address:x}\t{insn.mnemonic:8s} {insn.op_str}{disp}")
if __name__ == "__main__":
main()
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""真机 dump 解密后的 libhydeviceid.so (JNI_OnLoad 壳解密完成后).
用真机稳定注入通道 spawn+art_callsite, 等 libhydeviceid.so 加载且壳解密
(JNI_OnLoad 返回)后 Memory.dump 整个模块, 保存为本地 .so 供反汇编分析.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_dump_hydev.py [serial] [remote]
输出:
evidence/diag_phone/libhydeviceid_dump.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.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--');
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}, 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}
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"]
print(f"[*] 计划 dump {p['count']} 个 r-x 段", 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']}", flush=True)
elif t == "done":
print(f"[*] dump done, got {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)
print("[*] resumed, waiting dump...", flush=True)
t0 = time.time()
while (result["got"] < result["expected"] or result["expected"] == 0) and time.time() - t0 < 20:
time.sleep(0.5)
fh.close()
OUT.chmod(0o644)
print(f"[*] done got={result['got']}/{result['expected']} -> {OUT}", flush=True)
if __name__ == "__main__":
main()
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""真机: 主动 Java 调用 NativeEntry 的 getGUID/getMID/getCDID/getSDID/getHDID, 找 32hex hdid(field1.tag0).
hypasswordLogin 登录帧 field1.tag0 的 32hex hdid 未在 SSL 帧/其他 get* 中抓到,
最可能是 getGUID()/getMID()(之前未触发) 的返回值。本脚本在 Java VM 就绪后
用 Java.use 主动调用全部 get 方法并打印, 确定 32hex hdid 的来源方法。
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_java_call_nativeentry.py [serial] [remote]
输出:
evidence/diag_phone/java_nativeentry.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" / "java_nativeentry.json"
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';
setTimeout(function(){
Java.perform(function(){
var out = {};
try{
var NE = Java.use('com.huya.security.hydeviceid.NativeEntry');
send({type:'class-ok', cls:'com.huya.security.hydeviceid.NativeEntry'});
['init','getGUID','getMID','getCDID','getSDID','getHDID'].forEach(function(m){
try{ out[m] = NE[m](); }catch(e){ out[m] = '<ERR:'+String(e).slice(0,50)+'>'; }
send({type:'val', method:m, value:out[m]});
});
send({type:'all', out:out});
}catch(e){
send({type:'use-err', e:String(e).slice(0,200)});
}
});
}, 6000);
"""
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 = {"vals": {}, "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 == "val":
result["vals"][p["method"]] = p["value"]
print(f"[val] {p['method']} = {p['value']}", flush=True)
elif t == "class-ok":
print(f"[*] class found", flush=True)
elif t == "all":
print(f"[*] 全部读取完成", flush=True)
elif t in ("use-err",):
print(f"[!] use-err: {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(20)
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()
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""真机探针: 枚举 libhydeviceid.so / libudbauthunify.so 导出符号, 定位 32hex hdid 生成入口.
用真机稳定注入通道 (spawn + art_callsite bypass + resume) 长时存活。
App 启动即加载这两个 so, 枚举 exports 拿函数名+基址偏移, 为下一步
针对性 hook 生成函数(抓输入)做准备。
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_hdid.py [serial] [remote]
输出:
evidence/diag_phone/hdid_probe_exports.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_probe_exports.json"
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
ART_CALLSITE = RE / "evidence" / "scripts" / "bypass_msaoaid_maps_art_callsite.js"
PROBE_JS = r"""
'use strict';
function listExports(modName, tag){
try{
var md = Process.findModuleByName(modName);
if(!md){ send({type:'mod', name:modName, state:'not-loaded'}); return false; }
var ex = md.enumerateExports();
var sym = md.enumerateSymbols();
send({type:'mod', name:modName, base:''+md.base, size:md.size,
exports: ex.map(function(e){return {name:e.name, type:e.type, off:''+e.address.sub(md.base)};}),
symCount: sym.length,
syms: sym.map(function(s){return {name:s.name, off:''+s.address.sub(md.base)};})});
return true;
}catch(e){ send({type:'err', name:modName, e:String(e)}); return false; }
}
function hookPropGet(){
var t = Module.findExportByName('libc.so','__system_property_get');
if(!t) return;
Interceptor.attach(t,{
onEnter:function(a){ this.k = a[0].readCString()||''; },
onLeave:function(ret){
try{ send({type:'prop', k:this.k, v:(this.ctx.x1.readCString()||'')}); }catch(e){}
}
});
send({type:'propget-hooked'});
}
// App 启动早期就抓 libhydeviceid.so 加载后的符号 + 派生属性读取
setTimeout(function(){ listExports('libhydeviceid.so','hydev'); }, 2000);
setTimeout(function(){ listExports('libudbauthunify.so','udb'); }, 2000);
setTimeout(hookPropGet, 2000);
"""
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()
d.resume(pid)
print("[*] bypass loaded + resumed", flush=True)
result = {"modules": {}, "props": []}
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 == "mod":
result["modules"][p["name"]] = p
print(f"[mod] {p['name']} base={p.get('base')} exports={len(p.get('exports') or [])} syms={p.get('symCount')}", flush=True)
elif t == "prop":
result["props"].append({"k": p.get("k"), "v": p.get("v")})
elif t == "propget-hooked":
print("[*] __system_property_get hooked", flush=True)
elif t == "err":
print(f"[err] {p.get('name')}: {p.get('e')}", flush=True)
sc = session.create_script(PROBE_JS)
sc.on("message", on_message)
sc.load()
print("[*] probe loaded, collecting 12s", flush=True)
time.sleep(12)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
# 打印关键: 各 so 的导出符号名(去重)
for name, m in result["modules"].items():
print(f"\n=== {name} exports ({len(m.get('exports') or [])}) ===")
for e in (m.get("exports") or [])[:60]:
print(f" 0x{e['off']} {e['name']}")
print("\n=== 派生属性 (去重) ===")
seen = {}
for pr in result["props"]:
seen[pr["k"]] = pr["v"]
for k, v in seen.items():
print(f" {k} = {v}")
print(f"\n[*] saved {OUT}")
if __name__ == "__main__":
main()
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""真机探针 v3: 等 so 加载后 hook, 抓 32hex hdid 生成/使用链路 + 登录触发.
修正 v1/v2: 挂起时 libhydeviceid.so 尚未加载 -> 用 setInterval 轮询,
so 一加载即 hook:
- libhydeviceid.so: 枚举非 runtime 导出 + hook JNI_OnLoad(0x22c4c8)
- libudbauthunify.so: hook BusinessCfg::getHdid(0x26a484) / setSafeDeviceId(0x26a2e0) 带调用栈
resume 后 am start GameSdkLoginActivity 触发设备上报/登录, 让 hdid 被读取/生成.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_hdid3.py [serial] [remote]
输出:
evidence/diag_phone/hdid_probe3.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"
LOGIN_ACT = "com.duowan.kiwi/.loginui.impl.gamesdk.GameSdkLoginActivity"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "hdid_probe3.json"
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';
function stdstr(p){
try{
if(p.isNull()) return '';
var first = p.readU8();
if((first&1)===0){ var l=first>>1; return l? p.add(1).readUtf8String(l):''; }
else { var d=p.readPointer(); var l=p.add(8).readU64(); return (l&&l<256)? d.readUtf8String(l):''; }
}catch(e){ return ''; }
}
var hydev=null, udb=null;
function hookHydev(){
if(hydev) return;
var md = Process.findModuleByName('libhydeviceid.so');
if(!md) return;
hydev = md;
send({type:'hydev', base:''+md.base, size:md.size});
var ex = md.enumerateExports();
var useful = ex.filter(function(e){ return !/^_Z/.test(e.name) && !/^__/.test(e.name) && e.name!==''; });
send({type:'hydev-exports', count:useful.length,
exports: useful.map(function(e){ return {off:''+e.address.sub(md.base), name:e.name}; })});
try{
Interceptor.attach(md.base.add(0x22c4c8), {
onEnter:function(){ send({type:'jni-onload-enter', tid:Process.getCurrentThreadId()}); },
onLeave:function(ret){ send({type:'jni-onload-leave', ret:''+ret}); }
});
send({type:'jni-onload-hooked'});
}catch(e){ send({type:'hydev-err', e:String(e)}); }
}
function hookUdb(){
if(udb) return;
var md = Process.findModuleByName('libudbauthunify.so');
if(!md) return;
udb = md;
send({type:'udb', base:''+md.base});
try{
Interceptor.attach(md.base.add(0x26a484), {
onLeave:function(ret){ send({type:'hdid', str:stdstr(ret), tid:Process.getCurrentThreadId()}); }
});
send({type:'hdid-hooked'});
}catch(e){ send({type:'udb-err', e:String(e)}); }
try{
Interceptor.attach(md.base.add(0x26a2e0), {
onEnter:function(a){
send({type:'setsd', sd:stdstr(a[1]), hd:stdstr(a[2]), tid:Process.getCurrentThreadId()});
try{
var bt=Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0,20).map(function(x){return x.toString();});
send({type:'bt', bt:bt});
}catch(e){}
}
});
send({type:'setsd-hooked'});
}catch(e){}
}
setInterval(hookHydev, 40);
setInterval(hookUdb, 40);
"""
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 = {"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 {}
result["events"].append(p)
t = p.get("type")
if t in ("hdid", "setsd"):
print(f"[{t}] {p.get('hd') or p.get('str')} tid={p.get('tid')}", flush=True)
elif t == "bt":
print(" bt:", " <- ".join(p["bt"][:8]), flush=True)
elif t == "hydev":
print(f"[*] libhydeviceid loaded base={p.get('base')}", flush=True)
elif t == "hydev-exports":
print(f"[*] hydev 非runtime导出 {p.get('count')}:", flush=True)
for e in (p.get("exports") or [])[:40]:
print(f" 0x{e['off']} {e['name']}")
elif t in ("udb","hdid-hooked","setsd-hooked","jni-onload-hooked","jni-onload-enter","jni-onload-leave"):
print(f"[*] {t}", p if t in ("jni-onload-leave",) else "", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(5)
print("[*] 触发登录 Activity...", flush=True)
adb("shell", "am", "start", "-n", LOGIN_ACT)
time.sleep(15)
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()
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""真机探针 v4: 抓 libhydeviceid.so 生成 hdid 的输入(系统属性/文件/随机源).
libhydeviceid.so 加壳, 生成函数不导出。改为抓它的"输入":
- hook libc __system_property_get (spawn 挂起时已加载) -> App 启动读的属性
- hook libhydeviceid 的 hy_fopen64/hy_read/hy_syscall (壳文件包装) -> 访问的文件
- hook getHdid(0x26a484) 时同时 dump x0 原始字节, 定位为何返回空
这能判断 hdid 依赖 ro.serialno / ANDROID_ID / /proc 信息 / 随机源等, 决定能否复现.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_hdid4.py [serial] [remote]
输出:
evidence/diag_phone/hdid_probe4.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"
LOGIN_ACT = "com.duowan.kiwi/.loginui.impl.gamesdk.GameSdkLoginActivity"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "hdid_probe4.json"
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';
function stdstr(p){
try{
if(p.isNull()) return '';
var first = p.readU8();
if((first&1)===0){ var l=first>>1; return l? p.add(1).readUtf8String(l):''; }
else { var d=p.readPointer(); var l=p.add(8).readU64(); return (l&&l<512)? d.readUtf8String(l):''; }
}catch(e){ return ''; }
}
// 1) spawn 挂起时 hook __system_property_get (libc 已加载)
(function(){
try{
var t = Module.findExportByName('libc.so','__system_property_get');
if(!t){ send({type:'no-propget'}); return; }
Interceptor.attach(t,{
onEnter:function(a){ this.k = a[0].readCString()||''; this.vp = a[1]; },
onLeave:function(ret){ try{ send({type:'prop', k:this.k, v:this.vp.readCString()||''}); }catch(e){ send({type:'prop', k:this.k, v:'<err>'}); } }
});
send({type:'propget-hooked'});
}catch(e){ send({type:'propget-err', e:String(e)}); }
})();
var hydev=null;
function hookHydev(){
if(hydev) return;
var md = Process.findModuleByName('libhydeviceid.so');
if(!md) return;
hydev = md;
send({type:'hydev', base:''+md.base});
// 文件操作壳包装: hy_fopen64(0xf1298), hy_read(0x30bef0), hy_syscall(0x30bf3c)
var targets = {0xf1298:'hy_fopen64', 0x30bef0:'hy_read', 0x30bf3c:'hy_syscall', 0x30bf00:'hy_write', 0xf3360:'hyftell'};
Object.keys(targets).forEach(function(off){
try{
Interceptor.attach(md.base.add(parseInt(off,16)), {
onEnter:function(a){
var s='';
try{ s = (a[0]&&!a[0].isNull()) ? a[0].readCString(256)||'' : ''; }catch(e){}
send({type:'hyio', fn:targets[off], arg0:s, tid:Process.getCurrentThreadId()});
}
});
}catch(e){}
});
send({type:'hyio-hooked'});
}
// getHdid 返回空排查: dump x0 原始字节
function hookUdb(){
var md = Process.findModuleByName('libudbauthunify.so');
if(!md) return;
try{
Interceptor.attach(md.base.add(0x26a484), {
onLeave:function(ret){
var s=stdstr(ret);
var hex='';
try{ hex = Array.from(new Uint8Array(ret.readByteArray(64))).map(function(b){return ('0'+b.toString(16)).slice(-2);}).join(''); }catch(e){}
send({type:'hdid', str:s, ret:''+ret, hex:hex, tid:Process.getCurrentThreadId()});
}
});
send({type:'hdid-hooked'});
}catch(e){ send({type:'udb-err', e:String(e)}); }
}
setInterval(hookHydev, 40);
setInterval(hookUdb, 40);
"""
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": [], "hyio": [], "hdid": [], "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 == "prop":
result["props"].append(p); print(f"[prop] {p.get('k')} = {p.get('v')}", flush=True)
elif t == "hyio":
result["hyio"].append(p); print(f"[hyio] {p.get('fn')} arg0={p.get('arg0')}", flush=True)
elif t == "hdid":
result["hdid"].append(p); print(f"[hdid] str={p.get('str')!r} ret={p.get('ret')} hex={p.get('hex')}", flush=True)
elif t in ("propget-hooked","hydev","hyio-hooked","hdid-hooked","no-propget"):
print(f"[*] {t}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(4)
print("[*] 触发登录 Activity...", flush=True)
adb("shell", "am", "start", "-n", LOGIN_ACT)
time.sleep(15)
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()
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""真机 v6: hook SSL_write 抓 hypasswordLogin 登录帧, 提取实际 hdid + 追来源.
目标: 确认登录帧 t1.t0 的 32hex hdid 实际值(应为金样本 ed0db8...),
并 hook 它进入 WUP 帧前被读取的调用链。同时 hook:
- libc strstr/strcpy 等 (登录帧组装时 hdid 字符串被拷贝处)
- BusinessCfg::getSafeDeviceId(0x26a3c4) / setDeviceInfo(0x26a894)
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_hdid6.py [serial] [remote]
输出:
evidence/diag_phone/hdid_probe6.json
"""
from __future__ import annotations
import json
import re
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"
LOGIN_ACT = "com.duowan.kiwi/.loginui.impl.gamesdk.GameSdkLoginActivity"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "hdid_probe6.json"
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';
function hexb(p,n){try{return Array.from(new Uint8Array(p.readByteArray(n))).map(b=>('0'+b.toString(16)).slice(-2)).join('');}catch(e){return '';}}
function stdstr(p){
try{
if(p.isNull()) return '';
var f = p.readU8();
if((f&1)===0){ var l=f>>1; return l? p.add(1).readUtf8String(l):''; }
else { var d=p.readPointer(); var l=p.add(8).readU64(); return (l&&l<512)? d.readUtf8String(l):''; }
}catch(e){ return ''; }
}
var sslDone=false, udbHooked=false;
function hookSSL(){
if(sslDone) return;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<50||len>50000)return;
var h=''; try{ h=a[1].readCString(Math.min(len,600))||''; }catch(e){}
if(h.indexOf('hypasswordLogin')>=0){
send({type:'wup_login', len:len, hex:hexb(a[1],len)});
}
}});
});
send({type:'sslwrite-hooked'});
}catch(e){}
sslDone=true;
}
function hookUdb(){
if(udbHooked) return;
var md = Process.findModuleByName('libudbauthunify.so');
if(!md) return;
udbHooked=true;
send({type:'udb', base:''+md.base});
// 设备信息读取点
try{
Interceptor.attach(md.base.add(0x26a3c4), { // getSafeDeviceId
onLeave:function(ret){ send({type:'getsd', ret:''+ret}); }
});
}catch(e){}
try{
Interceptor.attach(md.base.add(0x26a894), { // setDeviceInfo(a1=this, a2..a5=std::string*)
onEnter:function(a){
send({type:'setdi', args:[stdstr(a[1]), stdstr(a[2]), stdstr(a[3]), stdstr(a[4])]});
}
});
}catch(e){}
send({type:'udb-hooked'});
}
// libhydeviceid 导出函数调用探测
var hyTried=false;
function hookHy(){
if(hyTried) return;
var md = Process.findModuleByName('libhydeviceid.so');
if(!md) return;
hyTried=true;
send({type:'hydev', base:''+md.base});
['hyfopen64','hy_read','hy_syscall','hy_write','hyftell'].forEach(function(n){
try{
var a = Module.findExportByName('libhydeviceid.so', n);
if(a) Interceptor.attach(a,{onEnter:function(args){
var s=''; try{ s=args[0].readCString(128)||''; }catch(e){}
send({type:'hycall', fn:n, a0:s, tid:Process.getCurrentThreadId()});
}});
}catch(e){}
});
send({type:'hydev-hooked'});
}
setInterval(hookSSL, 800);
setInterval(hookUdb, 50);
setInterval(hookHy, 50);
"""
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 = {"frames": [], "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 == "wup_login":
result["frames"].append(p)
b = bytes.fromhex(p.get("hex", ""))
# 提取帧里的 32hex hdid
h32 = re.findall(rb"[0-9a-f]{32}", b)
print(f"[WUP_LOGIN] len={p.get('len')} hdid候选: {[h.decode() for h in h32[:3]]}", flush=True)
elif t in ("udb","udb-hooked","sslwrite-hooked","hydev","hydev-hooked"):
print(f"[*] {t}", p if t=="udb" else "", flush=True)
elif t == "setdi":
print(f"[setDeviceInfo] args={p.get('args')}", flush=True)
elif t == "getsd":
print(f"[getSafeDeviceId] ret={p.get('ret')}", flush=True)
elif t == "hycall":
print(f"[hycall] {p.get('fn')} a0={p.get('a0')}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(4)
print("[*] 触发登录...", flush=True)
adb("shell", "am", "start", "-n", LOGIN_ACT)
time.sleep(18)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] 登录帧数: {len(result['frames'])} saved {OUT}")
if __name__ == "__main__":
main()
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""真机 v9: 触发一键登录, hook SSL_write 抓 WUP 登录帧 + createWupDeviceInfo(0x2746a0),
从登录帧提取 32hex hdid (t1.t0) 和其来源.
触发: 点击 GameSdkLoginActivity 的"本机号码一键登录" (mBtnLogin, 中心 540,1287),
App 会自动发起登录 WUP 请求, 帧内经 JCE 编码含 t1.t0=hdid 字段.
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_login_frame.py [serial] [remote]
输出:
evidence/diag_phone/login_frames.json
"""
from __future__ import annotations
import json
import re
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"
LOGIN_ACT = "com.duowan.kiwi/.loginui.impl.gamesdk.GameSdkLoginActivity"
REPO = Path("/Users/yml/codes/douyu_login_py")
OUT = REPO / "evidence" / "diag_phone" / "login_frames.json"
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 sslDone=false, udbHooked=false, cdiHooked=false;
function hookSSL(){
if(sslDone) return;
try{
var r=new ApiResolver('module');
r.enumerateMatchesSync('exports:*!SSL_write').forEach(function(m){
Interceptor.attach(m.address,{onEnter:function(a){
var len=a[2].toInt32();
if(len<200||len>80000) return;
try{
var buf=a[1].readByteArray(len);
send({type:'sslfr', len:len, hex:Array.from(new Uint8Array(buf)).map(function(b){return ('0'+b.toString(16)).slice(-2);}).join('')});
}catch(e){}
}});
});
send({type:'ssl-hooked'});
}catch(e){ send({type:'ssl-err', e:String(e)}); }
sslDone=true;
}
function hookUdb(){
if(udbHooked) return;
var md=Process.findModuleByName('libudbauthunify.so');
if(!md) return;
udbHooked=true;
send({type:'udb', base:''+md.base});
try{ // BusinessCfg::getHdid
Interceptor.attach(md.base.add(0x26a484),{onLeave:function(ret){
var s='<n>';
try{ s = ret.isNull()? '<null>' : ret.readCString(64)||'<?>'; }catch(e){ s='<err>'; }
var bt='';
try{ bt = Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0,16).map(function(x){return x.toString();}).join(' <- '); }catch(e){}
send({type:'getHdid', ret:''+ret, str:s, bt:bt});
}});
}catch(e){}
try{ // createWupDeviceInfo(wup::DeviceInfo*)
Interceptor.attach(md.base.add(0x2746a0),{onEnter:function(a){ send({type:'createWupDev-enter', tid:Process.getCurrentThreadId()}); }});
}catch(e){}
send({type:'udb-hooked'});
}
setInterval(hookSSL, 500);
setInterval(hookUdb, 60);
"""
def adb(*a):
return subprocess.run(["adb", "-s", SERIAL, *a], capture_output=True, text=True)
def tap(x, y):
adb("shell", f"input tap {x} {y}")
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 = {"frames": [], "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 == "sslfr":
result["frames"].append(p)
b = bytes.fromhex(p.get("hex", ""))
# WUP/JCE 帧特征: 找 t1.t0 的 32hex 候选
h32 = list(set(x.decode() for x in re.findall(rb"[0-9a-f]{32}", b)))
h40 = list(set(x.decode() for x in re.findall(rb"[0-9a-f]{40}", b)))
print(f"[ssl] len={p['len']} 32hex候选={h32[:4]} 40hex候选={h40[:2]}", flush=True)
elif t in ("udb","udb-hooked","ssl-hooked"):
print(f"[*] {t}", p if t=="udb" else "", flush=True)
elif t == "getHdid":
print(f"[getHdid] ret={p.get('ret')} str={p.get('str')[:60]!r}", flush=True)
if p.get("bt"):
print(f" bt: {p['bt']}", flush=True)
elif t == "createWupDev-enter":
print(f"[createWupDeviceInfo] enter tid={p.get('tid')}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed", flush=True)
time.sleep(6)
# 打开登录页 (不自动点击, 交给用户手动切账号密码登录)
adb("shell", "am", "start", "-n", LOGIN_ACT)
time.sleep(3)
duration = int(sys.argv[3]) if len(sys.argv) > 3 else 240
print(f"[*] 探针持续抓帧 {duration}s, 请手动登录...", flush=True)
time.sleep(duration)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"[*] 帧数: {len(result['frames'])} saved {OUT}")
if __name__ == "__main__":
main()
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""真机探针 v2: 抓 libhydeviceid.so 经 RegisterNatives 注册的 native 方法 + getHdid 调用栈.
libhydeviceid.so 没有 JNI/生成函数导出(核心方法经 JNI_OnLoad -> RegisterNatives 动态注册)。
本脚本在 spawn 挂起中 hook libart.so 的 RegisterNatives, App 启动早期注册会被捕获,
拿到 libhydeviceid.so 注册的 native 方法指针 -> 基址偏移, 定位 hdid 生成入口。
同时 hook libudbauthunify.so 的 BusinessCfg::getHdid(0x26a484) 抓 hdid 值+调用栈。
用法:
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python \
scripts/phone_probe_registernatives.py [serial] [remote]
输出:
evidence/diag_phone/hdid_registernatives.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_registernatives.json"
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';
function stdstr(p){
try{
if(p.isNull()) return '';
var first = p.readU8();
if((first & 1)===0){ var l=first>>1; return l? p.add(1).readUtf8String(l):''; }
else { var d=p.readPointer(); var l=p.add(8).readU64(); return (l&&l<256)? d.readUtf8String(l):''; }
}catch(e){ return ''; }
}
function modOf(a){ try{ var m=Process.findModuleByAddress(a); return m? {n:m.name, off:''+a.sub(m.base)}: {n:'?', off:'?'}; }catch(e){ return {n:'?',off:'?'}; } }
// 1) hook libart RegisterNatives
(function(){
var RN = Module.findExportByName('libart.so','_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi');
if(!RN){ send({type:'rn-notfound'}); return; }
send({type:'rn-found', a:''+RN});
Interceptor.attach(RN,{
onEnter:function(a){
var count = a[3].toInt32();
if(count<=0||count>500) return;
var cls='';
try{ cls = Java.vm.tryGetEnv().getClassName(a[1])||''; }catch(e){ cls='?'; }
for(var i=0;i<count;i++){
try{
var base=a[2].add(i*24);
var name=base.readPointer().readCString()||'';
var sig =base.add(8).readPointer().readCString()||'';
var fn =base.add(16).readPointer();
var mo=modOf(fn);
send({type:'reg', cls:cls, name:name, sig:sig, mod:mo.n, off:mo.off});
}catch(e){}
}
}
});
send({type:'rn-hooked'});
})();
// 2) hook BusinessCfg::getHdid (libudbauthunify.so 0x26a484)
(function(){
var md = Process.findModuleByName('libudbauthunify.so');
if(!md){ send({type:'udb-not-loaded'}); return; }
try{
Interceptor.attach(md.base.add(0x26a484),{
onLeave:function(ret){
var s=stdstr(ret);
send({type:'hdid', str:s, tid:Process.getCurrentThreadId()});
}
});
Interceptor.attach(md.base.add(0x26a2e0),{ // setSafeDeviceId(a2=sd, a3=hdid)
onEnter:function(a){
send({type:'setsd', sd:stdstr(a[1]), hd:stdstr(a[2]), tid:Process.getCurrentThreadId()});
try{
var bt=Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0,24).map(function(x){return x.toString();});
send({type:'bt', bt:bt});
}catch(e){}
}
});
send({type:'udb-hooked'});
}catch(e){ send({type:'udb-err', e:String(e)}); }
})();
"""
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()
print("[*] art_callsite loaded", flush=True)
result = {"regs": [], "hdid": [], "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 == "reg":
result["regs"].append(p)
elif t in ("hdid", "setsd"):
result["hdid"].append(p)
print(f"[{t}] {p}", flush=True)
elif t == "bt":
print(" bt:", " <- ".join(p["bt"][:8]), flush=True)
elif t in ("rn-found","rn-hooked","udb-hooked"):
print(f"[*] {t}", flush=True)
sc = session.create_script(JS)
sc.on("message", on_message)
sc.load()
d.resume(pid)
print("[*] resumed, collecting 18s", flush=True)
time.sleep(18)
result["detached"] = det
OUT.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding="utf-8")
# 汇总 libhydeviceid.so 注册的方法
print("\n=== libhydeviceid.so 注册的 native 方法 ===")
seen = set()
for r in result["regs"]:
if r.get("mod") == "libhydeviceid.so" and (r["name"], r["off"]) not in seen:
seen.add((r["name"], r["off"]))
print(f" 0x{r['off']} {r['name']}{r['sig']} (class={r.get('cls')})")
print(f"\n=== 所有 so 注册统计 ===")
from collections import Counter
c = Counter(r.get("mod") for r in result["regs"])
for mod, n in c.most_common():
print(f" {mod}: {n}")
print(f"\n[*] saved {OUT}")
if __name__ == "__main__":
main()
+168
View File
@@ -0,0 +1,168 @@
#!/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()
+149
View File
@@ -0,0 +1,149 @@
#!/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()