From f121f41f6186a21b53f90658fa5791060e96272b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 29 Aug 2026 02:25:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(huya):=20getOtp=E7=9B=B4=E8=B0=83=E5=85=A8?= =?UTF-8?q?=E9=80=9A(C2=E5=88=9D=E5=A7=8B=E5=8C=96+AppLoginData=E4=BC=AA?= =?UTF-8?q?=E9=80=A0)=20-=20OTP=E7=9C=9F=E5=B8=83=E5=B1=80=E5=AE=9A?= =?UTF-8?q?=E6=A1=88+=E5=A0=8632hex=E6=96=B0=E5=80=99=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/HUYA_HDID_ALGORITHM_GEN.md | 11 ++ tools/unidbg/hydev/src/hydev/AesProbe.java | 115 ++++++++++++++++++++- 2 files changed, 124 insertions(+), 2 deletions(-) diff --git a/docs/HUYA_HDID_ALGORITHM_GEN.md b/docs/HUYA_HDID_ALGORITHM_GEN.md index aaef40a..3855918 100644 --- a/docs/HUYA_HDID_ALGORITHM_GEN.md +++ b/docs/HUYA_HDID_ALGORITHM_GEN.md @@ -806,3 +806,14 @@ hypasswordLogin/MsgLoginReq/LogLoginReq → mid 各异, tail 恒定 8b38f1 → a - AppLoginData 字段布局: +0x40/+0x10/+0x8 (多 string), 全局材料=0x484000+0xa0 ### OTP tail = f(c,d) 差分 (推翻"固定"结论) + 真实布局空输出 (参数微差待调) ### 下轮 (R8): getInstance→getOtp(AppLoginData) 直调, 金样本值注满结构 → 输出 vs ed0db8 + +## §11.31 getOtp 直调成功 (R8) - C2 全链路在 emulator 内跑通 +### 实验定案 +- BusinessCfg::getInstance@0x281270 直调 ✓ → instance=0x12491540 (C2 真初始化!) +- BusinessCfg::getOtp(AppLoginData&)@0x26916c 直调 ✓ 无崩溃 (此路可行!) +- AppLoginData 伪造: std::string 双形态 (SSO<23 内联 / heap: len<<1|1 + [8]ptr + [16]cap) ✓ +- OTP 真布局 (269324): x0=to_string(serviceTime), x1=2, x2=counter(w25=0首调), x3/x4=AppLoginData+0x40/+0x10, + x5=全局串, x6=4, x7=nonce-scalar, [sp]=OUT& (nonce 不入 mid!) +- mid = f(in, cnt, s3, s4, s5); OTP 输出=[h6][00][16B mid][s5 加密块...] +### 新候选: 堆 0x127d4b90 = 275d0ff676c0d65114acdefbd2ad87a3 (32hex! 来源待查) +### R9: ① hook 269328 (post-OTP, X29 读 [x29-0x40] = OUT!) ② 0x123M-0x130M 扫窗 before/after 差分 diff --git a/tools/unidbg/hydev/src/hydev/AesProbe.java b/tools/unidbg/hydev/src/hydev/AesProbe.java index 508ab95..4f06d9d 100644 --- a/tools/unidbg/hydev/src/hydev/AesProbe.java +++ b/tools/unidbg/hydev/src/hydev/AesProbe.java @@ -145,11 +145,11 @@ public class AesProbe { UnidbgPointer pout = writeStdString(""); UnidbgPointer pm = writeStdString("mstack"); try { - // 真实布局: (x0=in, x1=2, x2=counter, x3, x4, x5, x6=4, x7=out, [sp]=m) + // 真实布局: (x0=in, x1=2, x2=counter, x3, x4, x5, x6=4, x7=nonce-scalar, [sp]=OUT&) module.callFunction(emulator, 0x32fa24L, UnidbgPointer.nativeValue(pa), 2L, counter, UnidbgPointer.nativeValue(ps3), UnidbgPointer.nativeValue(ps4), UnidbgPointer.nativeValue(ps5), - 4L, UnidbgPointer.nativeValue(pout), UnidbgPointer.nativeValue(pm)); + 4L, 0x12345678L, UnidbgPointer.nativeValue(pout)); String out = readStdString(pout); String shortIn = in.length() > 20 ? in.substring(0, 20) : in; String mid = out.length() > 36 ? out.substring(4, 36) : out; @@ -218,6 +218,105 @@ public class AesProbe { return ""; } + void callGetOtp() { + try { + // BusinessCfg::getInstance @0x281270 + Number inst = module.callFunction(emulator, 0x281270L); + long thisPtr = inst.longValue(); + System.out.println("[getOtp] instance=0x" + Long.toHexString(thisPtr)); + if (thisPtr == 0) { System.out.println("[getOtp] instance null"); return; } + // 伪造 AppLoginData: 4 个 string 槽 (24B SSO each) @ +0x8 +0x20 +0x38 +0x50 + com.github.unidbg.memory.MemoryBlock mb = emulator.getMemory().malloc(0x1000, false); + UnidbgPointer pv = mb.getPointer(); + pv.write(0, new byte[0x1000], 0, 0x1000); + writeStrAt(pv, 0x8, "hy_300023887"); + writeStrAt(pv, 0x20, "772ed992b0e161276f44ec63671e60155c506294"); + writeStrAt(pv, 0x38, "1e8bdf7d4f7a01d3"); + writeStrAt(pv, 0x50, "*hZrPb62GskrEeTYcLeUTL1fJ1OR8ky3x9qXQq0s6ICJV5v9T"); + // 调用 getOtp(AppLoginData&) @0x26916c + module.callFunction(emulator, 0x26916cL, thisPtr, UnidbgPointer.nativeValue(pv)); + byte[] rb = new byte[0x60]; + pv.read(0, rb, 0, rb.length); + StringBuilder sb2 = new StringBuilder(); + for (byte bb : rb) sb2.append(String.format("%02x", bb)); + System.out.println("[getOtp] call ok, struct: " + sb2); + System.out.println("[getOtp] done, scanning heap for outputs..."); + byte[] win = new byte[0x10000]; + int hits2 = 0; + for (long baseA = 0x12300000L; baseA < 0x13000000L && hits2 < 40; baseA += 0x10000L) { + UnidbgPointer hp2 = UnidbgPointer.pointer(emulator, baseA); + if (hp2 == null) continue; + try { hp2.read(0, win, 0, win.length); } catch (Throwable t) { continue; } + StringBuilder cur = new StringBuilder(); + int st = -1; + for (int i = 0; i < win.length; i++) { + int b = win[i] & 0xff; + if ((b >= 0x20 && b < 0x7f)) { if (st < 0) st = i; cur.append((char) b); } + else { + if (cur.length() >= 30 && cur.chars().allMatch(c -> "0123456789abcdef".indexOf(Character.toLowerCase(c)) >= 0 || "hZrPb62GskrEeTYcLeUTL1fJ1OR8ky3x9qXQq0s6ICJV5v9T".indexOf(c) >= 0)) { + System.out.println("[getOtp-heap] @" + String.format("0x%x", baseA + st) + " = " + cur); + hits2++; + } + cur.setLength(0); st = -1; + } + } + } + System.out.println("[getOtp] heap-scan done hits=" + hits2); + } catch (Throwable t) { + System.out.println("[getOtp] err: " + t); + } + } + + void writeStrAt(UnidbgPointer p, long off, String s) { + byte[] b = s.getBytes(java.nio.charset.StandardCharsets.UTF_8); + int n = b.length; + if (n < 23) { + p.write(off, new byte[]{(byte)(n << 1)}, 0, 1); + p.write(off + 1, b, 0, n); + p.write(off + 1 + n, new byte[]{0}, 0, 1); + } else { + // 真正堆串: [0]=len<<1|1 [8]=ptr [16]=cap + long cap = (n + 15) & ~15L; + com.github.unidbg.memory.MemoryBlock mb2 = emulator.getMemory().malloc((int)(cap + 8), false); + UnidbgPointer hp = mb2.getPointer(); + hp.write(0, b, 0, n); + hp.write(n, new byte[]{0}, 0, 1); + byte[] b8 = new byte[8]; + long v = com.github.unidbg.pointer.UnidbgPointer.nativeValue(hp); + for (int k = 0; k < 8; k++) b8[k] = (byte)(v >>> (8 * k)); + p.write(off, new byte[]{(byte)(n << 1 | 1)}, 0, 1); + p.write(off + 8, b8, 0, 8); + for (int k = 0; k < 8; k++) b8[k] = (byte)(cap >>> (8 * k)); + p.write(off + 16, b8, 0, 8); + } + } + + void scanUdbGlobals() { + long base = module.base; + byte[] buf = new byte[0x12000]; + int hits = 0; + for (int win = 0; win < 8 && hits < 20; win++) { + long addr = base + 0x470000L + win * 0x10000L; + UnidbgPointer pp = UnidbgPointer.pointer(emulator, addr); + if (pp == null) continue; + try { pp.read(0, buf, 0, buf.length); } catch (Throwable t) { continue; } + StringBuilder cur = new StringBuilder(); + int start = -1; + for (int i = 0; i < buf.length; i++) { + int b = buf[i] & 0xff; + if (b >= 0x20 && b < 0x7f) { if (start < 0) start = i; cur.append((char) b); } + else { + if (cur.length() >= 10) { + System.out.println("[globals] @" + String.format("0x%x", addr + start) + " = " + cur); + hits++; + } + cur.setLength(0); start = -1; + } + } + } + System.out.println("[globals] done hits=" + hits); + } + void dumpRegion(String tag, long addr, long size) { UnidbgPointer pp = UnidbgPointer.pointer(emulator, addr); if (pp == null) { System.out.println("[dump] " + tag + " null"); return; } @@ -396,6 +495,7 @@ public class AesProbe { public static void main(String[] args) throws Exception { String so = args.length > 0 ? args[0] : "so/libudbauthunify_merged.so"; AesProbe p = new AesProbe(so); + p.callGetOtp(); // 基准: 已知 16B 明文 + 24B 钥 // 64B 钥材料测试 (KeyExpansion 以 16B 步长读!!) String k64_zmhav = "ZMHAVPRaxJ3MtXDjduUnXAKQ" + "\0".repeat(0); @@ -469,6 +569,17 @@ public class AesProbe { p.callOtpReal("0", 0L, "hy_300023887", "772ed992b0e161276f44ec63671e60155c506294", "1e8bdf7d4f7a01d3", 1L); p.callOtpReal("3251699", 0L, "hy_300023887", "772ed992b0e161276f44ec63671e60155c506294", "7c5387e0539c023c31c4ff0e807e7256117385ee", 1L); p.callOtpReal("3251699", 0L, "hy_300023887", "772ed992b0e161276f44ec63671e60155c506294", "13.4.22", 1L); + p.callOtpReal("3251699", 0L, "hy_300023887", "772ed992b0e161276f44ec63671e60155c506294", "13.4.22", 0x10101010L); + // SDID 作 s5 (36B 证书族) + p.callOtpReal("3251699", 0L, "hy_300023887", "772ed992b0e161276f44ec63671e60155c506294", "hZrPb62GskrEeTYcLeUTL1fJ1OR8ky3x9qXQq0s6ICJV5v9T", 0x12345678L); + p.callOtpReal("3251699", 0L, "hy_300023887", "772ed992b0e161276f44ec63671e60155c506294", "*hZrPb62GskrEeTYcLeUTL1fJ1OR8ky3x9qXQq0s6ICJV5v9T", 0x12345678L); + p.callOtpReal("", 0L, "", "", "*hZrPb62GskrEeTYcLeUTL1fJ1OR8ky3x9qXQq0s6ICJV5v9T", 0x12345678L); + p.callOtpReal("1", 1L, "hy_300023887", "772ed992b0e161276f44ec63671e60155c506294", "*hZrPb62GskrEeTYcLeUTL1fJ1OR8ky3x9qXQq0s6ICJV5v9T", 0x12345678L); + // nonce=0 + 2024-08-24 时间戳 era + SDID 族 + p.callOtpReal("1724464550000", 0L, "hy_300023887", "772ed992b0e161276f44ec63671e60155c506294", "*hZrPb62GskrEeTYcLeUTL1fJ1OR8ky3x9qXQq0s6ICJV5v9T", 0L); + p.callOtpReal("1724464550", 0L, "hy_300023887", "772ed992b0e161276f44ec63671e60155c506294", "*hZrPb62GskrEeTYcLeUTL1fJ1OR8ky3x9qXQq0s6ICJV5v9T", 0L); + p.callOtpReal("1724464550000", 0L, "hy_300023887", "772ed992b0e161276f44ec63671e60155c506294", "hZrPb62GskrEeTYcLeUTL1fJ1OR8ky3x9qXQq0s6ICJV5v9T", 0L); + p.callOtpReal("1724464550000", 0L, "hy_300023887", "772ed992b0e161276f44ec63671e60155c506294", "1e8bdf7d4f7a01d3", 0L); p.callOtp("LogLoginReq", "ZMHAVPRaxJ3MtXDjduUnXAKQ"); // SDID 块直解 (36B 证书的两个16B块 × 全部钥) p.callDecodeAes("859acf6fad86b24ac479361c2de5132f", "ZMHAVPRaxJ3MtXDjduUnXAKQ");