白嫖的动力是无限的。

前言

本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。

工具

  1. Fiddler
  2. JADX
  3. IDA
  4. Frida

抓包

签到包内容

重放测试

使用Shift + R进行一个包的重放。

重放测试结果

好家伙,那么改一下_time参数试试。

重放测试结果

嗯,看来有签名校验,经过测试后确定是hkey这个参数。

JADX

打开jadx并把下载好的.apk文件拖入软件中,等待分析完成。分析完成之后直接打开搜索窗口,输入关键字hkey进行搜索。

搜索结果

发现没有可疑的类,那就用请求路径搜索试试。

搜索结果

嗯,找到目标了,但发现这是一个接口,没有直接进行定义。那么就按下x键跟踪到引用那边。

跟踪结果

到了这里之后基本就没事,继续往下跟就行,过程省略…

最终结果

最终到了这么一个地方,其中NDKTools.encode就是生成hkey的函数。但是在这里并没有看到hkey这个字段,经过了一番检查之后发现是这么生成的。

name = "hey".replace("e", "ke")

好,那就没问题了,再来看NDKTools.encode函数,发现其进入了so中。好家伙,那这层就算完了。

IDA

找函数的那些过程就省略了,这里直接上伪代码分析结果。

v11 = (unsigned __int8 *)((__int64 (__fastcall *)(JNIEnv *, __int64, _QWORD))(*v10)->GetStringUTFChars)(v10, v9, 0LL);// 请求的API路径
v12 = ((__int64 (__fastcall *)(JNIEnv *, __int64, _QWORD))(*v10)->GetStringUTFChars)(v10, v8, 0LL);// 当前时间(1649327297)
v13 = (unsigned __int8 *)((__int64 (__fastcall *)(JNIEnv *, __int64, _QWORD))(*v10)->GetStringUTFChars)(v10, v7, 0LL);// 随机生成的字符串([\da-zA-Z]{32,32})
result = 0LL;
// 判断3个参数是否为空
if ( v11 && v12 && v13 )
{
    // 参数不为空
    v93 = 22872;
    v91 = xmmword_3E60;
    v92 = 6293310241825115725LL;
    v95 = 0;
    v15 = strlen(v13);
    if ( v15 < 1 )                              // 判断随机字符串长度是否小于1
    {
    v16 = 0;
    }
    else
    {
    // 随机字符串大于1
    v16 = 0;                                  // 计数随机字符串中出现0-9的次数
    v17 = (unsigned int)v15;                  // 随机字符串的长度
    v18 = v94;
    do
    {
        v20 = *v13++;
        v19 = v20;
        v21 = v20 - 97;
        v22 = v20 - 32;
        if ( (unsigned int)(v20 - 48) < 0xA )
        ++v16;
        if ( v21 < 0x1A )
        v19 = v22;
        --v17;
        *v18++ = v19;
    }
    while ( v17 );
    }
    v36 = atoi(v12);                            // 当前时间(整型)
    dword_6110 = ((unsigned int)(v36 + v16) >> 16) & 0xFF;
    dword_6114 = (unsigned __int16)(v36 + v16) >> 8;
    v90 = v36 + v16;
    v86 = 0;                                    // 存储包含当前时间的向量
    v84 = 0u;
    v85 = 0u;
    v82 = 0u;
    v83 = 0u;
    v80 = 0u;
    v81 = 0u;
    v78 = 0u;
    v79 = 0u;
    v76 = 0u;
    v77 = 0u;
    v74 = 0u;
    v75 = 0u;
    v72 = 0u;
    v73 = 0u;
    dword_6118 = (unsigned int)(v36 + v16) >> 24;
    dword_611C = (v36 + v16) & 0xFF;
    v87 = (unsigned int)(v36 + v16) >> 24;      // 往向量中存储当前时间以供稍后sub_2BF0函数进行计算,时间是转换成大端存储的时间
    v88 = (unsigned int)(v36 + v16) >> 16;
    v89 = (unsigned __int16)(v36 + v16) >> 8;
    v70 = 0u;
    v71 = 0u;
    v37 = strlen(v11);
    v38 = (unsigned __int8 *)malloc(2
                                * (unsigned int)((unsigned __int64)(v37 + 2) * (unsigned __int128)0xAAAAAAAAAAAAAAABLL >> 64) & 0xFFFFFFFC | 1LL);
    v39 = strlen(v11);
    sub_29F8(v11, v39, v38);                // 将请求的API路径进行Base64编码
    v40 = strlen(v38);
    sub_2BF0((unsigned __int8 *)&v70, v38, (__int64)&v86, v40, 8LL);// 计算Base64编码后的请求的API路径与时间的哈希值,20字节
    v41 = *(_DWORD *)((unsigned __int64)&v70 & 0xFFFFFFFFFFFFFFF0LL | BYTE3(v71) & 0xF);
    dword_6124 = BYTE3(v71);
    dword_6108 = BYTE3(v71) & 0xF;
    dword_610C = v41;
    v42 = bswap32(v41);
    v43 = v42 & 0x7FFFFFFF;
    dword_6104 = (v42 & 0x7FFFFFFF) / 0x271F35A0;
    v67 = 15540725856023089LL;
    dword_6120 = v42;
    v44 = 1307386003LL * ((v42 >> 2) & 0x1FFFFFFF);
    v45 = (v42 & 0x7FFFFFFF) / 0x3AuLL;
    v46 = *((_BYTE *)&v91 + v43 - 58 * (_DWORD)v45);
    v47 = *((unsigned __int8 *)&v91 + (unsigned int)v45 - 58 * (2369637129u * v45 >> 37));
    LODWORD(v44) = *((unsigned __int8 *)&v91 + (v44 >> 40) - 58 * (unsigned int)(2369637129u * (v44 >> 40) >> 37));
    v48 = *((unsigned __int8 *)&v91 + v43 / 0x2FA28 - 58 * (2369637129u * (v43 / 0x2FA28uLL) >> 37));
    v49 = *((unsigned __int8 *)&v91 + v43 / 0xACAD10 - 58 * (2369637129u * (v43 / 0xACAD10uLL) >> 37));
    v69 = 0;
    LOBYTE(v67) = v46;                          // HKey第一位字节
    BYTE1(v67) = v47;                           // HKey第三位字节
    BYTE2(v67) = v44;                           // HKey第二位字节
    BYTE3(v67) = v48;                           // HKey第五位字节
    BYTE4(v67) = v49;                           // HKey第四位字节
    v66.n128_u64[0] = __PAIR__(v44, v47);
    v66.n128_u64[1] = __PAIR__(v49, v48);
    v68 = 0;
    sub_23FC((int *)&v66);                      // 计算最后两位校验码数据
    v50 = vaddvq_s32(v66);
    v51 = v50
        - 100
        * (((unsigned __int64)(1374389535LL * v50) >> 63)
        + ((signed int)((unsigned __int64)(1374389535LL * v50) >> 32) >> 5));
    sub_25F4((__int64)&v68, v52, v53, (unsigned int)v51, v54, v55, v56, v57, v66.n128_i64[0]);
    v58 = v68;
    if ( v51 >= 10 )
    v59 = v68;
    else
    v59 = 48;
    if ( v51 >= 10 )
    v58 = HIBYTE(v68);
    BYTE5(v67) = v59;                           // HKey第六位字节
    BYTE6(v67) = v58;                           // HKey第七位字节

我们需要关注的几个重点是

  1. v11 v12 v13这三个变量分别代表的是什么
  2. sub_29F8这个函数是做什么的
  3. sub_2BF0这个函数是做什么的
  4. 其中sub_2BF0函数中又包含了一个sub_2D50,那么这个函数又是做什么的
  5. sub_23FC这个函数是做什么的

通过对上下文进行比对,可以发现其实v11 v12 v13这三个变量其实就是在Java层传进来的JString转换成CString后的结果。打上注释。

再来看sub_29F8这个函数,进入之后发现其对着一个变量疯狂读取。

sub_29F8

有啥这么好看的?来,让我康康!

byte_41D4

过来之后可以看到是个字节数组,那就看看十六进制视图。

byte_41D4

好,base64Encode没跑了,打上注释。

接下来是sub_2BF0,进入之后看到做了一些没看懂的操作

sub_2BF0

veorq_s8

查了一下指令的文档,原来是对一个向量进行异或操作,那就没事了,我们继续。
再往下之后又进行了两次函数的调用,这个函数就是sub_2D50。没办法,点进去看看。
好像没看到有什么…
往下滑到底看一下,这时可以看到返回值是个20字节的数组

result

说到20字节的返回值能有什么呢,第一反应就是sha1啊。不过这里好像也没有看到有initialize values
说到这里,我翻看了《加密与跳楼(第4版)》第6章 找sha1的初始参数。

book
book

嗯…确实没有,直到我把它的参数放入计算器

calc

好家伙,原来是这样!打上注释。

突然想发个图:TNND,给我玩阴滴是吧.jpg

接下来是sub_23FC,进入之后发现就是对我们传进去的参数进行一堆的运算,然后原路返回,那就没事了,打上注释。

Frida

现在来Hook一下我们上面分析的几个函数

frida

hookresult

hookresult

没得毛病,进入下一步

nice

计算过程

总结一下hkey的计算过程大概就是这样

input requestPath
input timestamp
input randomString

output encodedRequestPath = base64Encode(requestPath)

process bias = getNumberCount(randomString)
output encodedTimestamp = byteSwap(timestamp + bias)

alloc timestampBuffer[72]
alloc requestPathBuffer[84]

process memcpy(timestampBuffer, encodedRequestPath, encodedRequestPath.length)
process vectorXor(timestampBuffer, "6666666666666666")
process memcpy(timestampBuffer + 68, encodedTimestamp, 4)
output timestampSha1Result = sha1(timestampBuffer, 72)

process memcpy(requestPathBuffer, encodedRequestPath, encodedRequestPath.length)
process vectorXor(requestPathBuffer, "\\\\\\\\\\\\\\\\")
process memcpy(requestPathBuffer + 64, timestampSha1Result, 20)
output requestPathSha1Result = sha1(requestPathBuffer, 84)

alloc characterMapping[58] = "23456789BCDFGHJKMNPQRTVWXY" + randomString.toUpperCase()
alloc checkSumBuffer[16]
alloc hkeyBuffer[7]

output indexFactor = byteSwap(requestPathSha1Result[19] & 0xF) & 0x7FFFFFFF

process hkeyBuffer[0] = characterMapping[indexFactor % 0x3A]
process hkeyBuffer[1] = characterMapping[indexFactor / 0x3A % 0x3A]
process hkeyBuffer[2] = characterMapping[indexFactor / 0xD24 % 0x3A]
process hkeyBuffer[3] = characterMapping[indexFactor / 0x2FA28 % 0x3A]
process hkeyBuffer[4] = characterMapping[indexFactor / 0xACAD10 % 0x3A]

process memcpy(checkSumBuffer, hkeyBuffer + 1, 4)
process checkSumBuffer = calcCheckSum(checkSumBuffer) // 纯数值计算
process checkSum = (vectorAdd(checkSumBuffer) % 100).toFixedWidthHexString(2)

process hkeyBuffer[5] = checkSum[0]
process hkeyBuffer[6] = checkSum[1]

output hkey = hkeyBuffer

其中calcCheckSum函数就是伪代码中的sub_23FC函数。由于函数是纯数值计算,所以直接套用就好了。

代码验证

写一份代码验证一下。

验证代码

验证结果

可以看到结果符合我们的预期。

结语

其实过程中在很多地方踩了坑,调试了好几次才懂了233。
那就这样了,有缘再见~