feat(dfp): 🏆 dfpReport 加密完全攻破 — RC4+gzip, 12/12 密文解密+逐字节回验 (R35-R37)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""dfpReport 编解码器 — R37 攻坚定论 (2026-08-29)
|
||||
|
||||
算法 (R35-R37 三轮活体捕获 + 12/12 历史密文解密验证):
|
||||
明文 JSON -> gzip (zlib deflate, gzip wrapper, 头 1f8b08 00 00000000 02 03)
|
||||
-> C = RC4(key) keystream XOR gzip (逐字节, 长度保持, 标准 KSA/PRGA 无 drop)
|
||||
-> wire body = 密文整段 (前 10B 即所谓 magic 571882cf664bb39401ee = keystream XOR gzip 头,
|
||||
不是魔数! 密钥同则 magic 同)
|
||||
-> 尾部 10B 为 gzip 流之后的附加段 (decompressobj unused_data)
|
||||
|
||||
密钥 = 32 字符 hex ASCII 串 (如 865a4924a40897ac1fcfe6b4c2abc798), 每注册轮换.
|
||||
捕获点: libhydeviceid.so!0x60e6c onEnter x1 = 密钥串 (tools/frida/hook_hy.js).
|
||||
XXTEA/静态密钥 f15e... 与 dfpReport 无关 (那是 hdid 子系统) — 45 函数零调用之谜即此.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys, zlib
|
||||
|
||||
|
||||
def rc4_keystream(key: bytes, n: int) -> bytes:
|
||||
S = list(range(256))
|
||||
j = 0
|
||||
for i in range(256):
|
||||
j = (j + S[i] + key[i % len(key)]) & 0xFF
|
||||
S[i], S[j] = S[j], S[i]
|
||||
out = bytearray()
|
||||
i = j = 0
|
||||
for _ in range(n):
|
||||
i = (i + 1) & 0xFF
|
||||
j = (j + S[i]) & 0xFF
|
||||
S[i], S[j] = S[j], S[i]
|
||||
out.append(S[(S[i] + S[j]) & 0xFF])
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def decrypt(body: bytes, key: bytes) -> bytes:
|
||||
"""body = wire body (magic 10B + cipher) -> 指纹 JSON 明文."""
|
||||
ks = rc4_keystream(key, len(body))
|
||||
plain = bytes(a ^ b for a, b in zip(body, ks))
|
||||
d = zlib.decompressobj(31) # gzip
|
||||
json_bytes = d.decompress(plain, 1 << 20)
|
||||
return json_bytes
|
||||
|
||||
|
||||
def encrypt(json_bytes: bytes, key: bytes) -> bytes:
|
||||
"""指纹 JSON -> wire body. 尾部 10B 附加段按全 0 处理 (服务端解 gzip 忽略)."""
|
||||
gz = gzip_wrap(json_bytes)
|
||||
ks = rc4_keystream(key, len(gz) + 10)
|
||||
return bytes(a ^ b for a, b in zip(gz + b"\x00" * 10, ks))
|
||||
|
||||
|
||||
def gzip_wrap(json_bytes: bytes) -> bytes:
|
||||
co = zlib.compressobj(9, zlib.DEFLATED, 31) # gzip wrapper
|
||||
gz = co.compress(json_bytes) + co.flush()
|
||||
# 对齐 turing 原生 gzip 头: OS 字节 = 0x03 (Unix), Python 写 0x13
|
||||
return gz[:9] + b"\x03" + gz[10:]
|
||||
|
||||
|
||||
def _selftest():
|
||||
MAGIC = bytes.fromhex("571882cf664bb39401ee")
|
||||
KEY = b"865a4924a40897ac1fcfe6b4c2abc798"
|
||||
body = open("evidence/dfp_live/dfp_body_4303.bin", "rb").read()
|
||||
i = body.find(MAGIC)
|
||||
wire = body[i:]
|
||||
js = decrypt(wire, KEY)
|
||||
assert js.startswith(b'{"Athena"'), js[:40]
|
||||
print(f"decrypt OK: {len(js)}B JSON")
|
||||
gz = gzip_wrap(js)
|
||||
re_enc = encrypt(js, KEY)
|
||||
n = len(gz)
|
||||
print(f"re-encrypt: gzip {n}B + 10B tail vs wire {len(wire)}B, "
|
||||
f"cipher part {'IDENTICAL' if re_enc[:n] == wire[:n] else 'DIFFERS'} "
|
||||
f"(magic10 {'==' if re_enc[:10] == wire[:10] else '!='})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) >= 3 and sys.argv[1] == "dec":
|
||||
key = sys.argv[3].encode()
|
||||
body = open(sys.argv[2], "rb").read()
|
||||
MAGIC = bytes.fromhex("571882cf664bb39401ee")
|
||||
i = body.find(MAGIC)
|
||||
sys.stdout.buffer.write(decrypt(body[i:], key))
|
||||
else:
|
||||
_selftest()
|
||||
@@ -0,0 +1,108 @@
|
||||
// R36b: 只挂 libz.so 的 deflate/inflate — 抓 XXTEA 输入明文 + 隐藏路径 backtrace
|
||||
// 教训: GL/EGL 驱动也有同名 deflate 导出但非 zlib ABI, hook 必崩 — 只认 libz.so
|
||||
'use strict';
|
||||
function emit(o){ try{ send(o); }catch(e){} }
|
||||
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 bt(ctx, depth){
|
||||
var out = [];
|
||||
try {
|
||||
var fr = Thread.backtrace(ctx, Backtracer.ACCURATE).slice(0, depth).map(function(a){
|
||||
var m = Process.findModuleByAddress(a);
|
||||
return m ? m.name + '!0x' + a.sub(m.base).toString(16) : a.toString();
|
||||
});
|
||||
if (fr.length) return fr;
|
||||
} catch(e){}
|
||||
try {
|
||||
out = Thread.backtrace(ctx, Backtracer.FUZZY).slice(0, depth).map(function(a){
|
||||
var m = Process.findModuleByAddress(a);
|
||||
return 'F:' + (m ? m.name + '!0x' + a.sub(m.base).toString(16) : a.toString());
|
||||
});
|
||||
} catch(e){}
|
||||
return out;
|
||||
}
|
||||
// z_stream (LP64): next_in@0 avail_in@8 total_in@16 next_out@24 avail_out@32 total_out@40
|
||||
var streams = {};
|
||||
var nDumps = 0;
|
||||
var armed = false;
|
||||
|
||||
function armDeflate(){
|
||||
if (armed) return;
|
||||
var libz = Process.findModuleByName('libz.so');
|
||||
if (!libz) return;
|
||||
var d = null, i = null;
|
||||
try { d = libz.getExportByName('deflate'); } catch(e){}
|
||||
try { i = libz.getExportByName('inflate'); } catch(e){}
|
||||
if (d) {
|
||||
armed = true;
|
||||
emit({event:'armed-deflate', mod:'libz.so'});
|
||||
Interceptor.attach(d, {
|
||||
onEnter(args){
|
||||
var strm = args[0]; var flush = args[1].toInt32();
|
||||
try {
|
||||
var key = strm.toString();
|
||||
var st = streams[key];
|
||||
if (!st){
|
||||
st = streams[key] = {out_base: strm.add(24).readPointer(), bt0: null, n: 0};
|
||||
var nin = strm.add(8).readU32();
|
||||
if (nin > 0 && nin < 200000){
|
||||
st.in_hex = hexb(strm.readPointer(), Math.min(nin, 65536));
|
||||
st.in_len = nin;
|
||||
}
|
||||
}
|
||||
if (!st.bt0 && st.n < 3) st.bt0 = bt(this.context, 24);
|
||||
st.n++;
|
||||
this.strm = strm; this.flush = flush;
|
||||
} catch(e){}
|
||||
},
|
||||
onLeave(ret){
|
||||
try {
|
||||
var st = streams[this.strm.toString()];
|
||||
if (!st) return;
|
||||
var zret = ret.toInt32();
|
||||
var totalOut = this.strm.add(40).readU64().toNumber();
|
||||
if (this.flush === 4 && (zret === 1 || zret === 0)){
|
||||
// 只转储 turing 调用 (bt 含 turing) 或大输入 (Lyra 帧 JSON) — 其余是 UI 资源压缩噪音
|
||||
var btStr = (st.bt0 || []).join(',');
|
||||
var isTuring = /turing/i.test(btStr);
|
||||
if (!isTuring && (st.in_len || 0) < 300){ delete streams[this.strm.toString()]; return; }
|
||||
var hex = hexb(st.out_base, Math.min(totalOut, 131072));
|
||||
nDumps++;
|
||||
emit({event:'deflate-out', i:nDumps, total: totalOut, ret: zret, turing: isTuring,
|
||||
in_len: st.in_len || 0, in_hex: st.in_hex || '', out_hex: hex, bt: st.bt0});
|
||||
delete streams[this.strm.toString()];
|
||||
}
|
||||
} catch(e){}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (i) {
|
||||
// inflate 不挂 — 图片解码热路径, 挂了首屏卡死 (R36 教训)
|
||||
}
|
||||
}
|
||||
|
||||
var poll = setInterval(function(){ armDeflate(); if (armed) clearInterval(poll); }, 150);
|
||||
|
||||
// SSL_write: 抓同轮 wire 密文
|
||||
var done = false;
|
||||
for (var mod of Process.enumerateModules()){
|
||||
var addr = null;
|
||||
try { addr = mod.getExportByName('SSL_write'); } catch(e){}
|
||||
if (!addr) continue;
|
||||
Interceptor.attach(addr, {
|
||||
onEnter(args){
|
||||
if (done) return;
|
||||
try {
|
||||
var num = args[2].toInt32();
|
||||
if (num < 60 || num > 60000) return;
|
||||
var u = new Uint8Array(args[1].readByteArray(num));
|
||||
var s = '';
|
||||
for (var i=0;i<u.length;i++) s += (u[i]>=32 && u[i]<127) ? String.fromCharCode(u[i]) : '.';
|
||||
if (s.indexOf('dfpReport') < 0) return;
|
||||
done = true;
|
||||
var hex = Array.prototype.map.call(u, function(x){return ('0'+x.toString(16)).slice(-2)}).join('');
|
||||
emit({event:'post', num:num, hex:hex});
|
||||
} catch(e){}
|
||||
}
|
||||
});
|
||||
}
|
||||
emit({event:'script-loaded'});
|
||||
@@ -0,0 +1,80 @@
|
||||
// R37: 挂 libhydeviceid.so 加密调用链 — 抓加密函数的密钥/nonce 参数
|
||||
// 已知链 (R36 deflate bt): 0x57e7c(gzip包装) <- 0x58d38 <- 0x60e6c <- 0x5b1f4 <- 0x5a938 <- 0x20e924 <- JNI
|
||||
'use strict';
|
||||
function emit(o){ try{ send(o); }catch(e){} }
|
||||
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 ''; } }
|
||||
var HY = null;
|
||||
var events = 0;
|
||||
var MAX = 400;
|
||||
|
||||
function dumpArgs(args, tag, ctx){
|
||||
if (events >= MAX) return;
|
||||
var row = {event:'call', fn:tag, args:[]};
|
||||
for (var i=0;i<4;i++){
|
||||
try {
|
||||
var p = args[i];
|
||||
var h = hexb(p, 48);
|
||||
// 判定是否像指针指向数据
|
||||
row.args.push({x:String(p), h: h});
|
||||
} catch(e){ row.args.push({x:String(args[i]), h:''}); }
|
||||
}
|
||||
try {
|
||||
row.bt = Thread.backtrace(ctx, Backtracer.ACCURATE).slice(0,12).map(function(a){
|
||||
var m=Process.findModuleByAddress(a); return m? m.name+'!0x'+a.sub(m.base).toString(16) : a.toString();
|
||||
});
|
||||
} catch(e){}
|
||||
events++;
|
||||
emit(row);
|
||||
}
|
||||
|
||||
function arm(){
|
||||
if (HY) return true;
|
||||
HY = Process.findModuleByName('libhydeviceid.so');
|
||||
if (!HY) return false;
|
||||
var offs = [0x57e7c, 0x58d38, 0x60e6c, 0x5b1f4, 0x5a938, 0x20e924];
|
||||
for (var off of offs){
|
||||
(function(off){
|
||||
try {
|
||||
Interceptor.attach(HY.base.add(off), {
|
||||
onEnter(args){ this.tag='0x'+off.toString(16); this.on=true;
|
||||
if (events < MAX) dumpArgs(args, this.tag+':e', this.context); },
|
||||
onLeave(ret){
|
||||
if (this.on && events < MAX){
|
||||
events++;
|
||||
emit({event:'ret', fn:this.tag, ret:String(ret), x0:String(this.context.x0), x1:String(this.context.x1)});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch(e){ emit({event:'hook-e', off:off, e:String(e).substring(0,120)}); }
|
||||
})(off);
|
||||
}
|
||||
emit({event:'armed-hy'});
|
||||
return true;
|
||||
}
|
||||
|
||||
var poll = setInterval(function(){ if (arm()) clearInterval(poll); }, 150);
|
||||
|
||||
// SSL_write: 同轮 wire 密文
|
||||
var done = false;
|
||||
for (var mod of Process.enumerateModules()){
|
||||
var addr = null;
|
||||
try { addr = mod.getExportByName('SSL_write'); } catch(e){}
|
||||
if (!addr) continue;
|
||||
Interceptor.attach(addr, {
|
||||
onEnter(args){
|
||||
if (done) return;
|
||||
try {
|
||||
var num = args[2].toInt32();
|
||||
if (num < 60 || num > 60000) return;
|
||||
var u = new Uint8Array(args[1].readByteArray(num));
|
||||
var s = '';
|
||||
for (var i=0;i<u.length;i++) s += (u[i]>=32 && u[i]<127) ? String.fromCharCode(u[i]) : '.';
|
||||
if (s.indexOf('dfpReport') < 0) return;
|
||||
done = true;
|
||||
var hex = Array.prototype.map.call(u, function(x){return ('0'+x.toString(16)).slice(-2)}).join('');
|
||||
emit({event:'post', num:num, hex:hex});
|
||||
} catch(e){}
|
||||
}
|
||||
});
|
||||
}
|
||||
emit({event:'script-loaded'});
|
||||
@@ -0,0 +1,142 @@
|
||||
// R35: 内存快照 @SSL_write — 定位密文缓冲区全部拷贝 + 邻域转储 + 会话密钥
|
||||
// 目标: 在密文刚生成/刚发出的瞬间, 从堆内存里找到 (a) 明文 deflate 缓冲 (b) 16B 会话密钥
|
||||
'use strict';
|
||||
function emit(o){ try{ send(o); }catch(e){} }
|
||||
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 ''; } }
|
||||
var STATIC_KEY = 'f15e78d23033c2c4bac8a925677e863a';
|
||||
var done = false;
|
||||
|
||||
function scanRanges(ranges, patternHex, maxHits){
|
||||
var pat = '';
|
||||
for (var i=0;i<patternHex.length;i+=2) pat += patternHex.substr(i,2)+' ';
|
||||
var out = [];
|
||||
for (var r=0; r<ranges.length && out.length<maxHits; r++){
|
||||
var rg = ranges[r];
|
||||
if (rg.size > 96*1024*1024) continue;
|
||||
try {
|
||||
var ms = Memory.scanSync(rg.base, rg.size, pat);
|
||||
for (var m=0; m<ms.length && out.length<maxHits; m++) out.push({a: ms[m].address, mod: rg.file ? rg.file.path : String(rg.base), prot: rg.protection});
|
||||
} catch(e){}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function enumAllRW(){
|
||||
return Process.enumerateRanges({protection:'rw-', coalesce:false});
|
||||
}
|
||||
|
||||
for (const mod of Process.enumerateModules()) {
|
||||
let addr = null;
|
||||
try { addr = mod.getExportByName('SSL_write'); } catch(e){}
|
||||
if (!addr) continue;
|
||||
Interceptor.attach(addr, {
|
||||
onEnter(args){
|
||||
if (done) return;
|
||||
try {
|
||||
var num = args[2].toInt32();
|
||||
if (num < 60 || num > 60000) return;
|
||||
var u = new Uint8Array(args[1].readByteArray(num));
|
||||
var s = '';
|
||||
for (var i=0;i<u.length;i++) s += (u[i]>=32 && u[i]<127) ? String.fromCharCode(u[i]) : '.';
|
||||
if (s.indexOf('dfpReport') < 0) return;
|
||||
done = true;
|
||||
var hex = Array.prototype.map.call(u, function(x){return ('0'+x.toString(16)).slice(-2)}).join('');
|
||||
var idx = hex.indexOf('571882cf664bb39401ee');
|
||||
emit({event:'post', num:num, hex:hex, idx:idx});
|
||||
if (idx < 0) return;
|
||||
|
||||
// 线上密文前缀 (magic 后 10B 起, 取 16B 作内存搜索指纹)
|
||||
var CIPH = hex.substring(idx+20, idx+20+32);
|
||||
emit({event:'cipher-prefix', ciph:CIPH});
|
||||
var bt = Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0,30).map(function(a){ var m=Process.findModuleByAddress(a); return m? m.name+'!0x'+a.sub(m.base).toString(16) : a.toString(); }).join(' <- ');
|
||||
emit({event:'bt', bt:bt});
|
||||
|
||||
setTimeout(function(){
|
||||
// ---- 1) Java 堆: Perseus/Guava 会话密钥 ----
|
||||
try {
|
||||
Java.perform(function(){
|
||||
try {
|
||||
Java.enumerateClassLoaders({
|
||||
onMatch: function(loader){
|
||||
try {
|
||||
Java.classFactory.loader = loader;
|
||||
Java.choose('com.tencent.turingfd.sdk.ams.ga.Perseus', {
|
||||
onMatch: function(inst){
|
||||
try {
|
||||
var g = inst.b.value;
|
||||
emit({event:'perseus', key: g ? String(g.c.value) : null,
|
||||
envLen: g && g.b && g.b.value ? g.b.value.length : -1,
|
||||
d: g && g.d ? String(g.d.value) : null});
|
||||
} catch(e){ emit({event:'perseus-e', e:String(e).substring(0,150)}); }
|
||||
},
|
||||
onComplete: function(){}
|
||||
});
|
||||
} catch(e){}
|
||||
},
|
||||
onComplete: function(){}
|
||||
});
|
||||
} catch(e){ emit({event:'j-e', e:String(e).substring(0,150)}); }
|
||||
});
|
||||
} catch(e){}
|
||||
|
||||
// ---- 2) native 堆扫描 ----
|
||||
var ranges = enumAllRW();
|
||||
emit({event:'ranges', n:ranges.length, bytes: ranges.reduce(function(a,r){return a+r.size;},0)});
|
||||
|
||||
// 2a. 密文拷贝定位
|
||||
var hits = scanRanges(ranges, CIPH, 24);
|
||||
emit({event:'cipher-copies', n:hits.length, addrs: hits.map(function(h){return String(h.a);})});
|
||||
// 每处转储 ±2KB 邻域 (前 8 处全量, 其余只记地址)
|
||||
for (var i=0;i<Math.min(hits.length,8);i++){
|
||||
var a = hits[i].a;
|
||||
var lo = a.sub(2048), hi = a.add(2048);
|
||||
emit({event:'nbhd', i:i, addr:String(a), src:hits[i].mod, lo:String(lo), hex: hexb(lo, 4096)});
|
||||
}
|
||||
|
||||
// 2b. 静态密钥出现位置
|
||||
var sh = scanRanges(ranges, STATIC_KEY, 40);
|
||||
emit({event:'static-key-hits', n:sh.length, addrs: sh.map(function(h){return String(h.a)+'@'+h.src;})});
|
||||
|
||||
// 2c. 密文邻域内 16 字符 [A-Za-z0-9] 候选密钥串 (±8KB of each cipher copy)
|
||||
var cands = {}, cn=0;
|
||||
for (var i=0;i<Math.min(hits.length,6) && cn<4000;i++){
|
||||
try {
|
||||
var buf = new Uint8Array(hits[i].a.sub(8192).readByteArray(16384));
|
||||
var run = '', start=0;
|
||||
for (var j=0;j<buf.length;j++){
|
||||
var c=buf[j];
|
||||
var ok = (c>=48&&c<=57)||(c>=65&&c<=90)||(c>=97&&c<=122);
|
||||
if (ok){ if(!run) start=j; run+=String.fromCharCode(c); }
|
||||
else {
|
||||
if (run.length===16 && !(cands[run])) { cands[run]=1; cn++; }
|
||||
run='';
|
||||
}
|
||||
}
|
||||
if (run.length===16 && !cands[run]) { cands[run]=1; cn++; }
|
||||
} catch(e){}
|
||||
}
|
||||
emit({event:'key-cands', n:cn, keys:Object.keys(cands)});
|
||||
|
||||
// 2d. 全堆 zlib 头候选 (78 01/5e/9c/da) — 限量转储 256B 供主机验证
|
||||
var zs=0;
|
||||
for (var r=0; r<ranges.length && zs<120; r++){
|
||||
var rg=ranges[r];
|
||||
if (rg.size>96*1024*1024) continue;
|
||||
for (var pi=0; pi<4 && zs<120; pi++){
|
||||
var zpat = ['78 01','78 5e','78 9c','78 da'][pi];
|
||||
var ms=[];
|
||||
try { ms = Memory.scanSync(rg.base, rg.size, zpat); } catch(e){ continue; }
|
||||
for (var m=0; m<ms.length && zs<120; m++){
|
||||
// 过滤: deflate 流应较长 (指纹 JSON ~1-3KB), 跳过落在密文拷贝邻域外的? 不跳, 交给主机验证
|
||||
var h = hexb(ms[m].address, 192);
|
||||
if (h) { emit({event:'zcand', pat:zpat, a:String(ms[m].address), hex:h}); zs++; }
|
||||
}
|
||||
}
|
||||
}
|
||||
emit({event:'scan-done', zcands:zs});
|
||||
}, 200);
|
||||
} catch(e){ emit({event:'e', e:String(e).substring(0,200)}); }
|
||||
}
|
||||
});
|
||||
}
|
||||
emit({event:'armed'});
|
||||
Reference in New Issue
Block a user