feat(huya): 证书算法完全闭环 - 差分法证明标准AES-128-ECB并复现铸证
- NativeFunction直调差分: FIPS-197 KAT逐位一致, UdbAESUtil无魔改 - 证书 = b64([0x0C][key_idx][AES128-ECB(key16, zeropad(P1_187B))]) - P1结构: [010400][5008][u16+20B随机][u16+40B指纹hex][u16+114B hyCred] - 密钥: rodata默认三条(AESkeyMgr id=3/4/5) + 服务端下发会话钥(key_idx区分) - tools/cert_forge.py 重铸证书与活体密文 FULL MATCH
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
# 虎牙 udb 登录证书(biz_token)算法 — 差分法完全闭环(2026-08-25)
|
||||||
|
|
||||||
|
> 状态:**加密算法 100% 复原,Python 重铸与活体密文逐位一致**
|
||||||
|
> 方法:静态反汇编(objdump) + Frida NativeFunction 直调差分 + C1 构造器抓钥
|
||||||
|
> 工具:`tools/udb_aes.py`(参考实现) `tools/cert_forge.py`(解析/铸造) —— 自检均通过
|
||||||
|
> 证据:`evidence/aes_diff.json` `evidence/cert_keycap.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、结论一句话
|
||||||
|
|
||||||
|
```
|
||||||
|
证书 = base64( [0x0C][key_idx][ AES-128-ECB(key16, zeropad(P1_187B)) ] )
|
||||||
|
```
|
||||||
|
|
||||||
|
- **UdbAESUtil 是教科书级标准 AES-128-ECB**,无任何魔改:
|
||||||
|
- FIPS-197 KAT 直调逐位一致:`Cipher(00112233..ff, key000102..0f) = 69c4e0d8...`
|
||||||
|
- KeyExpansion 标准展开,轮密钥按「字表转置」存储(176B @ this+0x208)
|
||||||
|
- 对象布局:+0x00 vptr | +0x08 S-box拷贝 | +0x108 InvS-box拷贝 | +0x208 轮密钥
|
||||||
|
- ZeroPadding:补 `\0` 到 16 倍数,**不追加整块**(187→192)。
|
||||||
|
- 此前"标准解密对不上"的原因:**拿错了密钥**(真钥不是 HuyaUdb 主密钥字符串本身)。
|
||||||
|
|
||||||
|
## 二、P1 明文结构(187B,两份活体样本逐字段对齐)
|
||||||
|
|
||||||
|
| 字段 | 长度 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `[01][04][00]` | 3 | 固定头 |
|
||||||
|
| `"5008"` | 4 | appId |
|
||||||
|
| `[u16_le=20]` + rnd | 22 | 每次生成随机(os.urandom 即可) |
|
||||||
|
| `[u16_le=40]` + fp | 42 | 设备指纹 ASCII hex `02df39879743...`,设备绑定跨会话不变 |
|
||||||
|
| `[u16_le=114]` + cred | 116 | **hyCred 二进制**(=base64(udb_cred) 解码),账号绑定 |
|
||||||
|
|
||||||
|
## 三、密钥机制(AESkeyMgr)
|
||||||
|
|
||||||
|
- rodata `0x1bc810` 内置三条 24 字符默认密钥(`AESkeyMgr::C2` 注册 id=3/4/5):
|
||||||
|
```
|
||||||
|
4VYcPdvKKqjBHZtCmbroRXHk ← 本次活体实测在用(encode_aes 收整条,C1 只取前16字节)
|
||||||
|
xXEDWqiKLGwEZ6HubEiswCqK
|
||||||
|
3FMHubdKosFrhmXNLHTNHZwe
|
||||||
|
```
|
||||||
|
- 证书第二字节 = **key_idx**:今日样本 `0x20` ↔ 上表第一条;更早样本 `0x50`
|
||||||
|
用三条默认钥均解不开 ⇒ 存在**服务端下发的会话密钥**(对应 dckey/check 的
|
||||||
|
NEWKEY/key_id 老线索)。服务端按 idx 选钥解密。
|
||||||
|
- ⚠️ `HuyaUdb1928374650qwertyuiop` 是本地登录态文件加密用的主密钥,
|
||||||
|
**不是**证书密钥(此前误判的根源)。
|
||||||
|
|
||||||
|
## 四、调用链(函数地址级)
|
||||||
|
|
||||||
|
```
|
||||||
|
HandlerGetH5InfoEx::onHandler @0x38d548
|
||||||
|
└ BusinessCfg::getOtp @0x2689f4
|
||||||
|
└ gen_biz_token(BIZTOKEN,...) @0x3307e4 # P1 组装(enpack_header/body,cred_packls,xxtea)
|
||||||
|
└ encode_aes(P1, key24str, out) @0x330218 # out.assign(常量); C1(key.data())
|
||||||
|
├ UdbAESUtil::C1(uchar* key16) @0x24eb08 # ★抓钥点: 每次必经
|
||||||
|
└ UdbAESUtil::encrypt @0x250038 # 补零逐块 -> _encrypt@0x24f9f0 -> Cipher@0x24f314(10轮)
|
||||||
|
└ CBase64::Encode
|
||||||
|
```
|
||||||
|
|
||||||
|
全部符号在 `.dynsym` 导出(binary stripped 但 dynsym 完整),可直接 NativeFunction。
|
||||||
|
|
||||||
|
## 五、复现步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) 差分验证(直调 C1/Cipher, FIPS-197 KAT)
|
||||||
|
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python scripts/hook_aes_diff.py
|
||||||
|
# 2) 抓当前会话证书密钥 + 完整 wupData
|
||||||
|
/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/.venv/bin/python scripts/hook_cert_keycap.py
|
||||||
|
# 3) 离线重铸自检
|
||||||
|
.venv/bin/python tools/cert_forge.py # FULL MATCH: True
|
||||||
|
# 4) 参考实现自检
|
||||||
|
.venv/bin/python tools/udb_aes.py
|
||||||
|
```
|
||||||
|
|
||||||
|
frida 注意事项:手机端 frida-server 14.2.18,必须用 RE 项目 venv(frida==14.2.18);
|
||||||
|
时序不可变:force-stop → spawn挂起 → bypass_msaoaid_maps_art_callsite +
|
||||||
|
mask_frida_maps_only 双脚本 → resume → 10s → patch_guard → 业务脚本。
|
||||||
|
直调 libc++ string 时**禁止让 push_back 触发 realloc**(会对 frida 缓冲区
|
||||||
|
operator delete 导致 abort——第一次闪退即此因,非反调试)。
|
||||||
|
|
||||||
|
## 六、剩余开放项
|
||||||
|
|
||||||
|
1. **key_idx↔密钥映射的服务端语义**:0x20/0x50 的生成规则、下发通道
|
||||||
|
(dckey/check? 登录态 JSON?)、轮换周期。工程上可绕过:直接 hook C1 取当期钥。
|
||||||
|
2. **任意账号 hyCred 获取**(硬骨头②):APP WUP 密码登录已通,
|
||||||
|
待从登录态/HA.getCred 提取目标账号 114B cred 即可离线铸其证书。
|
||||||
|
3. 铸造证书的活体验证:新随机数重建证书 → bindQrLoginUser(下一步)。
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"sched_kat": {
|
||||||
|
"rk": "0004080c0105090d02060a0e03070b0fd6d2dad6aaafa6ab74727876fdfaf1feb664be68923d9b30cfbdc5b30bf100feb6d26c04ffc2596974c90cbf4ebfbf414795f9fdf7356c05f73e328dbc03bcfd3ca950adaa9ff3f6a39daf22e8eb57aa5ef7a70a39a655a30f923d1f7d96c16b14e3444ef95f0aa970e2dfc01a8c4d2647a4e0ae431c16bf8765ba7a35b9f4d254f010be9985932c3257ed97d1689c4e13e3f34d1194072b1d4aa7307f178bc5",
|
||||||
|
"sbox_head": "637c777bf26b6fc53001672bfed7ab76"
|
||||||
|
},
|
||||||
|
"cipher_kat": {
|
||||||
|
"before": "00112233445566778899aabbccddeeff",
|
||||||
|
"after": "69c4e0d86a7b0430d8cdb78070b4c55a"
|
||||||
|
},
|
||||||
|
"cipher_huyaudb_first16": {
|
||||||
|
"before": "00000000000000000000000000000000",
|
||||||
|
"after": "be91fad4c0bc6f0902d4f7c3364edc74"
|
||||||
|
},
|
||||||
|
"inv_roundtrip": {
|
||||||
|
"ct": "3c3f19a3f3b5e6287e1d79eec2a5d11e",
|
||||||
|
"pt": "cafebabecafebabecafebabecafebabe"
|
||||||
|
},
|
||||||
|
"encpub_empty": {
|
||||||
|
"err": "TypeError: not a function"
|
||||||
|
},
|
||||||
|
"encpub_15B": {
|
||||||
|
"err": "TypeError: not a function"
|
||||||
|
},
|
||||||
|
"encpub_16B": {
|
||||||
|
"err": "TypeError: not a function"
|
||||||
|
},
|
||||||
|
"encpub_187B": {
|
||||||
|
"err": "Error: abort was called"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,222 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""UdbAESUtil 差分分析 harness(NativeFunction 直调)。
|
||||||
|
|
||||||
|
时序(不可变): force-stop -> spawn挂起 -> bypass -> resume -> 稳定10s -> patch_guard -> 业务脚本
|
||||||
|
静态已确认对象布局: +0x00 vptr | +0x08 S-box拷贝(256B) | +0x108 InvS-box拷贝(256B) | +0x208 轮密钥(176B)
|
||||||
|
|
||||||
|
导出符号(.dynsym):
|
||||||
|
C1(uchar*key) 0x24eb08 构造器
|
||||||
|
KeyExpansion 0x24ebd4
|
||||||
|
Cipher(uchar*) 0x24f314 10轮
|
||||||
|
InvCipher(uchar*) 0x24f778
|
||||||
|
_encrypt(uchar*,string&) 0x24f9f0
|
||||||
|
_decrypt 0x24fe90
|
||||||
|
encrypt(ret&,key?,in?) 0x250038
|
||||||
|
decrypt 0x2501a4
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import frida
|
||||||
|
|
||||||
|
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
|
||||||
|
|
||||||
|
JS = r"""
|
||||||
|
'use strict';
|
||||||
|
// 段错误防护: 拦截 native 崩溃, 上报后恢复而不是带崩进程
|
||||||
|
Process.setExceptionHandler(function(det){
|
||||||
|
send({type:'segv', info:{type:det.type, addr:String(det.address), msg:String(det.memory||'')}});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
var MOD = 'libudbauthunify.so';
|
||||||
|
var base = Process.getModuleByName(MOD).base;
|
||||||
|
send({type:'armed', base:String(base)});
|
||||||
|
|
||||||
|
var A = {
|
||||||
|
C1: new NativeFunction(base.add(0x24eb08), 'void', ['pointer','pointer']),
|
||||||
|
Cipher: new NativeFunction(base.add(0x24f314), 'void', ['pointer','pointer']),
|
||||||
|
InvCipher: new NativeFunction(base.add(0x24f778), 'void', ['pointer','pointer']),
|
||||||
|
enc_pub: new NativeFunction(base.add(0x250038), 'pointer', ['pointer','pointer','pointer']),
|
||||||
|
dec_pub: new NativeFunction(base.add(0x2501a4), 'pointer', ['pointer','pointer','pointer']),
|
||||||
|
};
|
||||||
|
|
||||||
|
function hex(p,n){ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }
|
||||||
|
function unhex(s){ var n=s.length>>1, b=new Uint8Array(n); for(var i=0;i<n;i++) b[i]=parseInt(s.substr(i*2,2),16); return b; }
|
||||||
|
function wrBytes(p, arr){ p.writeByteArray(arr); }
|
||||||
|
|
||||||
|
// libc++ std::string 构造 (老布局: cap@0|flagLSB, size@8, data@16 / SSO: b0=len<<1,data@1)
|
||||||
|
function mkstr(bytes){
|
||||||
|
var m = Memory.alloc(32);
|
||||||
|
var n = bytes.length;
|
||||||
|
if (n <= 22) {
|
||||||
|
Memory.writeByteArray(m, new Array(24).fill(0));
|
||||||
|
m.writeU8(n<<1);
|
||||||
|
if(n) wrBytes(m.add(1), bytes);
|
||||||
|
} else {
|
||||||
|
var buf = Memory.alloc(n+1);
|
||||||
|
wrBytes(buf, bytes); buf.add(n).writeU8(0);
|
||||||
|
m.writeU64( uint64(((n)<<1)|1) );
|
||||||
|
m.add(8).writeU64(uint64(n));
|
||||||
|
m.add(16).writePointer(buf);
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
function strBytes(sp){ // 读回 std::string 内容
|
||||||
|
var b0=sp.readU8();
|
||||||
|
if((b0&1)===0){ var l=b0>>1; return l? new Uint8Array(sp.add(1).readByteArray(l)) : new Uint8Array(0); }
|
||||||
|
var len=parseInt(sp.add(8).readU64().toString());
|
||||||
|
var dp=sp.add(16).readPointer();
|
||||||
|
return len? new Uint8Array(dp.readByteArray(len)) : new Uint8Array(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var OBJLEN = 0x300;
|
||||||
|
function newObj(){ return Memory.alloc(OBJLEN); }
|
||||||
|
|
||||||
|
rpc.exports = {
|
||||||
|
// 轮密钥展开对拍
|
||||||
|
sched: function(keyHex){
|
||||||
|
var o=newObj(), k=Memory.alloc(32);
|
||||||
|
wrBytes(k, unhex(keyHex));
|
||||||
|
A.C1(o, k);
|
||||||
|
return { rk: hex(o.add(0x208), 176), sbox_head: hex(o.add(8),16) };
|
||||||
|
},
|
||||||
|
// 单块 Cipher(读改前后,判断是否就地)
|
||||||
|
cipher: function(keyHex, blkHex){
|
||||||
|
var o=newObj(), k=Memory.alloc(32), b=Memory.alloc(48);
|
||||||
|
wrBytes(k, unhex(keyHex)); wrBytes(b, unhex(blkHex));
|
||||||
|
A.C1(o,k);
|
||||||
|
var before = hex(b,16);
|
||||||
|
A.Cipher(o,b);
|
||||||
|
return { before:before, after:hex(b,16) };
|
||||||
|
},
|
||||||
|
invcipher: function(keyHex, ctHex){
|
||||||
|
var o=newObj(), k=Memory.alloc(32), b=Memory.alloc(48);
|
||||||
|
wrBytes(k, unhex(keyHex)); wrBytes(b, unhex(ctHex));
|
||||||
|
A.C1(o,k);
|
||||||
|
A.InvCipher(o,b);
|
||||||
|
return { after: hex(b,16) };
|
||||||
|
},
|
||||||
|
// 公开 encrypt(ret,this=x1,x2) —— 按静态结论: static(ret=&ret, x1=&plain, x2=key临时)
|
||||||
|
encpub: function(plainHex, keyBytesHex){
|
||||||
|
var ret=Memory.alloc(32), plain=mkstr(unhex(plainHex)), key=mkstr(unhex(keyBytesHex));
|
||||||
|
try {
|
||||||
|
A.enc_pub(ret, plain, key);
|
||||||
|
return { out: hex(strBytes(ret)) };
|
||||||
|
} catch(e){ return { err:String(e) }; }
|
||||||
|
},
|
||||||
|
decpub: function(ctHex, keyBytesHex){
|
||||||
|
var ret=Memory.alloc(32), ct=mkstr(unhex(ctHex)), key=mkstr(unhex(keyBytesHex));
|
||||||
|
try {
|
||||||
|
A.dec_pub(ret, ct, key);
|
||||||
|
return { out: hex(strBytes(ret)) };
|
||||||
|
} catch(e){ return { err:String(e) }; }
|
||||||
|
},
|
||||||
|
ping: function(){ return 'pong'; }
|
||||||
|
};
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
|
||||||
|
pid = d.spawn(['com.duowan.kiwi'])
|
||||||
|
print(f'spawned {pid}')
|
||||||
|
s = d.attach(pid)
|
||||||
|
# 双重防护(与 hook_final_capture 完全一致): callsite bypass + maps 掩盖
|
||||||
|
s.create_script((RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()).load()
|
||||||
|
s.create_script((RE / "evidence/scripts/mask_frida_maps_only.js").read_text()).load()
|
||||||
|
d.resume(pid)
|
||||||
|
print('resumed')
|
||||||
|
time.sleep(11)
|
||||||
|
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
|
||||||
|
print('patch_guard on')
|
||||||
|
|
||||||
|
sc = s.create_script(JS)
|
||||||
|
state = {'armed': False, 'segv': 0}
|
||||||
|
OUT = Path('/Users/yml/codes/douyu_login_py/evidence/aes_diff.json')
|
||||||
|
def save(k, v):
|
||||||
|
try:
|
||||||
|
data = json.loads(OUT.read_text()) if OUT.exists() else {}
|
||||||
|
except Exception:
|
||||||
|
data = {}
|
||||||
|
data[k] = v
|
||||||
|
OUT.write_text(json.dumps(data, indent=1))
|
||||||
|
def on_msg(m, _):
|
||||||
|
if m.get('type') == 'error':
|
||||||
|
print('JS ERR:', str(m)[:300])
|
||||||
|
elif m.get('type') == 'send':
|
||||||
|
p = m.get('payload') or {}
|
||||||
|
if p.get('type') == 'armed':
|
||||||
|
state['armed'] = True
|
||||||
|
print('[armed]', p['base'])
|
||||||
|
elif p.get('type') == 'segv':
|
||||||
|
state['segv'] += 1
|
||||||
|
print(f"[SEGV #{state['segv']}] {p['info']}")
|
||||||
|
sc.on('message', on_msg)
|
||||||
|
sc.load()
|
||||||
|
assert state['armed']
|
||||||
|
print('ping:', sc.exports.ping())
|
||||||
|
|
||||||
|
k16 = '000102030405060708090a0b0c0d0e0f'
|
||||||
|
KEY27 = 'HuyaUdb1928374650qwertyuiop'.encode().hex()
|
||||||
|
|
||||||
|
# ---- 阶段1: KeyExpansion 轮密钥对拍 ----
|
||||||
|
r = sc.exports.sched(k16)
|
||||||
|
save('sched_kat', r); print('[sched] rk[:32]', r['rk'][:32])
|
||||||
|
|
||||||
|
# ---- 阶段2: 单块 Cipher KAT ----
|
||||||
|
r = sc.exports.cipher(k16, '00112233445566778899aabbccddeeff')
|
||||||
|
save('cipher_kat', r)
|
||||||
|
std = '69c4e0d86a7b0430d8cdb78070b4c55a'
|
||||||
|
ok = r.get('after') == std
|
||||||
|
print(f'[cipher KAT] {r.get("after")} 标准FIPS-197={std} match={ok}')
|
||||||
|
if not ok:
|
||||||
|
print('!! 与标准不一致 -> 自定义点在轮结构/密钥扩展, 后续阶段仍执行')
|
||||||
|
|
||||||
|
# ---- 阶段3: HuyaUdb 主密钥 first16 差分 ----
|
||||||
|
r = sc.exports.cipher(KEY27.encode().hex()[:32], '00' * 16)
|
||||||
|
save('cipher_huyaudb_first16', r); print('[cipher first16(HuyaUdb)]', r)
|
||||||
|
|
||||||
|
# ---- 阶段4: InvCipher 往返 ----
|
||||||
|
ct = sc.exports.cipher(k16, 'cafebabe' * 4).get('after', '')
|
||||||
|
pt = sc.exports.invcipher(k16, ct).get('after', '') if ct else ''
|
||||||
|
print(f'[inv roundtrip] ct={ct[:32]} -> pt={pt} match={pt == "cafebabe" * 4}')
|
||||||
|
save('inv_roundtrip', {'ct': ct, 'pt': pt})
|
||||||
|
|
||||||
|
# ---- 阶段5: 公开 encrypt(最可能崩, 放最后) ----
|
||||||
|
for name, pt in [('empty', ''), ('15B', 'aa' * 15), ('16B', 'bb' * 16), ('187B', 'cc' * 187)]:
|
||||||
|
try:
|
||||||
|
r = sc.exports.encpub(pt, KEY27)
|
||||||
|
o = r.get('out', '')
|
||||||
|
print(f'[encpub {name}] -> {len(o)//2}B head={o[:48]}')
|
||||||
|
save(f'encpub_{name}', r)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[encpub {name}] EXC {e}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
e = sc.exports.encpub('ab' * 40, KEY27)
|
||||||
|
dd = sc.exports.decpub(e.get('out', ''), KEY27)
|
||||||
|
print(f'[roundtrip 80B] ct={len(e.get("out",""))//2}B pt_ok={dd.get("out","").startswith("ab"*16)}')
|
||||||
|
save('roundtrip80', {'e': e, 'd': dd})
|
||||||
|
except Exception as ex:
|
||||||
|
print('[roundtrip] EXC', ex)
|
||||||
|
|
||||||
|
print('done, segv count:', state['segv'])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for attempt in range(4):
|
||||||
|
print(f'===== 尝试 #{attempt+1} =====')
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
break
|
||||||
|
except frida.InvalidOperationError as e:
|
||||||
|
print(f'session detached: {e}')
|
||||||
|
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
|
||||||
|
time.sleep(3)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'err: {e}')
|
||||||
|
time.sleep(3)
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""抓取证书加密真实密钥: hook UdbAESUtil::C1(构造器,收16B key) + encrypt 出入口。
|
||||||
|
|
||||||
|
背景: 差分已证明 Cipher=标准AES-128-ECB(tools/udb_aes.py selftest),
|
||||||
|
剩下唯一未知 = 生产者每次加密用的 key 字节。C1 是所有路径的必经点。
|
||||||
|
|
||||||
|
时序(不可变): force-stop -> spawn挂起 -> 双bypass -> resume -> 10s -> patch_guard -> hooks
|
||||||
|
触发: Java桥直呼 LoginProxy.getQUrlData(uid,"","") 复现证书生成。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import frida
|
||||||
|
|
||||||
|
RE = Path("/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0")
|
||||||
|
UID = 1199666914671
|
||||||
|
|
||||||
|
JS = r"""
|
||||||
|
'use strict';
|
||||||
|
Process.setExceptionHandler(function(d){
|
||||||
|
send({type:'segv', info:{type:d.type, addr:String(d.address)}});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
var base = Process.getModuleByName('libudbauthunify.so').base;
|
||||||
|
send({type:'armed', base:String(base)});
|
||||||
|
|
||||||
|
function rdStr(p){ // libc++ string 安全读取, 返回 {t,len,v(hex)}
|
||||||
|
try{
|
||||||
|
var b0=p.readU8();
|
||||||
|
if((b0&1)===0){ var l=b0>>1; if(l>4096) return {t:'s?',len:l,v:''};
|
||||||
|
return {t:'s',len:l,v:l?Array.from(new Uint8Array(p.add(1).readByteArray(l))).map(x=>('0'+x.toString(16)).slice(-2)).join(''):''}; }
|
||||||
|
var len=parseInt(p.add(8).readU64().toString());
|
||||||
|
if(len>65536) return {t:'l?',len:len,v:''};
|
||||||
|
var dp=p.add(16).readPointer();
|
||||||
|
return {t:'l',len:len,v:Array.from(new Uint8Array(dp.readByteArray(len))).map(x=>('0'+x.toString(16)).slice(-2)).join('')};
|
||||||
|
}catch(e){ return {t:'err',len:-1,v:String(e)}; }
|
||||||
|
}
|
||||||
|
function hx(p,n){ try{ return Array.from(new Uint8Array(p.readByteArray(n))).map(x=>('0'+x.toString(16)).slice(-2)).join(''); }catch(e){ return 'ERR'; } }
|
||||||
|
|
||||||
|
// 1) 构造器: 每次AES对象创建的16字节真钥
|
||||||
|
Interceptor.attach(base.add(0x24eb08), {
|
||||||
|
onEnter: function(a){ send({type:'c1', key:hx(a[1],16), bt:DebugSymbol.fromAddress(this.returnAddress).toString().slice(0,80)}); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2) 公开 encrypt(ret,x1,x2,...): 全参原始dump, 不再猜测约定
|
||||||
|
Interceptor.attach(base.add(0x250038), {
|
||||||
|
onEnter: function(a){
|
||||||
|
this.ra = DebugSymbol.fromAddress(this.returnAddress).toString().slice(0,90);
|
||||||
|
this.retbuf = a[0]; // sret 缓冲指针(onLeave时 x0 可能已变, 用入口快照)
|
||||||
|
send({type:'enc_in', ra:this.ra,
|
||||||
|
x0s:rdStr(a[0]), x1s:rdStr(a[1]), x2s:rdStr(a[2]), x3s:rdStr(a[3]),
|
||||||
|
x1raw:hx(a[1],32), x2raw:hx(a[2],32), x3raw:hx(a[3],32)});
|
||||||
|
},
|
||||||
|
onLeave: function(r){ send({type:'enc_out', ret:rdStr(this.retbuf)}); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2.5) encode_aes(in,key,out): 看选key逻辑
|
||||||
|
Interceptor.attach(base.add(0x330218), {
|
||||||
|
onEnter: function(a){
|
||||||
|
this.p0=rdStr(a[0]); this.p1=rdStr(a[1]);
|
||||||
|
this.ra = DebugSymbol.fromAddress(this.returnAddress).toString().slice(0,90);
|
||||||
|
},
|
||||||
|
onLeave: function(){ send({type:'encode_aes', ra:this.ra, p0:this.p0, p1:this.p1}); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3) _encrypt(block,out&) 入口: 确认其调用方与块内容
|
||||||
|
Interceptor.attach(base.add(0x24f9f0), {
|
||||||
|
onEnter: function(a){
|
||||||
|
send({type:'_enc', ra:DebugSymbol.fromAddress(this.returnAddress).toString().slice(0,90),
|
||||||
|
blk:hx(a[1],16)});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自动触发证书生成
|
||||||
|
Java.perform(function(){
|
||||||
|
var n=0;
|
||||||
|
function go(){
|
||||||
|
try{
|
||||||
|
var seed=Java.use('com.duowan.kiwi.base.login.udb.HuyaLoginProxy');
|
||||||
|
var F=Java.ClassFactory.get(seed.class.getClassLoader());
|
||||||
|
var inst=F.use('com.hysdkproxy.LoginProxy').getInstance();
|
||||||
|
var q=inst.getQUrlData(__UID__, "", "");
|
||||||
|
send({type:'qurl_done', len:q?q.length:0, data:q||''});
|
||||||
|
}catch(e){ n+=1; if(n%6===0) send({type:'retry',n:n}); setTimeout(go,5000); }
|
||||||
|
}
|
||||||
|
setTimeout(go, 20000);
|
||||||
|
});
|
||||||
|
""".replace('__UID__', str(UID))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
d = frida.get_device_manager().add_remote_device('127.0.0.1:31877')
|
||||||
|
pid = d.spawn(['com.duowan.kiwi'])
|
||||||
|
print(f'spawned {pid}')
|
||||||
|
s = d.attach(pid)
|
||||||
|
s.create_script((RE / "evidence/scripts/bypass_msaoaid_maps_art_callsite.js").read_text()).load()
|
||||||
|
s.create_script((RE / "evidence/scripts/mask_frida_maps_only.js").read_text()).load()
|
||||||
|
d.resume(pid)
|
||||||
|
print('resumed')
|
||||||
|
time.sleep(11)
|
||||||
|
s.create_script((RE / "evidence/scripts/patch_guard_block_termination.js").read_text()).load()
|
||||||
|
print('patch_guard on')
|
||||||
|
|
||||||
|
sc = s.create_script(JS)
|
||||||
|
events = []
|
||||||
|
got_armed = []
|
||||||
|
|
||||||
|
def on_msg(m, _):
|
||||||
|
if m.get('type') == 'error':
|
||||||
|
print('JS ERR:', str(m)[:240]); return
|
||||||
|
if m.get('type') != 'send':
|
||||||
|
return
|
||||||
|
p = m.get('payload') or {}
|
||||||
|
t = p.get('type')
|
||||||
|
if t != 'retry':
|
||||||
|
events.append(p)
|
||||||
|
if t == 'armed':
|
||||||
|
got_armed.append(1); print('[armed]', p['base'])
|
||||||
|
elif t == 'c1':
|
||||||
|
print(f"[C1] key={p['key']} <- {p['bt'][:60]}")
|
||||||
|
elif t == 'enc_in':
|
||||||
|
print(f"[enc] ra={p['ra'][:60]}")
|
||||||
|
print(f" x1s={p['x1s']['t']}:{p['x1s']['len']} head={p['x1s']['v'][:48]}")
|
||||||
|
print(f" x2s={p['x2s']['t']}:{p['x2s']['len']} head={p['x2s']['v'][:48]}")
|
||||||
|
print(f" x3s={p['x3s']['t']}:{p['x3s']['len']} head={p['x3s']['v'][:48]}")
|
||||||
|
elif t == 'enc_out':
|
||||||
|
print(f" out={p['ret']['t']}:{p['ret']['len']} head={p['ret']['v'][:48]}")
|
||||||
|
elif t == '_enc':
|
||||||
|
print(f"[_encrypt] blk={p['blk']} <- {p['ra'][:60]}")
|
||||||
|
elif t == 'encode_aes':
|
||||||
|
print(f"[encode_aes] p0={p['p0']['t']}:{p['p0']['len']},{p['p0']['v'][:48]} "
|
||||||
|
f"p1={p['p1']['t']}:{p['p1']['len']},{p['p1']['v'][:48]} <- {p['ra'][:56]}")
|
||||||
|
elif t == 'qurl_done':
|
||||||
|
print('[qurl_done]', p.get('len'), 'head:', (p.get('data') or '')[:60])
|
||||||
|
|
||||||
|
sc.on('message', on_msg)
|
||||||
|
sc.load()
|
||||||
|
assert got_armed
|
||||||
|
|
||||||
|
deadline = time.time() + 150
|
||||||
|
while time.time() < deadline:
|
||||||
|
time.sleep(2)
|
||||||
|
if any(e.get('type') == 'qurl_done' for e in events):
|
||||||
|
time.sleep(8)
|
||||||
|
break
|
||||||
|
|
||||||
|
Path('/Users/yml/codes/douyu_login_py/evidence/cert_keycap.json').write_text(
|
||||||
|
json.dumps(events))
|
||||||
|
from collections import Counter
|
||||||
|
print('saved:', Counter(e.get('type') for e in events))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for attempt in range(4):
|
||||||
|
print(f'===== 尝试 #{attempt+1} =====')
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
break
|
||||||
|
except frida.InvalidOperationError as e:
|
||||||
|
print(f'session detached: {e}')
|
||||||
|
subprocess.run(['adb', 'shell', 'am', 'force-stop', 'com.duowan.kiwi'])
|
||||||
|
time.sleep(3)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'err: {e}')
|
||||||
|
time.sleep(3)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""虎牙 udb 登录证书(biz_token)解析/铸造工具 —— 算法已完全闭环(2026-08-25)。
|
||||||
|
|
||||||
|
== 已证实的完整算法链 ==
|
||||||
|
|
||||||
|
证书 = base64( [0x0c][key_idx][AES-128-ECB(key16, zeropad(P1))] )
|
||||||
|
|
||||||
|
P1 (187B) 结构(两份活体样本逐字段对齐验证):
|
||||||
|
[0x01][0x04][0x00] 固定头
|
||||||
|
["5008"] appId
|
||||||
|
[u16_le=20][20B 随机数] 每次生成时随机
|
||||||
|
[u16_le=40][40B 指纹hex] 设备指纹ASCII("02df39879743..."), 设备绑定、跨会话不变
|
||||||
|
[u16_le=114][114B cred] hyCred 二进制 (=udb_cred base64解码)
|
||||||
|
|
||||||
|
key16 来源: AESkeyMgr 密钥表。rodata 内置默认三条(0x1bc810, 注册id=3/4/5):
|
||||||
|
4VYcPdvKKqjBHZtCmbroRXHk <- 本次活体实测在用(取前16字节)
|
||||||
|
xXEDWqiKLGwEZ6HubEiswCqK
|
||||||
|
3FMHubdKosFrhmXNLHTNHZwe
|
||||||
|
服务端可下发会话密钥(NEWKEY/key_id机制), 证书第二字节即 key_idx:
|
||||||
|
今日 getQUrlData 样本 idx=0x20 <-> "4VYcPdvKKqjBHZtC";
|
||||||
|
更早样本 idx=0x50 用默认钥解不开 => 属服务端下发钥。
|
||||||
|
UdbAESUtil 本体为标准 AES-128-ECB(FIPS-197 KAT 逐位一致), 无魔改。
|
||||||
|
|
||||||
|
调用链: HandlerGetH5InfoEx::onHandler -> BusinessCfg::getOtp -> ... ->
|
||||||
|
gen_biz_token(BIZTOKEN,P1部件...,out) @0x3307e4
|
||||||
|
-> enpack_header/enpack_body/cred_packls/xxtea_encrypt
|
||||||
|
-> encode_aes(P1, key24, out) @0x330218 # C1 只读 key 前16字节
|
||||||
|
-> UdbAESUtil::encrypt @0x250038 # 补零到16倍数逐块
|
||||||
|
-> CBase64::Encode
|
||||||
|
|
||||||
|
验证脚本: scripts/hook_aes_diff.py(差分) / scripts/hook_cert_keycap.py(抓钥)
|
||||||
|
证据: evidence/aes_diff.json / evidence/cert_keycap.json
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
from tools.udb_aes import udb_decrypt, udb_encrypt # noqa: E402
|
||||||
|
|
||||||
|
# so rodata 0x1bc810 起连排的三条24字符默认密钥(AESkeyMgr 注册 id=3/4/5)
|
||||||
|
KEY_TABLE_RAW = [
|
||||||
|
"4VYcPdvKKqjBHZtCmbroRXHk",
|
||||||
|
"xXEDWqiKLGwEZ6HubEiswCqK",
|
||||||
|
"3FMHubdKosFrhmXNLHTNHZwe",
|
||||||
|
]
|
||||||
|
DEFAULT_KEY16 = KEY_TABLE_RAW[0][:16].encode() # 本次活体验证在用
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- P1 组装/解析
|
||||||
|
def build_p1(app_id: bytes, fingerprint: bytes, cred: bytes,
|
||||||
|
rnd: bytes | None = None) -> bytes:
|
||||||
|
"""按活体样本结构组装 P1 明文。
|
||||||
|
|
||||||
|
fingerprint: 40字节 ASCII hex 形如 b'02df39879743...'(设备绑定)
|
||||||
|
cred: 114 字节 hyCred 二进制(base64(udb_cred) 解码后)
|
||||||
|
rnd: 缺省用 os.urandom(20)
|
||||||
|
"""
|
||||||
|
assert len(fingerprint) == 40, f"指纹应40B, 得到{len(fingerprint)}"
|
||||||
|
assert len(cred) == 114, f"cred应114B, 得到{len(cred)}"
|
||||||
|
r = os.urandom(20) if rnd is None else rnd
|
||||||
|
p1 = b"\x01\x04\x00" + app_id
|
||||||
|
p1 += struct.pack("<H", len(r)) + r
|
||||||
|
p1 += struct.pack("<H", len(fingerprint)) + fingerprint
|
||||||
|
p1 += struct.pack("<H", len(cred)) + cred
|
||||||
|
assert len(p1) == 187, len(p1)
|
||||||
|
return p1
|
||||||
|
|
||||||
|
|
||||||
|
def parse_p1(data: bytes) -> dict:
|
||||||
|
o = 0
|
||||||
|
|
||||||
|
def tk(n: int) -> bytes:
|
||||||
|
nonlocal o
|
||||||
|
b = data[o:o + n]
|
||||||
|
o += n
|
||||||
|
return b
|
||||||
|
|
||||||
|
out = {
|
||||||
|
"head": tk(3).hex(),
|
||||||
|
"app_id": tk(4),
|
||||||
|
}
|
||||||
|
n = struct.unpack("<H", tk(2))[0]
|
||||||
|
out["rnd"] = tk(n)
|
||||||
|
n = struct.unpack("<H", tk(2))[0]
|
||||||
|
out["fingerprint"] = tk(n)
|
||||||
|
n = struct.unpack("<H", tk(2))[0]
|
||||||
|
out["cred"] = tk(n)
|
||||||
|
out["trailing"] = data[o:]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 证书层
|
||||||
|
def forge_cert(p1: bytes, key16: bytes = DEFAULT_KEY16,
|
||||||
|
key_idx: int = 0x20, type_byte: int = 0x0C) -> bytes:
|
||||||
|
"""P1 -> 二进制证书 [type][key_idx][192B AES 密文]。"""
|
||||||
|
ct = udb_encrypt(key16, p1)
|
||||||
|
assert len(ct) == 192, len(ct)
|
||||||
|
return bytes([type_byte, key_idx]) + ct
|
||||||
|
|
||||||
|
|
||||||
|
def cert_b64(cert: bytes) -> str:
|
||||||
|
return base64.b64encode(cert).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cert(blob: bytes) -> tuple[int, int, bytes]:
|
||||||
|
"""[0x0c][idx][192B] -> (type, idx, ct)。"""
|
||||||
|
assert blob[0] == 0x0C and len(blob) >= 194, "非证书结构"
|
||||||
|
return blob[0], blob[1], blob[2:194]
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_cert(blob: bytes, key16: bytes = DEFAULT_KEY16) -> bytes:
|
||||||
|
_, _, ct = parse_cert(blob)
|
||||||
|
return udb_decrypt(key16, ct)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 兼容旧接口
|
||||||
|
def try_decrypt_all(ct192: bytes) -> list[tuple[str, bytes]]:
|
||||||
|
return [(k[:16], udb_decrypt(k[:16].encode(), ct192)) for k in KEY_TABLE_RAW]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import json
|
||||||
|
ev = json.load(open(Path(__file__).resolve().parent.parent /
|
||||||
|
"evidence/cert_keycap.json"))
|
||||||
|
enc_in = next(e for e in ev if e.get("type") == "enc_in")
|
||||||
|
P1 = bytes.fromhex(enc_in["x1s"]["v"])
|
||||||
|
fields = parse_p1(P1)
|
||||||
|
print("P1 解析:", {k: (v.hex()[:32] if isinstance(v, bytes) else v)
|
||||||
|
for k, v in fields.items()})
|
||||||
|
# 用捕获的 P1 重铸并与活体密文比对
|
||||||
|
qurl = next(e["data"] for e in ev if e.get("type") == "qurl_done" and e.get("data"))
|
||||||
|
import re
|
||||||
|
raw = base64.b64decode(qurl)
|
||||||
|
inner = []
|
||||||
|
for x in re.findall(rb"[A-Za-z0-9+/]{100,}={0,2}", raw):
|
||||||
|
try:
|
||||||
|
inner.append(base64.b64decode(x + b"=" * ((-len(x)) % 4)))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
blob = max(inner, key=len)
|
||||||
|
mine = forge_cert(P1)
|
||||||
|
print("活体证书 :", blob.hex()[:64])
|
||||||
|
print("重铸证书 :", mine.hex()[:64])
|
||||||
|
print("FULL MATCH:", mine == blob)
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"""UdbAESUtil 参考实现与设备实测对拍。
|
||||||
|
|
||||||
|
2026-08-25 Frida 直调差分结论(scripts/hook_aes_diff.py + evidence/aes_diff.json):
|
||||||
|
- UdbAESUtil::Cipher == 标准 AES-128 加密 (FIPS-197 KAT 逐位一致)
|
||||||
|
- UdbAESUtil::KeyExpansion == 标准展开,轮密钥按「列序/转置」存储
|
||||||
|
- C1(uchar* key) 只读 key 前 16 字节
|
||||||
|
即: 不存在自定义 AES;任何"解密对不上"都是密钥来源/调用方的问题。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
|
MASTER_KEY_STR = b"HuyaUdb1928374650qwertyuiop"
|
||||||
|
|
||||||
|
_SBOX = bytes.fromhex(
|
||||||
|
'637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0'
|
||||||
|
'b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b275'
|
||||||
|
'09832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cf'
|
||||||
|
'd0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2'
|
||||||
|
'cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdb'
|
||||||
|
'e0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08'
|
||||||
|
'ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9e'
|
||||||
|
'e1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16')
|
||||||
|
_INV = bytes.fromhex(
|
||||||
|
'52096ad53036a538bf40a39e81f3d7fb733317225e060830ca22c1e0941053ff'
|
||||||
|
'5013071554c6935c9b6b478e49af25ebd6d13ca8d1b972e183a15b8e35997231'
|
||||||
|
'd18def6e281286be5532e377b905b09393c428c0aab8858fddec191bdbaff54c'
|
||||||
|
'bb617a475e8db4f89d389128723abbd6142cbbb8da24014361a9cf83d0d90c0b'
|
||||||
|
'93ae82fdf3a50165c19fd02fa1ca67094c40b5f9bbc9ecd454f75bc46caa5e58'
|
||||||
|
'f4fc6d7893fa59957c04c0a3a8ef7a3493163b536fceaac2d940e70fa65048e9'
|
||||||
|
'4bccd5d83760a68309a2ded466b75c8976ee06753c70336a124b2042b8db9a8f'
|
||||||
|
'58eec4f73d116f33766f2cc10ed1233b84f21f2679120569ad47eb4e30665ff1')
|
||||||
|
|
||||||
|
|
||||||
|
def _xtime(a: int) -> int:
|
||||||
|
a <<= 1
|
||||||
|
return (a ^ 0x1B) & 0xFF if a & 0x100 else a
|
||||||
|
|
||||||
|
|
||||||
|
def expand_schedule_transposed(key16: bytes) -> bytes:
|
||||||
|
"""标准 AES-128 轮密钥,按 UdbAESUtil 的存储序输出(176B)。
|
||||||
|
|
||||||
|
设备布局: rk[round] 为 16B,按 state 列序存放(rk0..3 = key 的第0列)。
|
||||||
|
"""
|
||||||
|
assert len(key16) == 16
|
||||||
|
words = [list(key16[4 * i:4 * i + 4]) for i in range(4)]
|
||||||
|
rcon = 1
|
||||||
|
for i in range(4, 44):
|
||||||
|
t = list(words[i - 1])
|
||||||
|
if i % 4 == 0:
|
||||||
|
t = t[1:] + t[:1]
|
||||||
|
t = [_SBOX[b] for b in t]
|
||||||
|
t[0] ^= rcon
|
||||||
|
rcon = _xtime(rcon)
|
||||||
|
words.append([words[i - 4][j] ^ t[j] for j in range(4)])
|
||||||
|
# 设备实测布局: 每轮16B按字表转置存放 dev[r*4+c] = words[rnd*4+c][r]
|
||||||
|
out = bytearray()
|
||||||
|
for rnd in range(11):
|
||||||
|
blk = bytearray(16)
|
||||||
|
for c in range(4):
|
||||||
|
for r in range(4):
|
||||||
|
blk[r * 4 + c] = words[rnd * 4 + c][r]
|
||||||
|
out += blk
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_block(key16: bytes, block16: bytes) -> bytes:
|
||||||
|
return AES.new(key16, AES.MODE_ECB).encrypt(block16)
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_block(key16: bytes, block16: bytes) -> bytes:
|
||||||
|
return AES.new(key16, AES.MODE_ECB).decrypt(block16)
|
||||||
|
|
||||||
|
|
||||||
|
def zero_pad(data: bytes) -> bytes:
|
||||||
|
"""UdbAESUtil::encrypt 的填充:补 '\\0' 到 16 的倍数(不追加整块)。"""
|
||||||
|
if len(data) % 16 == 0:
|
||||||
|
return data
|
||||||
|
return data + b"\x00" * (16 - len(data) % 16)
|
||||||
|
|
||||||
|
|
||||||
|
def udb_encrypt(key16: bytes, plain: bytes) -> bytes:
|
||||||
|
p = zero_pad(plain)
|
||||||
|
return AES.new(key16, AES.MODE_ECB).encrypt(p)
|
||||||
|
|
||||||
|
|
||||||
|
def udb_decrypt(key16: bytes, ct: bytes) -> bytes:
|
||||||
|
return AES.new(key16, AES.MODE_ECB).decrypt(ct).rstrip(b"\x00")
|
||||||
|
|
||||||
|
|
||||||
|
def selftest() -> None:
|
||||||
|
# FIPS-197
|
||||||
|
kat_k = bytes(range(16))
|
||||||
|
kat_p = bytes.fromhex("00112233445566778899aabbccddeeff")
|
||||||
|
assert encrypt_block(kat_k, kat_p).hex() == "69c4e0d86a7b0430d8cdb78070b4c55a"
|
||||||
|
# 与设备 dump 的轮密钥对拍
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
j = Path(__file__).resolve().parent.parent / "evidence" / "aes_diff.json"
|
||||||
|
if j.exists():
|
||||||
|
dev = json.loads(j.read_text())["sched_kat"]["rk"]
|
||||||
|
assert expand_schedule_transposed(kat_k).hex() == dev, "schedule mismatch!"
|
||||||
|
dev_ct = json.loads(j.read_text())["cipher_huyaudb_first16"]["after"]
|
||||||
|
# harness 该样本实际喂入的 key = "4875796155646231"(hex串被二次编码, 见脚本注释)
|
||||||
|
ref = encrypt_block(b"4875796155646231", b"\x00" * 16).hex()
|
||||||
|
assert dev_ct == ref, f"huyaudb mismatch {dev_ct} vs {ref}"
|
||||||
|
# 主密钥前16字节的标准指纹(供真实样本比对)
|
||||||
|
ref_master = encrypt_block(MASTER_KEY_STR[:16], b"\x00" * 16).hex()
|
||||||
|
print("master first16 enc(zeros) =", ref_master)
|
||||||
|
print("udb_aes selftest OK")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
selftest()
|
||||||
Reference in New Issue
Block a user