From fda956beca4229d7a404acf29d83c7cdb2c9e633 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 29 Aug 2026 00:37:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(huya):=20=E9=87=8C=E7=A8=8B=E7=A2=91=20-?= =?UTF-8?q?=20UdbAESUtil=E7=BA=AFPython=E5=A4=8D=E5=88=BB=E6=88=90?= =?UTF-8?q?=E5=8A=9F(A/B/D=E4=B8=89=E5=90=91=E9=87=8F=E9=80=90=E5=AD=97?= =?UTF-8?q?=E8=8A=82)=20+=20=E7=99=BB=E5=BD=95=E5=B8=A7t1=3DProtoInfo(appS?= =?UTF-8?q?ign=E6=A7=BD=3DHDID32)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tools/huya_aes_replica.py 617行逐指令复刻, AES-128变异(64B钥材用[0:16]+列链式扩展+移位吸收) - 结构定案: t1 = {0:appSign=ed0db8! 1:appVer 2:sdkVer 3:lcid 4:clientIp 5:channel 6:countryCode} - APP_SIGN错误名=appSign字段校验失败; 下步InvCipher直解明文 --- docs/HUYA_HDID_ALGORITHM_GEN.md | 27 + tools/huya_aes_replica.py | 617 ++++++++++++++++++++ tools/unidbg/hydev/src/hydev/AesProbe.java | 22 + tools/unidbg/hydev/src/hydev/DiffProbe.java | 136 +++++ 4 files changed, 802 insertions(+) create mode 100644 tools/huya_aes_replica.py create mode 100644 tools/unidbg/hydev/src/hydev/DiffProbe.java diff --git a/docs/HUYA_HDID_ALGORITHM_GEN.md b/docs/HUYA_HDID_ALGORITHM_GEN.md index 59499c2..1228811 100644 --- a/docs/HUYA_HDID_ALGORITHM_GEN.md +++ b/docs/HUYA_HDID_ALGORITHM_GEN.md @@ -684,3 +684,30 @@ UdbAESUtilC1(…) + encrypt ← 后段 ### 下轮 1. 探针直接调 AESkeyMgr::getkey(1,…) 取派生钥 → md5_char16 合成验证 2. 子代理 AES 复刻 (运行中) + +## §11.21 里程碑: UdbAESUtil 纯 Python 复刻成功 + t1=ProtoInfo(appSign=HDID32) 对应 (2026-08-28 夜) + +### 1) tools/huya_aes_replica.py (617 行, 逐指令复刻, 子代理产出) +- 5 段内嵌 objdump 原文逐条比对一致; **A/B/D 三向量逐字节命中**: + enc("0123456789abcdef", ZMHAV-24B) = ba3fb8f156b03a9d7db185d1254e0730 ✓ + enc("0123456789abcdef", 64B-0123…) = 72727e881edcfd0100a718687909b565 ✓ + enc("0123456789abcdef", owNMia-24B) = 74bd517c7e5d2bbce63c8e98a192c760 ✓ +- **算法 = AES-128 变异**: 64B 钥材只用 key[0..15] (转置装载 rk[i]=key[4*(i%4)+i//4]); + 扩展 10 轮×16B=176B (AES-128 形); SubWord 输入=相邻列末字节 + 链式 XOR (非标准词独立); + 无独立 ShiftRows (位移吸收进寄存器置换共轭); SBOX/RCON/INV = 标准表。 + +### 2) 登录帧 t1 结构 = mobilequicklogin.ProtoInfo (结构级定案!) +| t1.tag | 字段 | 金样本值 | +|---|---|---| +| 0 | **appSign** | **ed0db8334cadd236c00cadf7e11ab5a5 (HDID32!!)** | +| 1 | appVer | 13.4.22 | +| 2 | sdkVer | 1.0.80138 | +| 3 | lcid | "" | +| 4 | clientIp | 127.0.0.1 | +| 5 | channel | xiaomi | +| 6 | countryCode | "" | +- **"APP_SIGN" 错误名 = 字段名 appSign 的校验失败** — 32hex-HDID32 = appSign 值 = 签名函数产物! + +### 3) 下一关: appSign (ed0db8) 的生成式 +- ed0db8 = encode_aes(16B-plaintext, key) 形态候选; 朴素明文组合 (aid/mid/DEVID40…异或/拼/零垫) 零命中 +- 下步: (a) 补 InvCipher 解密路径 → 直解 ed0db8 见明文; (b) 探针 encode_aes(设备材料块) 差分对拍 diff --git a/tools/huya_aes_replica.py b/tools/huya_aes_replica.py new file mode 100644 index 0000000..667652f --- /dev/null +++ b/tools/huya_aes_replica.py @@ -0,0 +1,617 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Bit-exact Python replica of Huya's proprietary AES (class UdbAESUtil) in +libudbauthunify.so, reconstructed from the ARM64 disassembly. + +Sources: + * Disassembly : /tmp/udb_dis.txt (objdump -d) + * Tables : libudbauthunify_merged.so @0x1c5d8e (SBOX), @0x1c5e8e + (INV-SBOX), @0x1c5f8e (RCON[1..10]) — all standard AES. + +Reverse-engineered structure (offsets in .text): + Ctor UdbAESUtil::UdbAESUtil(uchar* key) @0x24eb08 + copies SBOX (rodata 0x1c5d8e, 256B) -> this+0x08 + copies INV-SBOX(rodata 0x1c5e8e, 256B) -> this+0x108 + tail-call KeyExpansion(key, this+0x208) + + UdbAESUtil::KeyExpansion(uchar* key, uchar* rk) @0x24ebd4 + * only key[0..15] is ever used (bytes 16..63 are ignored) + * prologue: rk[i] = key[4*(i%4) + i//4] (4x4 transpose) + * 10 iterations -> 16 bytes each -> 176 bytes (11 round keys): + new[4j] = SBOX[prev[4*((j+1)%4)+3]] ^ (RCON[iter] if j==0 else 0) + ^ prev[4j] + new[4j+r] = new[4j+r-1] ^ prev[4j+r] (r = 1..3) + This is the classic 4-word AES-128 expansion, but the word + "RotWord+SubWord" term is applied to *columns* with the SBOX fed + by the last byte of the FOLLOWING column — a genuine mutation of + the standard key schedule. + + UdbAESUtil::Cipher(uchar* state) @0x24f314 + * initial AddRoundKey with rk[0..15] + * 10 rounds: SubBytes -> (MixColumns unless last round) -> + AddRoundKey(rk[1..10 loops]) ; round counter in x8 starts at 10, + decremented inside the AddRoundKey block, exit when it hits 0. + * The body is executed instruction-by-instruction below: SubBytes is + fused with a register permutation (SBOX results land in rotated + registers), MixColumns works on 32-bit sign-extended values with + csel-based xtime reduction, and a 5-register rename closes the + loop. All of that is reproduced literally. + * There is no separate ShiftRows: the row shift is absorbed into the + initial register permutation (the SBOX write-order) and the + MixColumns operand wiring. + + UdbAESUtil::encrypt / _encrypt (inline copy of the same core) @0x24f9f0 +""" + +import re +import sys + +# --------------------------------------------------------------------------- +# Tables (identical to the .so rodata; standard AES tables) +# --------------------------------------------------------------------------- +SBOX = bytes.fromhex( + "637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0" + "b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b275" + "09832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cf" + "d0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2" + "cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdb" + "e0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08" + "ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9e" + "e1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16") + +INV_SBOX = bytes.fromhex( + "52096ad53036a538bf40a39e81f3d7fb7ce339829b2fff87348e4344c4dee9cb" + "547b9432a6c2233dee4c950b42fac34e082ea16628d924b2765ba2496d8bd1257" + "2f8f66486689816d4a45ccc5d65b6926c704850fdedb9da5e154657a78d9d8490" + "d8ab008cbcd30af7e45805b8b34506d02c1e8fca3f0f02c1afbd0301138a6b3a" + "9111414f67dcea97f2cfcef0b4e67396ac7422e7ad3585e2f937e81c75df6e47" + "f11a711d29c5896fb7620eaa18be1bfc563e4bc6d279209adbc0fe78cd5af41f" + "dda8338807c731b11210592780ec5f60517fa919b54a0d2de57a9f93c99cefa0" + "e03b4dae2af5b0c8ebbb3c83539961172b047eba77d626e169146355210c7d") + +RCON = bytes([0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36]) + +M32 = 0xFFFFFFFF + + +def sxt8(b: int) -> int: + """Sign-extend byte b to a 32-bit register value (ldrsb semantics).""" + return b if b < 0x80 else (b - 0x100) & M32 + + +# --------------------------------------------------------------------------- +# KeyExpansion @0x24ebd4 (compact byte-level form of the loop, verified +# equation-by-equation against 0x24ec64..0x24ed54) +# --------------------------------------------------------------------------- +def key_expansion(key: bytes) -> bytes: + assert len(key) >= 16 + rk = bytearray(16 + 10 * 16) + + # prologue @0x24ebd4..0x24ec5c: rk[i] = key[4*(i % 4) + i // 4] + for i in range(16): + rk[i] = key[4 * (i % 4) + i // 4] + + # loop @0x24ec64..0x24ed54 (x8 = 0..9), each iteration -> one round key + for i in range(10): + p, n = i * 16, (i + 1) * 16 + rc = RCON[i] + ch = [0] * 4 + ch[0] = SBOX[rk[p + 7]] ^ rc ^ rk[p + 0] + ch[1] = SBOX[rk[p + 11]] ^ rk[p + 4] + ch[2] = SBOX[rk[p + 15]] ^ rk[p + 8] + ch[3] = SBOX[rk[p + 3]] ^ rk[p + 12] + for j in range(4): + rk[n + 4 * j] = ch[j] + for r in range(1, 4): + for j in range(4): + rk[n + 4 * j + r] = rk[n + 4 * j + r - 1] ^ rk[p + 4 * j + r] + return bytes(rk) + + +# --------------------------------------------------------------------------- +# Cipher @0x24f314 — literal instruction-level execution of the disassembly +# --------------------------------------------------------------------------- + +# (addr, mnemonic-with-operands) extracted verbatim from /tmp/udb_dis.txt +CIPHER_INIT = """ +24f32c: ldrb w9, [x1] +24f330: ldrb w11, [x0, #0x208] +24f334: mov w8, #0xa +24f338: ldrb w13, [x1, #0x1] +24f33c: ldrb w14, [x0, #0x20c] +24f340: ldrb w15, [x1, #0x2] +24f344: ldrb w16, [x0, #0x210] +24f348: eor w22, w9, w11 +24f34c: ldrb w9, [x1, #0x3] +24f350: ldrb w11, [x0, #0x214] +24f354: eor w20, w13, w14 +24f358: ldrb w10, [x1, #0x4] +24f35c: eor w6, w15, w16 +24f360: ldrb w14, [x0, #0x209] +24f364: ldrb w15, [x1, #0x5] +24f368: eor w3, w9, w11 +24f36c: ldrb w9, [x0, #0x20d] +24f370: ldrb w11, [x1, #0x6] +24f374: eor w7, w10, w14 +24f378: ldrb w10, [x0, #0x211] +24f37c: ldrb w14, [x1, #0x7] +24f380: eor w19, w15, w9 +24f384: ldrb w9, [x0, #0x215] +24f388: ldrb w12, [x1, #0x8] +24f38c: ldrb w15, [x1, #0x9] +24f390: eor w5, w11, w10 +24f394: ldrb w10, [x0, #0x20a] +24f398: eor w21, w14, w9 +24f39c: ldrb w9, [x0, #0x20e] +24f3a0: ldrb w13, [x1, #0xc] +24f3a4: ldrb w11, [x1, #0xd] +24f3a8: eor w2, w12, w10 +24f3ac: ldrb w14, [x1, #0xa] +24f3b0: ldrb w16, [x1, #0xe] +24f3b4: eor w17, w15, w9 +24f3b8: ldrb w10, [x1, #0xb] +24f3bc: ldrb w12, [x1, #0xf] +24f3c0: ldrb w9, [x0, #0x212] +24f3c4: ldrb w15, [x0, #0x216] +24f3c8: ldrb w4, [x0, #0x20b] +24f3cc: ldrb w25, [x0, #0x20f] +24f3d0: ldrb w26, [x0, #0x213] +24f3d4: eor w24, w14, w9 +24f3d8: eor w23, w10, w15 +24f3dc: add x9, x0, #0x8 +24f3e0: ldrb w27, [x0, #0x217] +24f3e4: eor w14, w13, w4 +24f3e8: eor w4, w11, w25 +24f3ec: eor w16, w16, w26 +24f3f0: add x10, x0, #0x227 +24f3f4: mov w11, #0x1b +24f3f8: eor w15, w12, w27 +""" + +ARK = """ +24f400: ldurb w6, [x10, #-0xf] +24f404: ldurb w21, [x10, #-0xb] +24f408: subs x8, x8, #0x1 +24f40c: ldurb w23, [x10, #-0x7] +24f410: ldurb w26, [x10, #-0xe] +24f414: ldurb w25, [x10, #-0x3] +24f418: eor w22, w22, w6 +24f41c: eor w20, w20, w21 +24f420: ldurb w21, [x10, #-0xa] +24f424: eor w6, w24, w23 +24f428: eor w7, w7, w26 +24f42c: ldurb w23, [x10, #-0x6] +24f430: ldurb w24, [x10, #-0x2] +24f434: ldurb w26, [x10, #-0x9] +24f438: eor w3, w3, w25 +24f43c: eor w19, w19, w21 +24f440: ldurb w25, [x10, #-0xd] +24f444: eor w5, w5, w23 +24f448: eor w21, w17, w24 +24f44c: ldurb w23, [x10, #-0x5] +24f450: eor w17, w4, w26 +24f454: ldurb w4, [x10, #-0x1] +24f458: eor w2, w2, w25 +24f45c: ldurb w25, [x10, #-0x8] +24f460: eor w24, w0, w23 +24f464: ldurb w0, [x10, #-0xc] +24f468: eor w23, w16, w4 +24f46c: ldurb w16, [x10, #-0x4] +24f470: eor w4, w13, w25 +24f474: ldrb w26, [x10], #0x10 +24f478: eor w14, w14, w0 +24f47c: eor w16, w12, w16 +24f480: eor w15, w15, w26 +""" + +SUBBYTES = """ +24f488: and x12, x22, #0xff +24f48c: and x13, x7, #0xff +24f490: and x17, x17, #0xff +24f494: ldrsb w22, [x9, x12] +24f498: and x12, x20, #0xff +24f49c: ldrsb w7, [x9, x13] +24f4a0: ldrsb w13, [x9, x12] +24f4a4: and x12, x19, #0xff +24f4a8: and x0, x2, #0xff +24f4ac: ldrsb w20, [x9, x12] +24f4b0: and x12, x4, #0xff +24f4b4: ldrsb w19, [x9, x17] +24f4b8: ldrsb w4, [x9, x12] +24f4bc: and x12, x6, #0xff +24f4c0: and x17, x5, #0xff +24f4c4: ldrsb w2, [x9, x0] +24f4c8: ldrsb w0, [x9, x12] +24f4cc: ldrsb w12, [x9, x17] +24f4d0: and x17, x24, #0xff +24f4d4: and x16, x16, #0xff +24f4d8: and x14, x14, #0xff +24f4dc: ldrsb w24, [x9, x17] +24f4e0: and x17, x3, #0xff +24f4e4: ldrsb w5, [x9, x16] +24f4e8: and x16, x21, #0xff +24f4ec: and x3, x23, #0xff +24f4f0: and x6, x15, #0xff +24f4f4: ldrsb w14, [x9, x14] +24f4f8: ldrsb w17, [x9, x17] +24f4fc: ldrsb w16, [x9, x16] +24f500: ldrsb w15, [x9, x3] +24f504: ldrsb w3, [x9, x6] +""" + +MIXCOL = """ +24f510: lsl w6, w22, #1 +24f514: lsl w21, w20, #1 +24f518: cmp w22, #0x0 +24f51c: lsl w25, w24, #1 +24f520: eor w26, w24, w22 +24f524: eor w23, w24, w20 +24f528: eor w27, w6, w11 +24f52c: eor w26, w26, w3 +24f530: eor w23, w23, w3 +24f534: csel w27, w27, w6, lt +24f538: eor w6, w21, w11 +24f53c: cmp w20, #0x0 +24f540: csel w6, w6, w21, lt +24f544: eor w21, w25, w11 +24f548: cmp w24, #0x0 +24f54c: csel w21, w21, w25, lt +24f550: eor w25, w26, w6 +24f554: lsl w26, w3, #1 +24f558: eor w23, w23, w27 +24f55c: eor w22, w20, w22 +24f560: eor w20, w25, w21 +24f564: eor w6, w23, w6 +24f568: eor w23, w26, w11 +24f56c: cmp w3, #0x0 +24f570: eor w3, w22, w3 +24f574: lsl w25, w7, #1 +24f578: csel w23, w23, w26, lt +24f57c: eor w3, w3, w21 +24f580: eor w22, w22, w24 +24f584: lsl w24, w19, #1 +24f588: eor w21, w3, w23 +24f58c: eor w3, w22, w27 +24f590: eor w22, w25, w11 +24f594: cmp w7, #0x0 +24f598: eor w26, w5, w19 +24f59c: lsl w27, w5, #1 +24f5a0: csel w22, w22, w25, lt +24f5a4: eor w25, w24, w11 +24f5a8: cmp w19, #0x0 +24f5ac: eor w26, w26, w17 +24f5b0: eor w3, w3, w23 +24f5b4: csel w23, w25, w24, lt +24f5b8: eor w24, w26, w22 +24f5bc: eor w26, w5, w7 +24f5c0: lsl w29, w17, #1 +24f5c4: eor w25, w27, w11 +24f5c8: cmp w5, #0x0 +24f5cc: eor w26, w26, w17 +24f5d0: eor w28, w19, w7 +24f5d4: eor w7, w24, w23 +24f5d8: csel w24, w25, w27, lt +24f5dc: eor w19, w26, w23 +24f5e0: eor w23, w29, w11 +24f5e4: cmp w17, #0x0 +24f5e8: eor w17, w28, w17 +24f5ec: lsl w25, w2, #1 +24f5f0: csel w23, w23, w29, lt +24f5f4: eor w17, w17, w24 +24f5f8: lsl w26, w4, #1 +24f5fc: eor w19, w19, w24 +24f600: eor w24, w28, w5 +24f604: eor w5, w17, w23 +24f608: eor w17, w25, w11 +24f60c: cmp w2, #0x0 +24f610: eor w22, w24, w22 +24f614: eor w27, w0, w4 +24f618: csel w24, w17, w25, lt +24f61c: eor w25, w26, w11 +24f620: cmp w4, #0x0 +24f624: lsl w28, w0, #1 +24f628: eor w17, w22, w23 +24f62c: csel w22, w25, w26, lt +24f630: eor w23, w27, w16 +24f634: eor w26, w0, w2 +24f638: cmp w0, #0x0 +24f63c: eor w25, w28, w11 +24f640: eor w26, w26, w16 +24f644: lsl w27, w16, #1 +24f648: eor w23, w23, w24 +24f64c: csel w25, w25, w28, lt +24f650: eor w26, w26, w22 +24f654: eor w28, w4, w2 +24f658: eor w2, w23, w22 +24f65c: lsl w23, w14, #1 +24f660: eor w4, w26, w25 +24f664: eor w22, w27, w11 +24f668: cmp w16, #0x0 +24f66c: eor w16, w28, w16 +24f670: lsl w26, w13, #1 +24f674: csel w22, w22, w27, lt +24f678: eor w16, w16, w25 +24f67c: eor w0, w28, w0 +24f680: eor w25, w23, w11 +24f684: cmp w14, #0x0 +24f688: eor w27, w16, w22 +24f68c: eor w16, w0, w24 +24f690: csel w0, w25, w23, lt +24f694: eor w23, w26, w11 +24f698: cmp w13, #0x0 +24f69c: eor w24, w12, w13 +24f6a0: eor w16, w16, w22 +24f6a4: csel w22, w23, w26, lt +24f6a8: eor w23, w24, w15 +24f6ac: lsl w24, w12, #1 +24f6b0: eor w25, w12, w14 +24f6b4: eor w23, w23, w0 +24f6b8: eor w13, w13, w14 +24f6bc: cmp w12, #0x0 +24f6c0: eor w14, w23, w22 +24f6c4: lsl w23, w15, #1 +24f6c8: eor w26, w24, w11 +24f6cc: eor w25, w25, w15 +24f6d0: csel w24, w26, w24, lt +24f6d4: cmp w15, #0x0 +24f6d8: eor w22, w25, w22 +24f6dc: eor w25, w23, w11 +24f6e0: eor w15, w13, w15 +24f6e4: eor w12, w13, w12 +24f6e8: csel w13, w25, w23, lt +24f6ec: eor w15, w15, w24 +24f6f0: eor w12, w12, w0 +24f6f4: eor w22, w22, w24 +24f6f8: eor w0, w15, w13 +24f6fc: eor w15, w12, w13 +24f700: mov w12, w0 +24f704: mov w0, w27 +24f708: mov w24, w21 +24f70c: mov w13, w22 +24f710: mov w22, w6 +""" + +FINAL_STORE = """ +24f718: strb w22, [x1] +24f720: strb w20, [x1, #0x1] +24f728: strb w19, [x1, #0x5] +24f730: strb w24, [x1, #0xa] +24f738: strb w21, [x1, #0x7] +24f740: strb w23, [x1, #0xb] +24f748: strb w7, [x1, #0x4] +24f74c: strb w2, [x1, #0x8] +24f750: strb w14, [x1, #0xc] +24f754: strb w17, [x1, #0x9] +24f758: strb w4, [x1, #0xd] +24f75c: strb w6, [x1, #0x2] +24f760: strb w5, [x1, #0x6] +24f764: strb w16, [x1, #0xe] +24f768: strb w3, [x1, #0x3] +24f76c: strb w15, [x1, #0xf] +""" + + +def _parse(blob): + """Parse the embedded disassembly text into [(addr, op, operands...)].""" + ins = [] + for ln in blob.strip().splitlines(): + m = re.match(r'^\s*([0-9a-f]{6}):\s+([a-z0-9]+)\s*(.*)$', ln) + assert m, f"cannot parse line: {ln!r}" + rest = m.group(3).split('//')[0] + ops = re.findall(r'\[[^\]]*\]|\[[^\]]*\]?\s*,?\s*#[0-9a-f]+' + r'|\blt\b|[wx][0-9]{1,2}|#-?0x[0-9a-f]+|#[0-9a-f]+', rest) + ins.append((int(m.group(1), 16), m.group(2), ops)) + return ins + + +def _check_sequence(blob, lo, hi, name): + """Sanity: the embedded list must exactly match the disassembly file.""" + import os + if not os.path.exists('/tmp/udb_dis.txt'): + return + want = {} + pat = re.compile(r'^\s*([0-9a-f]{6}):\s+[0-9a-f]{8}\s+(.*)$') + with open('/tmp/udb_dis.txt') as f: + for ln in f: + m = pat.match(ln) + if m: + a = int(m.group(1), 16) + if lo <= a <= hi: + want[a] = ' '.join(m.group(2).split('//')[0].split()) + got = {} + for a, op, ops in _parse(blob): + got[a] = op + (' ' + ' '.join(ops) if ops else '') + want = {a: v for a, v in want.items() + if a in got} # ignore stack-save noise (ldp/stp) not embedded + ok = True + for a in sorted(set(got) | set(want)): + if got.get(a, '').replace(',', '') != want.get(a, '').replace(',', ''): + ok = False + print(f"[{name}] addr {a:#x}: embedded={got.get(a)!r} " + f"disasm={want.get(a)!r} (first diff shown)") + break + if ok: + print(f"[{name}] embedded instruction list matches the disassembly file") + + +class _Ctx: + """Register file + memory view for the Cipher loop.""" + + def __init__(self, pt: bytes, rk: bytes): + self.R = {} + self.pt = pt + self.rk = rk # 176-byte expanded key (this+0x208) + self.rkpos = 0x1f # x10 = this+0x227 -> index 0x1f within rk + self.out = bytearray(16) + + def exec_(self, ins): + for addr, op, ops in ins: + # -- register-register / immediate ALU --------------------- + if op == 'mov': + d, s = ops + if s.startswith('#'): + self.R[int(d[1:])] = int(s[1:], 16) + else: + self.R[int(d[1:])] = self.R[int(s[1:])] + elif op == 'add': + d, s, imm = ops + if s == 'x0': + # add x9, x0, #0x8 -> SBOX base (ignored) + # add x10, x0, #0x227 -> rk offset base + if int(imm[1:], 16) == 0x227: + self.rkpos = int(imm[1:], 16) - 0x208 + elif op == 'and': + d, s, imm = ops # and xR, xS, #0xff + assert int(imm[1:], 16) == 0xff + self.R[int(d[1:])] = self.R[int(s[1:])] & 0xff + elif op == 'eor': + d, a, b = ops + self.R[int(d[1:])] = self.R[int(a[1:])] ^ self.R[int(b[1:])] + elif op == 'lsl': + d, s, sh = ops # lsl wR, wS, #1 + assert int(sh[1:], 16) == 1 + self.R[int(d[1:])] = (self.R[int(s[1:])] << 1) & M32 + elif op == 'cmp': + a, b = ops + val = self.R[int(a[1:])] + self.lt = bool(val & 0x80000000) # cmp wX, #0x0 + elif op == 'subs': + d, s, imm = ops + self.R[int(d[1:])] = self.R[int(s[1:])] - int(imm[1:], 16) + elif op == 'csel': + d, t, f_, cc = ops # csel wR, wT, wF, lt + self.R[int(d[1:])] = self.R[int(t[1:])] if self.lt \ + else self.R[int(f_[1:])] + # -- loads -------------------------------------------------- + elif op in ('ldrb', 'ldurb'): + self._load(ops) + elif op == 'ldrsb': + d = ops[0] + mem = ops[1] + m = re.match(r'\[(x\d+),\s*(x\d+)\]', mem) + base, idx = m.group(1), m.group(2) + assert base == 'x9' + self.R[int(d[1:])] = sxt8(SBOX[self.R[int(idx[1:])] & 0xff]) + # -- stores (final only) ------------------------------------ + elif op == 'strb': + d, mem = ops # strb wR, [x1, #imm] + self._store(d, mem) + else: + raise AssertionError(f"unhandled op {op}") + + def _load(self, ops): + d, mem = ops[0], ops[1] + m = re.match(r'\[(x\d+)(?:,\s*#(-?0x[0-9a-f]+))?\]', mem) + base, imm = m.group(1), m.group(2) + off = int(imm, 16) if imm else 0 + if base == 'x1': + val = self.pt[off] + elif base == 'x0': + val = self.rk[off - 0x208] + elif base == 'x10': + val = self.rk[self.rkpos + off] + if ops[2:] and ops[2].startswith('#0x10'): + # ldrb w26, [x10], #0x10 -- post-increment + self.rkpos += 0x10 + else: + raise AssertionError(f"load base {base}") + self.R[int(d[1:])] = val + + def _store(self, d, mem): + m = re.match(r'\[(x1)(?:,\s*#(0x[0-9a-f]+))?\]', mem) + off = int(m.group(2), 16) if m.group(2) else 0 + self.out[off] = self.R[int(d[1:])] & 0xff + + +def cipher(pt: bytes, rk: bytes) -> bytes: + """Encrypt one 16-byte block (exact replica of UdbAESUtil::Cipher).""" + assert len(pt) == 16 and len(rk) == 176 + ctx = _Ctx(pt, rk) + init = _parse(CIPHER_INIT) + sub = _parse(SUBBYTES) + mc = _parse(MIXCOL) + ark = _parse(ARK) + fin = _parse(FINAL_STORE) + + ctx.exec_(init) # initial AddRoundKey + register setup + assert ctx.R[8] == 10 and ctx.rkpos == 0x1f + for _ in range(10): + ctx.exec_(sub) # SubBytes (fused with register permutation) + if ctx.R[8] != 1: # cmp x8, #1 ; b.eq -> skip MixColumns + ctx.exec_(mc) # MixColumns + rename + ctx.exec_(ark) # AddRoundKey; subs x8; post-inc rkpos + if ctx.R[8] == 0: # b.eq 0x24f718 -> final store + break + # rkpos must have advanced to the next round key block + # (x10 = this+0x227 + 16*k ; block k+1 spans rk[16+16k .. 31+16k]) + ctx.exec_(fin) + return bytes(ctx.out) + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- +def encode_aes(plaintext: bytes, key: bytes) -> bytes: + """16-byte block encrypt; key of any length >= 16 (only first 16 used).""" + if len(plaintext) != 16: + raise ValueError("plaintext must be exactly 16 bytes") + if len(key) < 16: + raise ValueError("key must be at least 16 bytes") + return cipher(plaintext, key_expansion(key)) + + +# --------------------------------------------------------------------------- +# Self-test vs the authoritative unidbg AesProbe vectors +# --------------------------------------------------------------------------- +def _check(name, got, want): + ok = got.hex() == want + print(f"[{name}] {'PASS' if ok else 'FAIL'} got={got.hex()} want={want}") + return ok + + +def main(): + _check_sequence(CIPHER_INIT, 0x24f32c, 0x24f3f8, "CIPHER_INIT") + _check_sequence(ARK, 0x24f400, 0x24f480, "ARK") + _check_sequence(SUBBYTES, 0x24f488, 0x24f506, "SUBBYTES") + _check_sequence(MIXCOL, 0x24f510, 0x24f712, "MIXCOL") + _check_sequence(FINAL_STORE, 0x24f718, 0x24f76c, "FINAL_STORE") + + keyA = b"0123456789abcdef" * 4 + keyB = b"ZMHAVPRaxJ3MtXDjduUnXAKQ" + b"\0" * (64 - 24) + keyD = b"owNMiaCgcHmqoTr3iRamFuHj" + b"\0" * (64 - 24) + pt = b"0123456789abcdef" + + # --- hard assertions (all three verified against the AesProbe output) --- + okA = _check("A", encode_aes(pt, keyA), "72727e881edcfd0100a718687909b565") + okB = _check("B", encode_aes(pt, keyB), "ba3fb8f156b03a9d7db185d1254e0730") + okD = _check("D", encode_aes(pt, keyD), "74bd517c7e5d2bbce63c8e98a192c760") + + # --- vector C from the task brief -------------------------------------- + # plaintext given as 16 hex chars = 8 bytes; a 16-byte block is required. + # The 16-byte form used by the original probe is not present in this + # workspace (AesProbe.java only calls encode_aes with the A/B/D inputs), + # so C cannot be reproduced until its full plaintext is supplied. + c_pt = bytes.fromhex("1e8bdf7d4f7a01d3") + if len(c_pt) != 16: + print(f"[C] SKIP: plaintext truncated in task brief " + f"({len(c_pt)} of 16 bytes given) - cannot verify " + f"2cdcf6ad78b8fe0b9ed56004054d2a09") + okC = True + else: + okC = _check("C", encode_aes(c_pt, keyB), + "2cdcf6ad78b8fe0b9ed56004054d2a09") + + # key[16..63] must be irrelevant (matches the disassembly) + assert encode_aes(pt, keyA[:16]) == encode_aes(pt, keyA) + + print() + if okA and okB and okC and okD: + print("ALL VERIFIABLE PROBE VECTORS (A/B/D) REPRODUCED BIT-EXACTLY") + return 0 + print("MISMATCH(ES) DETECTED") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/tools/unidbg/hydev/src/hydev/AesProbe.java b/tools/unidbg/hydev/src/hydev/AesProbe.java index f6fdf39..74fd611 100644 --- a/tools/unidbg/hydev/src/hydev/AesProbe.java +++ b/tools/unidbg/hydev/src/hydev/AesProbe.java @@ -156,6 +156,27 @@ public class AesProbe { return ""; } + void dumpKeyMgr() { + UnidbgPointer self = emulator.getMemory().malloc(0x80, false).getPointer(); + self.write(0, new byte[0x80], 0, 0x80); + module.callFunction(emulator, 0x26fcc8L, UnidbgPointer.nativeValue(self)); + System.out.println("[keymgr] C2 done. object 0x80B:"); + byte[] buf = new byte[0x80]; + self.read(0, buf, 0, 0x80); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 0x80; i++) { + byte b = buf[i]; + if (b >= 0x20 && b < 0x7f) sb.append((char) b); else sb.append('.'); + if ((i + 1) % 16 == 0) sb.append('\n'); + } + System.out.println(sb.toString()); + // 也尝试 getkey(1,0) 返回值 (x0) + try { + Number r = module.callFunction(emulator, 0x26871cL, UnidbgPointer.nativeValue(self), 1L, 0L); + System.out.println("[keymgr] getkey(1,0) ret-x0 = " + Long.toHexString(r == null ? -1 : r.longValue())); + } catch (Throwable t) { System.out.println("[keymgr] getkey err " + t); } + } + static String toHex(String s) { StringBuilder sb = new StringBuilder(); for (byte b : s.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1)) sb.append(String.format("%02x", b)); @@ -192,6 +213,7 @@ public class AesProbe { p.callOtp("abcd", "ZMHAVPRaxJ3MtXDjduUnXAKQ"); p.callXxtea("abc", "ZMHAVPRaxJ3MtXDjduUnXAKQ"); p.callMd5Char16("abc"); + p.dumpKeyMgr(); p.emulator.close(); p.emulator.close(); } diff --git a/tools/unidbg/hydev/src/hydev/DiffProbe.java b/tools/unidbg/hydev/src/hydev/DiffProbe.java new file mode 100644 index 0000000..e05bdcd --- /dev/null +++ b/tools/unidbg/hydev/src/hydev/DiffProbe.java @@ -0,0 +1,136 @@ +package hydev; + +import com.github.unidbg.AndroidEmulator; +import com.github.unidbg.Module; +import com.github.unidbg.arm.backend.BackendFactory; +import com.github.unidbg.arm.backend.DynarmicFactory; +import com.github.unidbg.linux.android.AndroidEmulatorBuilder; +import com.github.unidbg.linux.android.AndroidResolver; +import com.github.unidbg.linux.android.dvm.DalvikModule; +import com.github.unidbg.linux.android.dvm.VM; +import com.github.unidbg.memory.Memory; +import com.github.unidbg.pointer.UnidbgPointer; + +import java.io.File; +import java.util.Arrays; + +/** + * 差分探针: 与 huya_aes_replica.py 对拍 encode_aes@0x330218 + * 1) C-vector: in="1e8bdf7d4f7a01d3"(ASCII 16B) key=ZMHAV..+40NUL -> ? + * 2) 随机差分: 若干 16B 明文 x 64B 钥 + */ +public class DiffProbe { + + private final AndroidEmulator emulator; + private final Module module; + + DiffProbe(String soPath) throws Exception { + BackendFactory backend = new DynarmicFactory(true); + emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.duowan.kiwi") + .addBackendFactory(backend).build(); + Memory memory = emulator.getMemory(); + memory.setLibraryResolver(new AndroidResolver(23)); + VM vm = emulator.createDalvikVM(); + vm.setJni(new com.github.unidbg.linux.android.dvm.AbstractJni() {}); + vm.setVerbose(false); + DalvikModule dm = vm.loadLibrary(new File(soPath), false); + dm.callJNI_OnLoad(emulator); + module = dm.getModule(); + } + + UnidbgPointer writeStdString(String s) { + Memory mem = emulator.getMemory(); + byte[] data = s.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); + int len = data.length; + UnidbgPointer p; + if (len <= 22) { + p = mem.malloc(24, false).getPointer(); + byte[] buf = new byte[24]; + buf[0] = (byte) (len << 1); + System.arraycopy(data, 0, buf, 1, len); + p.write(0, buf, 0, buf.length); + } else { + UnidbgPointer heap = mem.malloc(len + 1, false).getPointer(); + heap.write(0, data, 0, len); + heap.write(len, new byte[]{0}, 0, 1); + p = mem.malloc(24, false).getPointer(); + byte[] cap = new byte[8]; Arrays.fill(cap, (byte) 0xff); + p.write(0, cap, 0, 8); + p.write(8, new byte[]{(byte) (len & 0xff), (byte) (len >> 8), 0, 0, 0, 0, 0, 0}, 0, 8); + byte[] ptrBytes = new byte[8]; + long pv = UnidbgPointer.nativeValue(heap); + for (int i = 0; i < 8; i++) ptrBytes[i] = (byte) (pv >> (8 * i)); + p.write(16, ptrBytes, 0, 8); + } + return p; + } + + String readStdString(UnidbgPointer p) { + byte[] one = new byte[1]; + p.read(0, one, 0, 1); + int first = one[0] & 0xff; + if ((first & 1) == 0) { + int len = first >> 1; + byte[] dat = new byte[len]; + p.read(1, dat, 0, len); + return new String(dat, java.nio.charset.StandardCharsets.ISO_8859_1); + } else { + long size = 0; + byte[] szb = new byte[8]; + p.read(8, szb, 0, 8); + for (int i = 0; i < 8; i++) size |= ((long) (szb[i] & 0xff)) << (8 * i); + byte[] ptrb = new byte[8]; + p.read(16, ptrb, 0, 8); + long pv = 0; + for (int i = 0; i < 8; i++) pv |= ((long) (ptrb[i] & 0xff)) << (8 * i); + UnidbgPointer data = UnidbgPointer.pointer(this.emulator, pv); + byte[] dat2 = new byte[(int) size]; + data.read(0, dat2, 0, (int) size); + return new String(dat2, java.nio.charset.StandardCharsets.ISO_8859_1); + } + } + + String enc(String in, String key) { + UnidbgPointer pin = writeStdString(in); + UnidbgPointer pkey = writeStdString(key); + UnidbgPointer pout = writeStdString(""); + module.callFunction(emulator, 0x330218L, UnidbgPointer.nativeValue(pin), + UnidbgPointer.nativeValue(pkey), UnidbgPointer.nativeValue(pout)); + String out = readStdString(pout); + StringBuilder sb = new StringBuilder(); + for (byte b : out.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1)) + sb.append(String.format("%02x", b)); + return sb.toString(); + } + + static String hexBytes(int n, long seed) { + // deterministic "random" 16-byte hex string + StringBuilder sb = new StringBuilder(); + java.util.Random r = new java.util.Random(seed); + for (int i = 0; i < n; i++) sb.append(String.format("%02x", r.nextInt(256))); + return sb.toString(); + } + + public static void main(String[] args) throws Exception { + String so = args.length > 0 ? args[0] : "so/libudbauthunify_merged.so"; + DiffProbe p = new DiffProbe(so); + String kzm = "ZMHAVPRaxJ3MtXDjduUnXAKQ" + "\0".repeat(40); + String k00 = "0123456789abcdef".repeat(4); + + System.out.println("[C1] ascii-16 in=1e8bdf7d4f7a01d3 keyB -> " + p.enc("1e8bdf7d4f7a01d3", kzm)); + System.out.println("[C2] flag+15 in=\\u0020+0123456789abcde keyB -> " + p.enc("\u00200123456789abcde", kzm)); + System.out.println("[C3] hexbytes8pad in=1e8bdf7d4f7a01d3+0000000000000000 keyB -> " + p.enc("1e8bdf7d4f7a01d3\0\0\0\0\0\0\0\0", kzm)); + System.out.println("[X0] in=0123456789abcdef keyB -> " + p.enc("0123456789abcdef", kzm)); + + for (int i = 0; i < 8; i++) { + long seed = 1000 + i; + String in = hexBytes(16, seed); + String key = hexBytes(64, seed ^ 0x5a5a); + System.out.println("[R" + i + "] in=" + in + " key=" + key + " -> " + p.enc(in, key)); + } + // 用 64 个 'A' 测试纯 64B ASCII 钥 + System.out.println("[KAA] in=0123456789abcdef key='A'*64 -> " + p.enc("0123456789abcdef", "A".repeat(64))); + // 24B 短钥 (short string, flag 参与装载?) + System.out.println("[K24] in=0123456789abcdef key=0123456789abcdef(16B short) -> " + p.enc("0123456789abcdef", "0123456789abcdef")); + } +} \ No newline at end of file