白嫖的动力是无限的。
前言
本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。
工具
抓包
重放测试
使用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第七位字节
我们需要关注的几个重点是
v11 v12 v13
这三个变量分别代表的是什么sub_29F8
这个函数是做什么的sub_2BF0
这个函数是做什么的- 其中
sub_2BF0
函数中又包含了一个sub_2D50
,那么这个函数又是做什么的 sub_23FC
这个函数是做什么的
通过对上下文进行比对,可以发现其实v11 v12 v13
这三个变量其实就是在Java
层传进来的JString
转换成CString
后的结果。打上注释。
再来看sub_29F8
这个函数,进入之后发现其对着一个变量疯狂读取。
有啥这么好看的?来,让我康康!
过来之后可以看到是个字节数组,那就看看十六进制视图。
好,base64Encode
没跑了,打上注释。
接下来是sub_2BF0
,进入之后看到做了一些没看懂的操作
查了一下指令的文档,原来是对一个向量进行异或操作,那就没事了,我们继续。
再往下之后又进行了两次函数的调用,这个函数就是sub_2D50
。没办法,点进去看看。
好像没看到有什么…
往下滑到底看一下,这时可以看到返回值是个20字节的数组
说到20字节的返回值能有什么呢,第一反应就是sha1
啊。不过这里好像也没有看到有initialize values
…
说到这里,我翻看了《加密与跳楼(第4版)》第6章 找sha1
的初始参数。
嗯…确实没有,直到我把它的参数放入计算器
好家伙,原来是这样!打上注释。
突然想发个图:TNND,给我玩阴滴是吧.jpg
接下来是sub_23FC
,进入之后发现就是对我们传进去的参数进行一堆的运算,然后原路返回,那就没事了,打上注释。
Frida
现在来Hook
一下我们上面分析的几个函数
没得毛病,进入下一步
计算过程
总结一下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。
那就这样了,有缘再见~