PAYLOAD 二进制视图

图:算法输入 PAYLOAD 的部分十六进制转储。

本文是 yb-u-r-results-algorithm.md 的运算展开版。前一文保存完整输入字节、RSA 模数、捕获的 221 字节 PS 及主要 trace 行号;本文按数据流展示具体操作次序、使用的公式和本次可手算核对的中间值。trace 没有给出的上游业务含义仍不作推断。

所有数组下标从 0 开始。|| 表示字节拼接,XOR 表示按位异或,LE32 / BE32 表示 32 位小端 / 大端编码。数学上的模 2^32 加法按 32 位无符号数回绕。

0. 数据依赖

ASCII("1")、ASCII(key)、ASCII(params)、ASCII(time)、ASCII(nonce)
    -> S -> MD5 -> H
    -> 第二次 clock_gettime -> u -> ChaCha20 -> CHACHA16
    -> F、P、K、M -> MD5(P||K) -> D -> X -> R
    -> 26-byte body || 8 个 00 -> 小写 hex -> sign

PAYLOAD -> pad -> 内层 AES-CBC -> 5-byte frame header
        -> pad -> 外层 AES-CBC(A32[0:16], A32[16:32])
        -> Base64 -> params

PS、A32、RSA 公钥(n,e) -> EM -> RSA_C -> Base64 -> key
fd 3 nonce 输入的前 8 字节 -> 小写 hex -> nonce
第一次 clock_gettime -> 十进制毫秒 ASCII -> time
ASCII("1")、appKey -> JSON 文本和转义 -> crypt、extra

1. crypt 与 extra

1.1 crypt

  1. 顶层 crypt 片段由 memmove 从 0xe4ffdac1 复制 13 字节;copy 参数和结果见 trace29.log:8130328-8130359。其中 JSON 冒号和引号之间的值字节是 31,即 ASCII 1。8130000-8130029 的 X6=0x31 参数属于 extra 的内层对象构造,不是这里的顶层片段。
  2. 把捕获到的 13 字节按固定前缀、单字节值、后缀拆开:

    7b 22 63 72 79 70 74 22 3a 22 || 31 || 22 2c
    {  "  c  r  y  p  t  "  :  "     1    "  ,
    
  3. 结果长 10+1+2=13 字节,即 {"crypt":"1",。缓冲区转储和长度见 8130328-8130359,随后它被并入外层对象,见 8155999-8156008。

1.2 extra

  1. 内层构造调用把 X6=0x31 作为 ASCII crypt 值,并从 0x1220e000 指向的数据取 appKey;调用参数见 trace29.log:8130000-8130029。appKey 的 16 字节 0AND05U9F09CE1CW 及复制见 411345-411352。
  2. 把 appKey 与 ASCII 1 放进已捕获的紧凑文本:

    E = {"appKey":"0AND05U9F09CE1CW","crypt":"1"}
    

    E 长 41 字节。0x31 参数、appKey 输入和紧凑文本相关构造见 8130000-8130060。

  3. 作为外层 JSON 的字符串值时,对 E 的每一个双引号前插入一个反斜杠。E 中有 8 个双引号,所以转义内容长度为 41+8=49 字节:

    {\"appKey\":\"0AND05U9F09CE1CW\",\"crypt\":\"1\"}
    

    49 字节内容转储见 8130040-8130057。

  4. 外层字段依次追加 9 字节 "extra":"、上述 49 字节和 1 字节结束引号,总计 9+49+1=59 字节。完整片段见 8130347-8130359。JSON 解析后去掉的只是转义层,extra 字符串值仍为 41 字节的 E。

2. key

2.1 组装 RSA 编码消息

  1. RSA 后缀 A32 为 32 字节:

    A32[0:16]  = 03 00 00 f3 重复 4 次
    A32[16:32] = 04 00 00 f4 重复 4 次
    

    两个 16 字节区段的暂存和转储见 trace29.log:4039997-4040002、4040044-4040049;复制进填充缓冲区见 4161069-4161079。相同 A32 稍后被用作外层 AES 的 key 与 IV。

  2. 取 270 字节 DER 公钥,解析出 256 字节模数 n 和指数 e=65537=0x10001;DER 与寄存器解析见 4088824-4088825、4152370-4152414。完整 n 字节见 yb-u-r-results-algorithm.md 的 key 小节。
  3. 填充缓冲区起始两个字节写 00 02。随后依次调用 rand(),令候选字节 b = return_value & 0xff:若 b=00,丢弃并重试;否则把 b 写入 PS 并将写指针加一。重复至 221 个非零字节。分支和写入见 4157735-4157768、4157790-4161058。本次完整 PS 列表见前一文的 key 小节,最后一个字节为 0d。
  4. 在 PS 后写一个分隔字节 00,再复制 A32。布局及长度为:

    EM = 00 02 || PS[221] || 00 || A32[32]
    len(EM) = 2 + 221 + 1 + 32 = 256
    

    分隔零位于缓冲区索引 0xdf,A32 从索引 0xe0 开始,见 4161068-4161079。

2.2 公钥幂模与 256 字节结果

  1. 将 EM 按大端解释成整数 m=OS2IP(EM)。运算为 c=m^e mod n。由于 e=65537=2^16+1,一种等价的重复平方过程是:

    q0 = m mod n
    q1 = q0*q0 mod n
    q2 = q1*q1 mod n
    ...
    q16 = q15*q15 mod n
    c = q16*m mod n
    

    每一步都在模 n 下约简。trace 中的大整数运算区间为 4498101-4498150、7100000-7183490。

  2. 把 c 用大端补零到 256 字节:RSA_C=I2OSP(c,256)。trace 中大整数输出的内部字节序先做反序搬移,形成该 256 字节大端序列后再送入 Base64;搬移见 7183460-7183490、7186300-7186490。

2.3 Base64 转为字符串

  1. 每 3 字节 b0,b1,b2 组成 24 位数,切成 4 个 6 位索引:

    i0 = b0 >> 2
    i1 = ((b0 & 0x03) << 4) | (b1 >> 4)
    i2 = ((b1 & 0x0f) << 2) | (b2 >> 6)
    i3 = b2 & 0x3f
    
  2. 本次首组三字节是 53 3e 4e:

    i0 = 0x53 >> 2                         = 0x14
    i1 = ((0x53 & 0x03) << 4) | (0x3e >> 4) = 0x33
    i2 = ((0x3e & 0x0f) << 2) | (0x4e >> 6) = 0x39
    i3 = 0x4e & 0x3f                       = 0x0e
    

    以上为十六进制索引 0x14,0x33,0x39,0x0e,分别查 Base64 字符表得到 U,z,5,O。编码器读写和表访问见 7186723、7186790、7186859、7186925;输出以 Uz5O 开始,见 7214231-7214244。

  3. 对 256 字节输入,256=3*85+1。最后一组只有 1 个数据字节,编码器输出两个有效字符并补 ==,正文 342 字符,总长 344 字符。长度及 == 写入见 7214231-7214289;完整字段值见 8053222-8053229。

3. nonce

  1. fd 3 把 32 字节读入 0x125643a0;调用和返回长度见 trace29.log:8051612-1619。这次算法只使用缓冲区前 8 字节。
  2. 首字节 load 取得 read[0]=05。接着两个未对齐 32 位 load 取得:

    read[1:5] = 00 00 f5 05
    read[4:8] = 05 00 00 f5
    

    读取地址和寄存器值见 8051725-1733。

  3. 写入暂存区时,单字节 05 放在有效数据偏移 0,第一组 4 字节放在偏移 1 至 4,第二组放在偏移 4 至 7。两组在偏移 4 重叠,值都是 05,故最终八字节仍为:

    05 || 00 00 f5 05 || 00 00 f5
    = 05 00 00 f5 05 00 00 f5
    

    load/store 和暂存缓冲区转储见 8051745-1774。

  4. 对每个输入字节 b,先查 hex_table[b >> 4],再查 hex_table[b & 0x0f]。例如 0x05 -> '0' || '5',0xf5 -> 'f' || '5'。八个字节得到小写 ASCII 050000f5050000f5,转换表和缓冲区见 8054185-4207。该 16 字节文本被复制为 nonce 字段,见 8157235-7237。

4. params

4.1 内层 AES-128-CBC

  1. 输入 PAYLOAD 是 trace 中完整捕获的 2504 字节;MD5 为 c380a76de14498edb0806eb394c4ca06,完整转储见 trace29.log:3474891-3475050。算法把捕获字节直接作为明文,不先改变其中的 ASCII 或二进制字段。
  2. 内层 AES key 是 ff802003f67f55be8b535a97b01b459d,IV 是 b8221969c27a8019d533b139ade71bcc。查找参数和实际字节见 3474100-3474605、3475647-3476576。
  3. PKCS#7 以 16 为块长。计算 p=16-(2504 mod 16)=8,于是末尾补 8 个 08:

    INNER_PLAIN = PAYLOAD || 08 08 08 08 08 08 08 08
    len(INNER_PLAIN) = 2512 = 157*16
    

    填充字节的写入见 3477250、3477390-3477419。

  4. AES-128 把 16 字节 key 切成四个 32 位字 w[0..3],再扩展为 w[0..43],每四个字组成一个 16 字节轮密钥,共 11 个。对 i=4..43:

    temp = w[i-1]
    if i mod 4 == 0:
        temp = SubWord(RotWord(temp)) XOR Rcon[i/4]
    w[i] = w[i-4] XOR temp
    

    RotWord 循环左移一个 byte,SubWord 对四个 byte 分别查 AES S-box。trace 中的 key 输入和扩展循环见 3475647-3476552。

  5. CBC 先做明文块与 IV 的异或,再送入 AES:C1=AES_K(P1 XOR IV);后续块为 Ci=AES_K(Pi XOR C(i-1))。每个 AES-128 block 的轮步骤是:初始 AddRoundKey;第 1 至 9 轮依次 SubBytes、ShiftRows、MixColumns、AddRoundKey;第 10 轮执行 SubBytes、ShiftRows、AddRoundKey,不执行 MixColumns。ShiftRows 将状态的第 0、1、2、3 行分别循环左移 0、1、2、3 字节;MixColumns 对每列在 GF(2^8) 中乘以 AES 矩阵 [02 03 01 01; 01 02 03 01; 01 01 02 03; 03 01 01 02],域多项式为 0x11b。trace 中首块 CBC 异或、key expansion 和后续块链式异或见 3477507-3477511、3475647-3476552、3481057-3481069。
  6. 首块可以直接复算。PAYLOAD 前 16 字节和 IV 分别为:

    P1 = 0a 79 08 01 12 1d 31 37 39 30 34 33 38 36 35 39
    IV = b8 22 19 69 c2 7a 80 19 d5 33 b1 39 ad e7 1b cc
    
    P1 XOR IV
       = b2 5b 11 68 d0 67 b1 2e ec 03 85 0a 95 d1 2e f5
    
    C1 = AES_K(P1 XOR IV)
       = 98 26 a6 b8 ca b8 ed 3a 4f ab 85 18 f7 ce fa f3
    

    第一字节 0a XOR b8 = b2 的 trace 证据见 3477507-7511;整块输出见 4036840-6848。

4.2 帧封装、外层 AES-128-CBC

  1. 将 5 字节头 03 00 00 00 01 前置于 2512 字节内层密文:

    FRAME = 03 00 00 00 01 || INNER_CIPHER
    len(FRAME) = 5 + 2512 = 2517
    

    头部写入见 4036827-6832,合并长度见 4036840-6848、4037015-7018。

  2. 外层 key 取 A32[0:16],即 03 00 00 f3 重复四次;外层 IV 取 A32[16:32],即 04 00 00 f4 重复四次。A32 的字节见 4161069-1079;key schedule 和 IV 载入见 7216359-6382、7217283-7289。
  3. 2517 mod 16=5,所以 PKCS#7 补 16-5=11 个 0b:

    OUTER_PLAIN = FRAME || 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b
    len(OUTER_PLAIN) = 2517 + 11 = 2528 = 158*16
    

    余数及填充值计算见 7217283-7292,填充写入见 7217351-7432、7217744-8401。

  4. 外层第一 CBC 块与 IV 异或。它由帧头和内层密文首 11 字节组成:

    OUTER_PLAIN[0:16] = 03 00 00 00 01 98 26 a6 b8 ca b8 ed 3a 4f ab 85
    OUTER_IV[0:16]    = 04 00 00 f4 04 00 00 f4 04 00 00 f4 04 00 00 f4
    XOR               = 07 00 00 f4 05 98 26 52 bc ca b8 19 3e 4f ab 71
    

    对该结果执行外层 AES-128,得到首密文块 c6 6b 0a f2 fe 63 87 7d 7f e3 c0 d3 93 66 79 0a,见 7781193-1195、7782326-2332。之后各块使用前一密文块,见 7222007-2023。

4.3 Base64

  1. Base64 输入是 2528 字节 OUTER_CIPHER。按每 3 字节切组,分别用 key 小节列出的 4 个 6 位索引公式查 64 字符表。
  2. 首组 c6 6b 0a 的索引逐项为:

    c6 >> 2                         = 31
    ((c6 & 03) << 4) | (6b >> 4)   = 26
    ((6b & 0f) << 2) | (0a >> 6)   = 2c
    0a & 3f                         = 0a
    

    索引 31,26,2c,0a 查表得到 xmsK,与 params 首四个字符相同。读入密文字节和编码写入见 7782993-7783231。

  3. 2528=3*842+2,最后组只有两个输入字节,因此最后输出三个字符和一个 =。总字符数为 4*ceil(2528/3)=3372。末尾填充和完整 Base64 文本见 7782331-2333、8053454-3461、8053671。

5. time

  1. 第一次 clock_gettime(0) 返回 tv_sec=0x6ab7ed14=1790438676 和 tv_nsec=0x23957f40=597000000,调用与读数见 trace29.log:8052788-2812。
  2. 先把纳秒截断为微秒:

    tv_nsec / 1000 = 597000 微秒
    U = tv_sec*1,000,000 + 597000
      = 1,790,438,676,597,000 微秒
    

    trace 里的中间寄存器值为 0x65c64fd4f8908,运算见 8052795-2810。

  3. 再将微秒总数截断除以 1000 得毫秒:floor(U/1000)=1790438676597。等价地,tv_sec*1000 + floor(tv_nsec/1,000,000) 得到同一值。该整数被转成 13 个十进制 ASCII 字节,调用参数和输出见 8052850-2931、8054091-4093。

6. sign

6.1 首次 MD5 输入与 H

  1. 在字段缓冲区按顺序拷贝原始 ASCII,不先 Base64 解码,也不做 JSON 转义:

    S = ASCII("1") || ASCII(key) || ASCII(params)
        || ASCII(time) || ASCII(nonce)
    
  2. 字节数依次为 1,344,3372,13,16,故 len(S)=3746。首部、各字段复制和 MD5 标记见 trace29.log:8054285-4290、8053222-3229、8053454-3461、8054091-4093、8054200-4207。首字节是 31;末尾 ASCII 是 1790438676597,接 nonce 050000f5050000f5,见 8054287-4289、8054519-4521。
  3. MD5 预处理把消息 bit 长度作为小端 64 位数追加。此处 3746*8=29968=0x7510;3746 mod 64=34,先追加 80,再追加 21 个 00,最后追加 LE64(29968)=10 75 00 00 00 00 00 00,得到 59 个 64 字节块。
  4. 每块按 16 个小端 32 位字 W[0..15] 读取;MD5 状态初始化为 A=0x67452301, B=0xefcdab89, C=0x98badcfe, D=0x10325476。每块执行 64 步,按轮号选择函数与字索引:

    0..15:  f=(B&C)|(~B&D)      g=i
    16..31: f=(D&B)|(~D&C)      g=(5*i+1) mod 16
    32..47: f=B XOR C XOR D    g=(3*i+5) mod 16
    48..63: f=C XOR (B|~D)      g=(7*i) mod 16
    
    temp = (A + f + AC[i] + W[g]) mod 2^32
    (A,B,C,D) = (D, (B + ROTL32(temp,s[i])) mod 2^32, B, C)
    

    AC[i]=floor(2^32*abs(sin(i+1))),移位表依次为 [7,12,17,22] 重复 4 次、[5,9,14,20] 重复 4 次、[4,11,16,23] 重复 4 次、[6,10,15,21] 重复 4 次。64 步后把工作状态加回块入口状态;所有 32 位加法均模 2^32。最终 A、B、C、D 各按小端输出为 16 字节 H。trace 对本次 3746 字节输入记录的 MD5 为 H=c04762cdd7f0992a8725c1ecc2399bcd,见 8054285。

6.2 第二次时钟与 ChaCha20

  1. 第二次时钟样本给出 tv_nsec=0x240f9140=605000000。取本秒微秒部分 u=floor(tv_nsec/1000) mod 1,000,000=605000=0x00093b48,见 8054834-4856、8097900-7954。
  2. 初始化 ChaCha20 64 字节状态,按 little-endian 32 位 word 顺序:

    constants = ASCII("expand 32-byte k")
    key       = LE32(u) || 00*28 = 48 3b 09 00 || 00*28
    counter   = 00*8
    nonce     = a3 a2 44 ef 7f e5 a4 65
    

    常量、key、counter、nonce 的装载见 8097930-7935、8099250-9350。nonce 按两个 LE32 word 读入,即 ef44a2a3、65a4e57f。

  3. 每个 ChaCha quarter-round 对四个 32 位 word (a,b,c,d) 依序做:

    a = a+b; d = ROTL32(d XOR a,16)
    c = c+d; b = ROTL32(b XOR c,12)
    a = a+b; d = ROTL32(d XOR a, 8)
    c = c+d; b = ROTL32(b XOR c, 7)
    

    一轮依次处理列 quarter-round 和对角线 quarter-round;共 10 个 double-round,即 20 轮。指令中的 add、xor 和 rotate 见 8099500-8100430。最后把轮后状态与初始状态逐 word 相加,并以 little-endian 写回 64 字节块。

  4. 本次 ChaCha 块前 16 字节为 CHACHA16=a60ac71c5c9da46356884f470f4de6b0。逐字节与 H 异或:

    H         = c0 47 62 cd d7 f0 99 2a 87 25 c1 ec c2 39 9b cd
    CHACHA16  = a6 0a c7 1c 5c 9d a4 63 56 88 4f 47 0f 4d e6 b0
    F         = 66 4d a5 d1 8b 6d 3d 49 d1 ad 8e ab cd 74 7d 7d
    

    向量异或指令和 F 的写入见 8100410-8100450。

6.3 生成 P、K、M 和第二次 MD5

  1. 32 字节常量 raw 为 36474030333e36313b4d4c4f3e3e3f5227502a512d26212d212f5c5e282a2810,见 8100524-0617。对 i=0..31 逐字节计算:

    ascii[i] = raw[i] XOR (i+1) XOR 0x21 XOR 0x20
    

    例如:

    i=0: 0x36 XOR 0x01 XOR 0x21 XOR 0x20 = 0x36  -> ASCII '6'
    i=1: 0x47 XOR 0x02 XOR 0x21 XOR 0x20 = 0x44  -> ASCII 'D'
    i=2: 0x40 XOR 0x03 XOR 0x21 XOR 0x20 = 0x42  -> ASCII 'B'
    

    全部结果是 ASCII 6DB579083FFB211C7C8D917494FC4561。每两字符作为一个 hex byte 解码,得到 P=6db579083ffb211c7c8d917494fc4561。循环、两字符解析及 P 写入见 8100631-8101431、8101439-8101446、8101608-8103934。

  2. K 从模板 28 00 47 2b 00 2c 4c 4b 开始,用 F[5]=6d 覆盖偏移 1,用 H[4]=d7 覆盖偏移 4:

    K = 28 || 6d || 47 2b || d7 || 2c 4c 4b
      = 28 6d 47 2b d7 2c 4c 4b
    

    读值和写入见 8100505-0514、8103780-3947。

  3. M 按 16 字节位置拼装:

    M = H[0:4] || 00*4 || F[1:5] || LE32(0x12345678)
      = c0 47 62 cd || 00 00 00 00 || 4d a5 d1 8b || 78 56 34 12
      = c04762cd000000004da5d18b78563412
    

    来源切片和常量写入见 8104220-4259。

  4. 第二次 MD5 输入是 16 字节 P 后接 8 字节 K:

    P||K = 6d b5 79 08 3f fb 21 1c 7c 8d 91 74 94 fc 45 61
           28 6d 47 2b d7 2c 4c 4b
    len(P||K)=24 bytes; bit_length=192=0xc0
    

    MD5 预处理为 P||K || 80 || 00*31 || LE64(192),其中 LE64(192)=c0 00 00 00 00 00 00 00,恰好构成一个 64 字节块。24 字节拼接及 MD5 长度见 8104001-4043;digest 为 D=72c8328262f0171c0a2971766aa59388,见 8105152-5170。

6.4 两层异或和最终 hex

  1. 将八字节 K 循环两次,与 D 异或:

    D             = 72 c8 32 82 62 f0 17 1c 0a 29 71 76 6a a5 93 88
    K repeated    = 28 6d 47 2b d7 2c 4c 4b 28 6d 47 2b d7 2c 4c 4b
    X = D XOR K   = 5a a5 75 a9 b5 dc 5b 57 22 44 36 5d bd 89 df c3
    

    i mod 8 索引循环及异或指令见 8105152-5550。

  2. X 与 M 逐字节异或:

    X             = 5a a5 75 a9 b5 dc 5b 57 22 44 36 5d bd 89 df c3
    M             = c0 47 62 cd 00 00 00 00 4d a5 d1 8b 78 56 34 12
    R = X XOR M   = 9a e2 17 64 b5 dc 5b 57 6f e1 e7 d6 c5 df eb d1
    
  3. 26 字节正文由以下部分按次序组成:LE32(0x1085434c)=4c438510、H[4]=d7、F[5]=6d、BE32(u)=00093b48、16 字节 R。总长 4+1+1+4+16=26。缓冲区各部分写入见 8105360-5552。
  4. 正文后追加 8 个零字节,成为 34 字节。每字节拆为高、低半字节,分别查小写 hex 表,因此 34 字节生成 68 个 ASCII 字符。补零、hex 转换循环见 8105559-5564、8105652-8110048;最终文本为:

    4c438510d76d00093b489ae21764b5dc5b576fe1e7d6c5dfebd10000000000000000
    

7. JSON 序列化

  1. 字段按 crypt, extra, key, nonce, params, sign, time 顺序写入对象,相关拼接片段见 8155999-8156017、8157235-7237、8401235-1481、8401491-1498、8402458-2462。
  2. extra 是一个 JSON 文本字符串,所以内层 8 个引号各用 \" 表示;解析外层 JSON 时反斜杠被解码掉,字符串内容回到 41 字节 E。
  3. key 和 params 的 Base64 中有 /。最终 JSON 文本把它们输出为 \/,JSON 解析后仍是 /。这类 JSON 转义发生在字符串算法之后,不参加 RSA、AES 或 MD5 输入。字段片段和转义后的请求 JSON 见 8156017-6024、8398846、8398861、8401265。

8. 复核边界

完整 PAYLOAD、RSA 模数和 PS 原始记录、各最终字段文本及逐字段长度见 yb-u-r-results-algorithm.md。本文展开算法转换与可手算的中间块;对 trace 未标明的路径字符串、随机数 seed、PAYLOAD 字段 schema 和业务语义不作推断。


Trace 文件搜索工具:本文分析使用的工具是 https://github.com/AndroidReverser-Test/trace_search。