继续破解进度
This commit is contained in:
@@ -127,3 +127,147 @@ t=3695: 17 f9 70 f9 b1 3c 4c be 77 74 14 ee 5e 7c e0 88 3a e2 c3 2c 72 63 8c 10
|
||||
|
||||
**验证 keystream 种子**:evidence/dfp_pair_sync.json 有 5 组同步 pair,
|
||||
两组 keystream 差 vs 时间戳差 → 判断种子是否时间戳/计数器。
|
||||
|
||||
---
|
||||
|
||||
## 八、本会话重大发现(新!)
|
||||
|
||||
### 1. 输入结构确定: [恒定JSON 554B][collection ~3400B 每轮变化]
|
||||
- 恒定 JSON(554B,已有全部字段,含 hdid/deviceId):`{"appId":"5008",...,"widthPixels":"1080"}`
|
||||
- collection 是大 JSON:`{"touch_events":[...],"net_intfs":"...","phoneInfo":"{...}","qimei16/36":"...","risk_apps":"com.topjohnwu.magisk|bin.mt.plus|","scanned_appname":"<binary ~2900B>",...}`
|
||||
- collection 尾部含 **Poseidon 反 frida 检测字段**:`"Poseidon_D1":"libhydeviceid.so|hooked:libhydeviceid.so|hooked:libc.so|"`, `Poseidon_D3:"zygisk|"`, `Poseidon_D5:"/product/bin/su\n|"`, `adb_input_list:"device_name=Virtual|"`(huyaudb 读 /proc/self/maps 检测 hook)
|
||||
- collection 每轮变化(触摸事件/权限/网络/反检测结果/扫描二进制)
|
||||
|
||||
### 2. keystream 每轮变化,但跨轮/跨会话有强共享结构(非纯随机!)
|
||||
|
||||
多 session(6+ 个 pipeline 证据文件)9+ 轮 ks[0..553]=cipher[:554]^JSON 分析:
|
||||
- **跨会话模板复用**:相同 10B 前缀在多个独立 session 反复出现!
|
||||
- `07f970f9b8bc50be774e` ×3(155601-r1, 160316-r1, 160316-r2)
|
||||
- `17f97019b93c6cbe7764` ×3(155601-r3, 160316-r3, 160855-r2, 170031-r2)
|
||||
- `17f97ef9b83c6cbe774a` ×3(args session 多轮)
|
||||
- `07f870f9b8bc50be774e` ×2(v2)
|
||||
- 同一 session 连续轮次可完全相同(160316: r1 = r2 = `07f970f9b8bc50be774e`)
|
||||
- 每位置 ks 值域很小:8 轮样本中大部分位置只有 2~8 个不同值(随机期望 8),前 6 字节仅 2~3 个值
|
||||
- **高共享对**:r2(160855,t=18273) vs r5(170031,t=100816) 共享 140/554B(25%;随机期望 0.4%),最长连续 20B;r0 vs r4 共享 80B
|
||||
- 结论:**keystream 不是每会话随机种子动态生成,而更像 固定模板库 + 状态选择**(生成器需进一步确认,但"所有字节每轮全新随机"的假设被推翻)
|
||||
|
||||
### 3. 反 frida 检测: hook 了 libhydeviceid 的事件, collection JSON 的 Poseidon 字段值相应变化(每轮检测结果不同)
|
||||
|
||||
### 4. 骨架 pipeline hook 已稳定(轻量,符合约定)
|
||||
- `scripts/hook_dfp_pipeline.py` 只 hook 6 函数(topA_20e924/midA_5a938/cryptoA_5b1f4/copy_61098/pktA_7f744/pktB_92128)
|
||||
- **必须遵守 hook 约定**:spawn → bypass x2 → resume+11s → patch_guard → **最后**加载主 hook JS(顺序颠倒会卡启动界面,已踩坑多次)
|
||||
- 事件每轮:copy_61098 ×2(MAGIC 缓冲)、pktA/pktB(MAGIC + taf 段)、wire(密文全长)、jsonhits(内存搜 JSON 完整 554B)
|
||||
- **GOT 桩方案已放弃**:144 个 br x17 桩 hook 太重,App 卡启动/卡点击(动态间接调用表 0x3ba000/0x3b9000 运行时才解密,静态也解不出)
|
||||
|
||||
### 5. 本地 capstone 反汇编(ELF vaddr==fileoff)
|
||||
- cryptoA_5b1f4: 16 个 bl 目标(0x5f1b4 首调、0x45aa0×26、0x76ab0×9、0x461c0×6、0x6ec68×4、0x7398c×3、0x1f07ec×2…)
|
||||
- 0x461c0/0x45aa0 = **间接调用桩**(adrp x16;ldr x17,[x16,#off];br x17),GOT 表运行时解密失败静态解析
|
||||
- 0x5f1b4 = 字符串构建函数(状态机混淆),非生成器;cryptoA 主体无标量/NEON XOR 循环 → XOR 内核在间接调用或查表中
|
||||
|
||||
### 6. 下一步(铸造路线评估,优先!)
|
||||
- 服务端旧结论:重放任意 wire body → HTTP 200,hdid/device_id = 明文回显
|
||||
- 若 keystream 真是模板库,铸造新设备 = 改明文 JSON(hdid/deviceId) + 复用任意一轮 keystream XOR → 重放
|
||||
- **行动**:纯 Python 构造新设备 t2(改明文 + 模板 ks)重放验证 200 → 若成功,铸造路线打通,生成器破解降级为可选
|
||||
|
||||
---
|
||||
|
||||
## 九、铸造路线实验(本会话最新,部分成功!)
|
||||
|
||||
### 1. 纯 Python 重放 100% 可行(不需要 keystream!)
|
||||
- `scripts/forge_replay.py`:取证据里一帧完整 wire → 重构 HTTP/1.1 POST(Content-Length 等长) → socket+TLS 直连 `wsapi.huya.com:443` → **原样重放返回 `HTTP/1.1 200 OK` + 344B 响应**(稳定复现)
|
||||
- **XOR 自反性铸造**:`新密文 = 原密文 XOR (原JSON XOR 新JSON)`,JSON 区(554B)等长替换 hdid/deviceId/appkey,collection 区不动 → 不需要 ks、不需要生成器、不需要设备!
|
||||
- 服务端**确实解析 t2 明文**(见下),不是纯透传 200
|
||||
|
||||
### 2. 响应结构(344B taf/wup 包)
|
||||
- 4B 总长 + 头 + `huyaudbwebui` `dfpReport` + 字段:
|
||||
- **t1 (tag22)**: `865a4924a40897ac1fcfe6b4c2cbb045` + `&` + base64(~43B,每次变化,像会话加密数据)
|
||||
- **tag70**: `actionV(<40hex>` (设备标识回显)
|
||||
- 40hex 之后有 taf 尾字段(0x0b 8c... 未解析)
|
||||
|
||||
### 3. 服务端行为(受控实验)
|
||||
| 请求 | actionV(40hex) |
|
||||
|---|---|
|
||||
| baseline 原样 | `7c5387e0...17385ee`(**= 明文 hdid!**) |
|
||||
| 只改 hdid | 新值(≠ 新 hdid,每次实验都不同) |
|
||||
| 只改 deviceId | 新值(≠ 新 deviceId) |
|
||||
| 用 actionV1 当 hdid 再上报 | 又一个新值(不稳定) |
|
||||
|
||||
- actionV 对 hdid/deviceId 变化均敏感 → **服务端解密解析了 t2 明文** ✓
|
||||
- baseline 时 actionV == 明文 hdid(服务端回显/认可当前设备)
|
||||
- 改身份后 actionV 每次都是新值且不稳定 → 未确认"下发新设备身份"机制,可能 actionV 含时间戳/会话随机,或需配合后续登录流程
|
||||
|
||||
### 4. 待验证决策点
|
||||
1. actionV 是否 = HMAC/带时间戳编码(需要多次同明文请求对照:两次同明文 → actionV 相同则与明文确定性相关;不同则含随机)
|
||||
2. 需要抓完整登录流程(登录请求也用同一 hdid?)确认铸造出的新身份能否通过登录
|
||||
3. 若 actionV 含随机/时间戳 → 服务端可能靠 t1 或其它字段做设备绑定 → 需重新审视铸造入口
|
||||
|
||||
---
|
||||
|
||||
## 十、29 轮 keystream 全量分析(决定性结论!)
|
||||
|
||||
合并 6+ session、29 轮 ks[0..553] 分析:
|
||||
|
||||
### 1. keystream 前缀模板复用(铁证)
|
||||
```
|
||||
17f97019b93c6cbe7764 x4: 160316r3, 160855r2, 170031r2, 175740r2 ← 4 个不同 session 同轮次位置!
|
||||
07f970f9b8bc50be774e x3: 155601r1, 160316r1, 160316r2
|
||||
17f97ef9b83c6cbe774a x3: args.j r3/4/5
|
||||
07f870f9b8bc50be774e x2: v2 r1/r2
|
||||
```
|
||||
- 完整 10B 前缀跨 session 精确重复,**不是随机**
|
||||
|
||||
### 2. 同 session 完全重复
|
||||
- **args.j r3 = r4 = r5: ks[0..553] 全部 554 字节相同!**(shared=554)
|
||||
- v2 r1≈r2(51 共享),160316 r1≈r2(36 共享)
|
||||
|
||||
### 3. 高共享对(跨 session)
|
||||
- 160855r2 vs 170031r2: **140/554 共享,最长连续 20B**
|
||||
- 170031r3 vs 175740r3: 97 共享
|
||||
- 170031r2 vs 175740r2: 70 共享,最长连续 31B
|
||||
- 160855r1 vs 160855r4: 80 共享(同 session 首尾)
|
||||
|
||||
### 4. 结论
|
||||
- **keystream 与"轮次序号/状态"强相关,同一序号位置在多个 session 复用相同(或高相似)ks**
|
||||
- 不是每 session 随机种子;更像 **固定生成器 + 状态/序号选择**(同轮次位置 → 同 ks)
|
||||
- 对铸造的含义:任意一帧真实 ks 即可加密伪造明文(服务端行为见下节,这是关键限制)
|
||||
|
||||
### 5. 服务端铸造验证(forge_replay.py 实测,重要!)
|
||||
- **原样重放**:HTTP 200,344B 响应稳定,actionV = `7c5387...`(= 真 hdid,稳定回显)
|
||||
- **改明文 JSON 任何身份字段(hdid/deviceId/appkey)**:200 OK,但 actionV 变为
|
||||
`[32hex 类MD5] + "10" + [6hex]` 动态 token,同明文两次上报 token 不同
|
||||
- 响应 t1(tag22)恒定 `865a4924a40897ac1fcfe6b4c2cbb045&base64`(base64 部分每次变化)
|
||||
- **推论**:服务端识别"真设备"靠 明文三元组 (hdid, deviceId, appkey) 与库匹配;
|
||||
纯改明文不匹配 → 动态 token → **铸造新设备需同时伪造三元组一致的完整设备身份**
|
||||
(可能还要 collection 里 qimei/device_id 对应)否则服务端不认可
|
||||
|
||||
### 6. 下一步两个方向
|
||||
A. **铸造**:找出服务端设备三元组的匹配逻辑(抓真实 App 首次注册流程/或抓登录后的完整身份链),
|
||||
纯 Python 全真伪造(JSON+collection 齐改)重放验证 actionV 稳定性
|
||||
B. **生成器**:既然 ks 与轮次序号相关,继续定位生成函数(0x5f1b4 初始化 + cryptoA 调用链),
|
||||
复现 keystream 生成,任意轮次任意长度可算
|
||||
|
||||
---
|
||||
|
||||
## 十一、服务端设备绑定校验(差分实验定论!)
|
||||
|
||||
对 t2 密文做逐区差分修改重放,观察服务端响应:
|
||||
|
||||
| 修改内容 | actionV 结果 |
|
||||
|---|---|
|
||||
| baseline 原样 | **稳定回显真 hdid** `7c5387...` |
|
||||
| JSON 非身份字段(widthPixels 1080→1081) | **稳定回显真 hdid**(不受影响!) |
|
||||
| JSON 身份字段 hdid | 动态 token `[MD5样式32hex]10[6hex]`,每次不同 |
|
||||
| JSON 身份字段 deviceId | 动态 token |
|
||||
| JSON 身份字段 appkey | 动态 token |
|
||||
| collection 区任意 1 字节(@560/@617/@753/@953 等) | 动态 token(波及全 collection) |
|
||||
|
||||
**结论:服务端绑定校验 = (hdid, deviceId, appkey 三元组) + (collection 完整性)**
|
||||
- 服务端解密 t2 → 解析 JSON 三元组 + collection → 与库中设备匹配 → 匹配则回显设备 hdid,不匹配则返回动态 token
|
||||
- **动态 token 不可复用**(同明文两次上报不同)→ 不是"注册下发",更像"未知设备拒绝标记"
|
||||
- 铸造的真正瓶颈不是加密(已破解),而是**服务端以 (三元组, collection) 识别设备**
|
||||
- collection 含 qimei16/36、scanned_appname 等指纹 → 需与三元组自洽才能被服务端认可
|
||||
|
||||
### 后续(铸造完整方案待定)
|
||||
1. 抓真实 App **首次注册/重置设备后**的第一帧 dfpReport(看服务端如何初始分配设备 id)
|
||||
2. 或逆向 collection 的指纹算法(哪些字段参与绑定)
|
||||
3. 验证"设备上等长 patch hdid + App 自行生成 t2"路线(修复 patch 崩溃:可能因 JSON 长度校验/内存写坏)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"new_hdid": "609375c8df11e6abe2600f15df3299db74ab4512",
|
||||
"new_devid": "31fbba051fa349777adbcd3ddadc4b75469c75ef",
|
||||
"new_appkey": "6c29378b49c8671e9797e587f14fae24",
|
||||
"new_json": "{\"appId\":\"5008\",\"appVer\":\"13.4.22\",\"appkey\":\"6c29378b49c8671e9797e587f14fae24\",\"channel\":\"xiaomi\",\"deviceId\":\"31fbba051fa349777adbcd3ddadc4b75469c75ef\",\"deviceName\":\"M2102J2SC\",\"hdid\":\"609375c8df11e6abe2600f15df3299db74ab4512\",\"heightPixels\":\"2120\",\"isCloud\":0,\"isForbidLog\":1,\"isHome\":0,\"isPre\":0,\"openAppId\":\"\",\"savePath\":\"/data/user/0/com.duowan.kiwi/files/huyaudb\",\"sdkVer\":\"1.0.80138\",\"servantName\":\"huyaudbwebui\",\"shareAppDataPath\":\"/data/user/0/com.duowan.kiwi/files/huyaudb\",\"systemInfo\":\"android\",\"systemVer\":\"M2102J2SC,30,11\",\"terminalType\":1,\"",
|
||||
"exp1_status": "HTTP/1.1 200 OK",
|
||||
"exp2_status": "HTTP/1.1 200 OK",
|
||||
"exp2_resp": "0000015810032c3100804c560c68757961756462776562756966096466705265706f72747d0001012a08000206001d0000010c0604745273701d000101150a060231301620383635613439323461343038393761633166636665366234633263626230343526b4505177656d414e394e486b5a4b6f4d71584b355178424979707149546151454f726d4873627143555a68515a714c444932412b685831747472"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""铸造验证 v1: 重放 dfpReport wire → 服务端回显(不动设备)。
|
||||
|
||||
路线(XOR 自反性,不需要 keystream):
|
||||
密文 = 明文 XOR ks => 新密文 = 原密文 XOR (原JSON XOR 新JSON)
|
||||
JSON 区(前 554B)等长替换 hdid/deviceId/appkey,collection 区保持原样。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import binascii
|
||||
import json
|
||||
import random
|
||||
import ssl
|
||||
import socket
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
EVID = Path("/Users/yml/codes/douyu_login_py/evidence")
|
||||
HOST = "wsapi.huya.com"
|
||||
|
||||
MAGIC = bytes.fromhex("571882cf664bb39401ee")
|
||||
|
||||
|
||||
def load_first_wire():
|
||||
for f in sorted((EVID / "dfp_pipeline_170031.json", EVID / "dfp_pipeline_160855.json"),
|
||||
reverse=True):
|
||||
d = json.load(open(f))
|
||||
for e in d:
|
||||
if e.get("type") == "wire":
|
||||
return binascii.unhexlify(e["hex"])
|
||||
raise SystemExit("no wire found")
|
||||
|
||||
|
||||
def send_http(host, path, body, timeout=60):
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
raw = socket.create_connection((host, 443), timeout=10)
|
||||
s = ctx.wrap_socket(raw, server_hostname=host)
|
||||
req = (
|
||||
f"POST {path} HTTP/1.1\r\n"
|
||||
f"i-ver: 1\r\n"
|
||||
f"Content-Type: application/octet-stream\r\n"
|
||||
f"Content-Length: {len(body)}\r\n"
|
||||
f"Host: {HOST}\r\n"
|
||||
f"Connection: Keep-Alive\r\n"
|
||||
f"Accept-Encoding: identity\r\n"
|
||||
f"User-Agent: okhttp/3.14.9\r\n"
|
||||
f"\r\n"
|
||||
).encode() + body
|
||||
s.sendall(req)
|
||||
s.settimeout(timeout)
|
||||
buf = b""
|
||||
try:
|
||||
while True:
|
||||
chunk = s.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
# 响应头拿到 + 至少等 2s 累积 body
|
||||
if b"\r\n\r\n" in buf and len(buf) > 300:
|
||||
time.sleep(2)
|
||||
try:
|
||||
while True:
|
||||
c = s.recv(4096)
|
||||
if not c:
|
||||
break
|
||||
buf += c
|
||||
except socket.timeout:
|
||||
break
|
||||
break
|
||||
finally:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
return buf
|
||||
|
||||
|
||||
def gen_random_hex(n):
|
||||
return "".join(random.choice("0123456789abcdef") for _ in range(n))
|
||||
|
||||
|
||||
def main():
|
||||
wire = load_first_wire()
|
||||
idx = wire.find(b"\r\n\r\n")
|
||||
body = wire[idx + 4:]
|
||||
mp = body.find(MAGIC)
|
||||
assert mp >= 0, "magic not in body"
|
||||
cipher = body[mp + 10:]
|
||||
print(f"[*] wire={len(wire)}B body={len(body)}B cipher={len(cipher)}B")
|
||||
|
||||
# 明文 JSON (canonical 554B from pair_sync)
|
||||
ps = json.load(open(EVID / "dfp_pair_sync.json"))
|
||||
jsonb = None
|
||||
for p in ps:
|
||||
if p.get("type") != "plain":
|
||||
continue
|
||||
for item in p["plains"]:
|
||||
b = bytes.fromhex(item["hex"])
|
||||
if b[:8] == b'{"appId"' and len(b) >= 554:
|
||||
jsonb = b[:554]
|
||||
break
|
||||
if jsonb:
|
||||
break
|
||||
assert jsonb, "no json"
|
||||
orig = jsonb.decode("utf-8", "replace")
|
||||
|
||||
# --- 实验1: 原样重放 ---
|
||||
print("\n[实验1] 原样重放")
|
||||
r = send_http(HOST, "/", body)
|
||||
status = r.split(b"\r\n", 1)[0].decode("utf-8", "replace")
|
||||
print(f" resp: {status} len={len(r)}")
|
||||
hdrs, _, rbody = r.partition(b"\r\n\r\n")
|
||||
print(f" resp body head: {rbody[:80].hex(' ')}")
|
||||
|
||||
# --- 实验2: 改明文 hdid/deviceId/appkey (等长) → Δ XOR ---
|
||||
new_hdid = gen_random_hex(40)
|
||||
new_devid = gen_random_hex(40)
|
||||
new_appkey = gen_random_hex(32)
|
||||
new = orig
|
||||
new = new.replace(f'"hdid":"{orig.split(chr(34)+"hdid"+chr(34))[1].split(chr(34))[1]}"',
|
||||
f'"hdid":"{new_hdid}"')
|
||||
# 更稳妥: 正则替换
|
||||
import re
|
||||
new = re.sub(r'"hdid":"[0-9a-f]{40}"', f'"hdid":"{new_hdid}"', new)
|
||||
new = re.sub(r'"deviceId":"[0-9a-f]{40}"', f'"deviceId":"{new_devid}"', new)
|
||||
new = re.sub(r'"appkey":"[0-9a-f]{32}"', f'"appkey":"{new_appkey}"', new)
|
||||
assert new != orig, "no field replaced"
|
||||
nb = new.encode("utf-8")
|
||||
assert len(nb) == 554, f"len {len(nb)}"
|
||||
|
||||
delta = bytes(a ^ b for a, b in zip(nb, jsonb))
|
||||
new_cipher = bytes(a ^ b for a, b in zip(cipher, delta + b"\x00" * (len(cipher) - len(delta))))
|
||||
new_body = body[:mp + 10] + new_cipher
|
||||
# Content-Length 不变 (等长)
|
||||
print(f"\n[实验2] 改 hdid={new_hdid[:12]}... deviceId={new_devid[:12]}... appkey={new_appkey[:8]}...")
|
||||
r = send_http(HOST, "/", new_body)
|
||||
status = r.split(b"\r\n", 1)[0].decode("utf-8", "replace")
|
||||
print(f" resp: {status} len={len(r)}")
|
||||
hdrs, _, rbody = r.partition(b"\r\n\r\n")
|
||||
print(f" resp body head: {rbody[:160].hex(' ')}")
|
||||
|
||||
out = {
|
||||
"new_hdid": new_hdid, "new_devid": new_devid, "new_appkey": new_appkey,
|
||||
"new_json": new,
|
||||
"exp1_status": status,
|
||||
"exp2_status": status,
|
||||
"exp2_resp": rbody[:160].hex(),
|
||||
}
|
||||
(EVID / "forge_replay_result.json").write_text(json.dumps(out, ensure_ascii=False, indent=1))
|
||||
print(f"\n[*] saved -> evidence/forge_replay_result.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -19,7 +19,8 @@ import frida
|
||||
|
||||
PACKAGE = "com.duowan.kiwi"
|
||||
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
|
||||
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_pipeline_v2.json")
|
||||
import time as _t
|
||||
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/dfp_pipeline_" + _t.strftime("%H%M%S") + ".json")
|
||||
|
||||
JS = r"""
|
||||
'use strict';
|
||||
@@ -63,7 +64,7 @@ function decodeArg(p, tag){
|
||||
if (startsWith(p, MAGIC_HDR)){
|
||||
res.kind = 'MAGIC';
|
||||
var pre = '';
|
||||
try{ pre = hexb(p.sub(1536), 1536); }catch(e){}
|
||||
try{ pre = hexb(p.sub(8192), 8192); }catch(e){}
|
||||
res.pre = pre;
|
||||
res.full = hexb(p, 9000);
|
||||
return res;
|
||||
@@ -109,6 +110,49 @@ function decodeArg(p, tag){
|
||||
}catch(e){ return null; }
|
||||
}
|
||||
|
||||
// deep decode: scan 1024B at p for plausible pointers, dump 9000B at each
|
||||
function deepDeref(p){
|
||||
var out = {reg: 'deep', addr: String(p)};
|
||||
var found = [];
|
||||
var seen = {};
|
||||
try{
|
||||
// also dump page-aligned base 12000B
|
||||
try{
|
||||
var pg = p.and(ptr('0xfffffffffffff000'));
|
||||
if (isReadable(pg)){
|
||||
found.push({kind: 'page', ptr: String(pg), full: hexb(pg, 12000)});
|
||||
seen[String(pg)] = true;
|
||||
}
|
||||
}catch(e){}
|
||||
var base = p.sub(512);
|
||||
for (var i = 0; i < 1024; i += 8){
|
||||
var v;
|
||||
try{ v = base.add(i).readU64(); }catch(e){ continue; }
|
||||
var cand = v.and(ptr('0x7fffffffffff'));
|
||||
var s = cand.toString();
|
||||
if (cand == ptr('0')) continue;
|
||||
// heap-ish: 0x7[7-9ca].. or 0xb4.. or 0x77..
|
||||
if (!(/^0x(7[78a-c9]|b4)[0-9a-f]{9,}$/.test(s))) continue;
|
||||
if (seen[s]) continue;
|
||||
if (!isReadable(cand)) continue;
|
||||
seen[s] = true;
|
||||
var entry = {off: i - 512, ptr: s};
|
||||
var kind = 'raw';
|
||||
if (startsWith(cand, JSON_HDR)) kind = 'JSON';
|
||||
else if (startsWith(cand, MAGIC_HDR)) kind = 'MAGIC';
|
||||
entry.kind = kind;
|
||||
if (kind === 'JSON' || kind === 'MAGIC'){
|
||||
entry.full = hexb(cand, 9000);
|
||||
} else {
|
||||
entry.head = hexb(cand, 64) || '';
|
||||
}
|
||||
found.push(entry);
|
||||
}
|
||||
}catch(e){}
|
||||
out.entries = found;
|
||||
return out;
|
||||
}
|
||||
|
||||
function hookFn(off, name){
|
||||
try{
|
||||
var base = Process.getModuleByName('libhydeviceid.so').base;
|
||||
@@ -120,6 +164,10 @@ function hookFn(off, name){
|
||||
var pr = decodeArg(ctx[r], r);
|
||||
if (pr) ev[r] = pr;
|
||||
});
|
||||
// cryptoA: deep deref x0
|
||||
if (name === 'cryptoA_5b1f4'){
|
||||
ev.deep = deepDeref(ctx.x0);
|
||||
}
|
||||
send({type:'pipe', ev: ev});
|
||||
}
|
||||
});
|
||||
@@ -146,6 +194,29 @@ function attachOne(base, off, name){
|
||||
var pr = decodeArg(ctx[r], r);
|
||||
if (pr) ev[r] = pr;
|
||||
});
|
||||
// cryptoA: deep deref x0
|
||||
if (name === 'cryptoA_5b1f4'){
|
||||
ev.deep = deepDeref(ctx.x0);
|
||||
}
|
||||
// copy_61098 second call (x0=MAGIC): heap scan for JSON + dump 4600B
|
||||
if (name === 'copy_61098' && (ev.x0||{}).kind === 'MAGIC'){
|
||||
var hits = [];
|
||||
try{
|
||||
var pat = '7b 22 61 70 70 49 64 22 3a 22 35 30 30 38';
|
||||
var ranges = Process.enumerateRanges('rw-');
|
||||
for (var ri = 0; ri < ranges.length; ri++){
|
||||
var rng = ranges[ri];
|
||||
if (rng.size > 0x10000000) continue;
|
||||
var m = Memory.scanSync(rng.base, rng.size, pat);
|
||||
for (var mi = 0; mi < m.length; mi++){
|
||||
var full = hexb(m[mi].address, 4600) || '';
|
||||
var h = full.substring(0, 96);
|
||||
hits.push({addr: String(m[mi].address), head: h, full: full});
|
||||
}
|
||||
}
|
||||
}catch(e){}
|
||||
send({type:'jsonhits', n: hits.length, hits: hits, t: Date.now() - t0});
|
||||
}
|
||||
send({type:'pipe', ev: ev});
|
||||
}
|
||||
});
|
||||
@@ -183,6 +254,20 @@ try{
|
||||
"""
|
||||
|
||||
|
||||
def load_with_retry(session, js, n=5):
|
||||
last = None
|
||||
for i in range(n):
|
||||
try:
|
||||
s = session.create_script(js)
|
||||
s.load()
|
||||
return s
|
||||
except Exception as e:
|
||||
last = e
|
||||
print(f"[retry {i}] {e}", flush=True)
|
||||
time.sleep(3)
|
||||
raise last
|
||||
|
||||
|
||||
def main():
|
||||
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
|
||||
pid = device.spawn([PACKAGE])
|
||||
@@ -226,26 +311,25 @@ def main():
|
||||
print(f"[{ev.get('t')}ms] {ev.get('fn')}::{ev.get('phase')} " + " ".join(parts), flush=True)
|
||||
events.append({"type": "pipe", "ev": ev})
|
||||
OUT.write_text(json.dumps(events))
|
||||
elif t == "jsonhits":
|
||||
print(f"[{p.get('t')}ms] [jsonhits] n={p.get('n')}", flush=True)
|
||||
events.append({"type": "jsonhits", "t": p.get('t'), "hits": p.get('hits')})
|
||||
OUT.write_text(json.dumps(events))
|
||||
elif t == "wire":
|
||||
print(f"[{p.get('t')}ms] [wire] len={p.get('len')}", flush=True)
|
||||
events.append({"type": "wire", "len": p.get('len'), "t": p.get('t'), "hex": p.get('hex')})
|
||||
OUT.write_text(json.dumps(events))
|
||||
|
||||
script = session.create_script(JS)
|
||||
script.on("message", on_message)
|
||||
script.load()
|
||||
print("[*] 主JS 已装载, 加载 bypass + resume", flush=True)
|
||||
session.create_script(
|
||||
(RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()
|
||||
).load()
|
||||
session.create_script(
|
||||
(RE / "evidence/scripts/mask_frida_maps_only.js").read_text()
|
||||
).load()
|
||||
# 严格按约定顺序: spawn -> bypass x2 -> resume + sleep 11 -> patch_guard -> 最后主 hook JS
|
||||
print("[*] loading bypass", flush=True)
|
||||
load_with_retry(session, (RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text())
|
||||
load_with_retry(session, (RE / "evidence/scripts/mask_frida_maps_only.js").read_text())
|
||||
device.resume(pid)
|
||||
time.sleep(11)
|
||||
session.create_script(
|
||||
(RE / "evidence/scripts/patch_guard_block_termination.js").read_text()
|
||||
).load()
|
||||
load_with_retry(session, (RE / "evidence/scripts/patch_guard_block_termination.js").read_text())
|
||||
print("[*] loading 主 hook JS (最后)", flush=True)
|
||||
script = load_with_retry(session, JS)
|
||||
script.on("message", on_message)
|
||||
print("[*] 已 resume。等待 dfpReport", flush=True)
|
||||
try:
|
||||
while True:
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
"""间接调用解析器 v3 (按需启用, 零常驻开销):
|
||||
默认不挂任何桩 hook; copy_61098 轮次边界(x1=MAGIC)触发时动态 attach 72 个 br 桩,
|
||||
3 秒后全部 detach。捕获 crypto 调用图谱定位 keystream 内核。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import frida
|
||||
|
||||
PACKAGE = "com.duowan.kiwi"
|
||||
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
|
||||
OUT = Path("/Users/yml/codes/douyu_login_py/evidence/got_resolve.json")
|
||||
|
||||
JS = r"""
|
||||
'use strict';
|
||||
Process.setExceptionHandler(function(d){ return true; });
|
||||
send({type:'armed'});
|
||||
var t0 = Date.now();
|
||||
var MAGIC_HDR = [0x57,0x18,0x82,0xcf,0x66,0x4b,0xb3,0x94,0x01,0xee];
|
||||
var modBase = null;
|
||||
var counts = new Map();
|
||||
var roundN = 0;
|
||||
var stubHandles = []; // Interceptor handles to detach
|
||||
var stubsActive = false;
|
||||
var activeSince = 0;
|
||||
|
||||
function hexb(p, n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(function(b){return ('0'+b.toString(16)).slice(-2)}).join(''); }catch(e){ return ''; } }
|
||||
function startsWith(p, arr){
|
||||
try{ var b = new Uint8Array(p.readByteArray(arr.length)); for (var i=0;i<arr.length;i++) if (b[i]!==arr[i]) return false; return true; }catch(e){ return false; }
|
||||
}
|
||||
|
||||
function hookBrSite(addr){
|
||||
try{
|
||||
var h = Interceptor.attach(addr, {
|
||||
onEnter: function(a){
|
||||
var ctx = this.context;
|
||||
var tgt = ctx.x17;
|
||||
if (tgt.isNull()) return;
|
||||
var off = tgt.sub(modBase).toInt32();
|
||||
counts.set(off, (counts.get(off) || 0) + 1);
|
||||
}
|
||||
});
|
||||
return h;
|
||||
}catch(e){ return null; }
|
||||
}
|
||||
|
||||
function enableStubs(){
|
||||
if (stubsActive) return;
|
||||
stubsActive = true;
|
||||
activeSince = Date.now();
|
||||
stubHandles = [];
|
||||
for (var i = 0; i < 24; i++){
|
||||
var h = hookBrSite(modBase.add(0x461c0 + i*0x10 + 0xc));
|
||||
if (h) stubHandles.push(h);
|
||||
}
|
||||
for (var i = 0; i < 48; i++){
|
||||
var h = hookBrSite(modBase.add(0x45aa0 + i*0x10 + 0xc));
|
||||
if (h) stubHandles.push(h);
|
||||
}
|
||||
send({type:'stubs_on', n: stubHandles.length, t: Date.now()-t0});
|
||||
// 3s 后自动摘除
|
||||
setTimeout(function(){
|
||||
if (!stubsActive) return;
|
||||
for (var j = 0; j < stubHandles.length; j++){
|
||||
try{ stubHandles[j].detach(); }catch(e){}
|
||||
}
|
||||
stubHandles = [];
|
||||
stubsActive = false;
|
||||
send({type:'stubs_off', t: Date.now()-t0});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function tryHookRound(){
|
||||
try{
|
||||
var m = Process.getModuleByName('libhydeviceid.so');
|
||||
modBase = m.base;
|
||||
Interceptor.attach(m.base.add(0x61098), {
|
||||
onEnter: function(a){
|
||||
var ctx = this.context;
|
||||
if (!startsWith(ctx.x1, MAGIC_HDR)) return;
|
||||
roundN++;
|
||||
// dump round data BEFORE enabling stubs (crypto already done)
|
||||
var arr = [];
|
||||
counts.forEach(function(v, k){ if (v > 2) arr.push([k, v]); });
|
||||
send({type:'round', n: roundN, t: Date.now()-t0, counts: arr, activeMs: Date.now()-activeSince});
|
||||
counts = new Map();
|
||||
send({type:'magic', t: Date.now()-t0, full: hexb(ctx.x1, 4300)});
|
||||
// 延迟到回调外再挂桩(frida 回调内 attach 会失败)
|
||||
setTimeout(function(){ counts = new Map(); enableStubs(); }, 15);
|
||||
}
|
||||
});
|
||||
send({type:'round_hooked'});
|
||||
}catch(e){ setTimeout(tryHookRound, 500); }
|
||||
}
|
||||
setTimeout(tryHookRound, 400);
|
||||
|
||||
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 < 100 || len > 20000) return;
|
||||
var head = '';
|
||||
try{ head = a[1].readCString(Math.min(len, 2000)); }catch(e){ return; }
|
||||
if (head.indexOf('dfpReport') < 0) return;
|
||||
send({type:'wire', len:len, t: Date.now()-t0});
|
||||
}
|
||||
});
|
||||
});
|
||||
send({type:'ssl_hooked'});
|
||||
}catch(e){ send({type:'ssl_err', e:String(e)}); }
|
||||
"""
|
||||
|
||||
|
||||
def load_with_retry(session, js, n=4):
|
||||
last = None
|
||||
for i in range(n):
|
||||
try:
|
||||
s = session.create_script(js)
|
||||
s.load()
|
||||
return s
|
||||
except Exception as e:
|
||||
last = e
|
||||
print(f"[retry {i}] {e}", flush=True)
|
||||
time.sleep(3)
|
||||
raise last
|
||||
|
||||
|
||||
def main():
|
||||
device = frida.get_device_manager().add_remote_device("127.0.0.1:31877")
|
||||
pid = device.spawn([PACKAGE])
|
||||
print(f"[*] spawned {PACKAGE} pid={pid}", flush=True)
|
||||
session = device.attach(pid)
|
||||
|
||||
events = []
|
||||
|
||||
def on_message(message, data):
|
||||
if message.get("type") == "error":
|
||||
print("[JS-ERR]", str(message)[:200], flush=True)
|
||||
return
|
||||
if message.get("type") != "send":
|
||||
return
|
||||
p = message.get("payload") or {}
|
||||
t = p.get("type")
|
||||
if t == "armed":
|
||||
print("[*] JS armed", flush=True)
|
||||
elif t == "round_hooked":
|
||||
print("[*] round boundary hooked", flush=True)
|
||||
elif t == "stubs_on":
|
||||
print(f"[t={p.get('t')}ms] stubs ON x{p.get('n')}", flush=True)
|
||||
elif t == "stubs_off":
|
||||
print(f"[t={p.get('t')}ms] stubs OFF", flush=True)
|
||||
elif t == "round":
|
||||
c = dict(p.get('counts') or [])
|
||||
top = sorted(c.items(), key=lambda x: -x[1])[:15]
|
||||
print(f"[t={p.get('t')}ms round{p.get('n')}] (stubs active {p.get('activeMs')}ms) calls={sum(c.values())} distinct={len(c)}", flush=True)
|
||||
for off, cnt in top:
|
||||
print(f" off=0x{off:x} x{cnt}", flush=True)
|
||||
events.append({"type": "round", **p})
|
||||
OUT.write_text(json.dumps(events))
|
||||
elif t == "magic":
|
||||
events.append({"type": "magic", "t": p.get('t'), "full": p.get('full')})
|
||||
OUT.write_text(json.dumps(events))
|
||||
elif t == "wire":
|
||||
print(f"[t={p.get('t')}ms] [wire] len={p.get('len')}", flush=True)
|
||||
events.append({"type": "wire", **p})
|
||||
OUT.write_text(json.dumps(events))
|
||||
|
||||
# 严格按约定顺序: spawn -> bypass x2 -> resume + sleep 11 -> patch_guard -> 最后主 hook JS
|
||||
print("[*] loading bypass", flush=True)
|
||||
load_with_retry(session, (RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text())
|
||||
load_with_retry(session, (RE / "evidence/scripts/mask_frida_maps_only.js").read_text())
|
||||
device.resume(pid)
|
||||
time.sleep(11)
|
||||
load_with_retry(session, (RE / "evidence/scripts/patch_guard_block_termination.js").read_text())
|
||||
print("[*] loading 主 hook JS (最后)", flush=True)
|
||||
script = load_with_retry(session, JS)
|
||||
script.on("message", on_message)
|
||||
print("[*] all loaded. waiting dfpReport", flush=True)
|
||||
try:
|
||||
while True:
|
||||
time.sleep(5)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
print(f"[*] done -> {OUT}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user