前言
本文仅供学习探讨之用,如果侵犯您的权益请联系我删除。
上文
前情提要
就在我写完上一篇文章后没过一个礼拜,小黑盒就又改 hkey
计算方法了,断断续续花了一礼拜终于搞清楚它的算法。
在这里分享一下逆向的过程。
@> 本文适用于小黑盒 1.3.121 ~ 1.3.124 ,小黑盒历史版本参考我的仓库
Smali 分析
使用Jadx打开安装包,找到hkey
的计算函数
可以看到两个版本的 hkey
计算方式略有区别,最明显的就是替换的字符改了
修改以后还是提示参数错误,肯定是 encode
函数也改了,接下来换用IDA
So 分析
先贴一下IDA生成的反汇编代码,也不用看的很仔细
int __fastcall encode(int *a1, int a2, int a3, int a4, int a5)
{
int *v5; // r9
int v6; // r11
_DWORD *v7; // r4
char *v8; // r5
char *v9; // r0
size_t v10; // r0
int v11; // r10
int v12; // r0
int v13; // r0
_DWORD *v14; // r2
const char *v15; // r1
char *v16; // r10
const char *v17; // r11
size_t v18; // r4
size_t v19; // r4
signed int v20; // r8
char *v21; // r6
char *v22; // r0
int v23; // r12
int i; // lr
int v25; // r5
int v26; // r0
unsigned int v27; // r4
int v28; // r3
unsigned int v29; // r1
int v30; // r8
const char *v31; // r4
int j; // r1
int k; // r0
int v34; // r0
size_t v35; // r5
size_t v36; // r6
char *v37; // r5
char *v38; // r0
int v39; // r1
int v40; // r12
int l; // lr
int v42; // r6
int v43; // r0
unsigned int v44; // r4
int v45; // r3
unsigned int v46; // r2
int result; // r0
int v48; // [sp+0h] [bp-48h]
int v49; // [sp+4h] [bp-44h]
int *v50; // [sp+8h] [bp-40h]
char *nptr; // [sp+Ch] [bp-3Ch]
int v52; // [sp+10h] [bp-38h]
_DWORD *v53; // [sp+14h] [bp-34h]
char s; // [sp+19h] [bp-2Fh]
int v55; // [sp+24h] [bp-24h]
v5 = a1;
v6 = a4;
v7 = &_stack_chk_guard;
if ( j_check_signature(a1) != 1 )
goto LABEL_28;
v53 = &_stack_chk_guard;
v8 = (char *)&v48 - ((strlen(KEYTWO[0]) + 62) & 0xFFFFFFF8);
v9 = strcpy(v8, KEYTWO[0]);
v10 = strlen(v9);
_aeabi_memcpy(&v8[v10], "/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time=", 56);
v7 = 0;
v11 = (*(int (__fastcall **)(int *, int, _DWORD))(*v5 + 676))(v5, v6, 0);
v12 = *v5;
v52 = a5;
v13 = (*(int (__fastcall **)(int *, int, _DWORD))(v12 + 676))(v5, a5, 0);
if ( v11 )
{
v14 = v53;
if ( !v13 )
goto LABEL_26;
v49 = v6;
v50 = &v48;
v15 = (const char *)v11;
v16 = (char *)v13;
v17 = v15;
v18 = strlen(v15);
v19 = v18 + strlen(v8);
v20 = v19 + strlen(v16);
v21 = (char *)&v48 - ((v20 + 7) & 0xFFFFFFF8);
strcpy((char *)&v48 - ((v20 + 7) & 0xFFFFFFF8), v17);
v22 = strcat(v21, v8);
nptr = v16;
strcat(v22, v16);
v23 = v20 - 1;
for ( i = 0; v23 > i; ++i )
{
v25 = 0;
while ( v25 < v23 - i )
{
v26 = (int)&v21[v25];
v27 = (unsigned __int8)v21[v25];
v28 = v25;
v29 = (unsigned __int8)v21[v25++ + 1];
if ( v27 > v29 )
{
v21[v28] = v29;
*(_BYTE *)(v26 + 1) = v27;
}
}
}
v30 = v20 / 3;
v31 = (const char *)malloc(v30 + 2);
for ( j = 0; j <= v30 + 1; ++j )
v31[j] = 0;
for ( k = 0; k <= v30; ++k )
v31[k] = v21[3 * k];
v34 = atoi(nptr);
sprintf(&s, "%#x", v34);
v35 = strlen(v31);
v36 = strlen(&s) + v35;
v37 = (char *)&v48 - ((v36 + 7) & 0xFFFFFFF8);
v38 = strcpy(v37, v31);
strcat(v38, &s);
v39 = v49;
v40 = v36 - 1;
for ( l = 0; v40 > l; ++l )
{
v42 = 0;
while ( v42 < v40 - l )
{
v43 = (int)&v37[v42];
v44 = (unsigned __int8)v37[v42];
v45 = v42;
v46 = (unsigned __int8)v37[v42++ + 1];
if ( v44 > v46 )
{
v37[v45] = v46;
*(_BYTE *)(v43 + 1) = v44;
}
}
}
(*(void (__fastcall **)(int *, int, const char *))(*v5 + 680))(v5, v39, v17);
(*(void (__fastcall **)(int *, int, char *))(*v5 + 680))(v5, v52, nptr);
v7 = (_DWORD *)j_charToJstring(v5, v37);
}
v14 = v53;
LABEL_26:
if ( *v14 == v55 )
return (int)v7;
LABEL_28:
result = *v7 - v55;
if ( *v7 == v55 )
result = j_j_charToJstring(v5, UNSIGNATURE[0]);
return result;
}
撇开变量部分,稍微整理了一下变量名,以_p
结尾的都是指针,具体变量类型参考原始代码
首先可以看到调用了_stack_chk_guard
,看名字应该是栈溢出保护,不管他
第58~61
行看起来复杂实际上是新建了一个数组,然后把字符串KWYTWO
和/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time=
拼起来存进数组buffer1
中
最后有两个__fastcall
,根据变量名称,猜测跟传入的参数有关
首先检查了是否传入了urlpath
和timestamp
这两个变量,如果传入值为空就跳出
第77~84
行看起来挺复杂,实际上就是计算出urlpath
+buffer1
+timestamp
的长度(用于开辟内存空间),然后把这三个字符串拼起来,存进buffer2
中
最后是一个循环,看上去蛮复杂,实际上就是冒泡排序,让buffer2
中的字符按照Ascii
顺序排序
首先取buffer2
长度的 1/3+2,创建数组buffer3
,然后清空数组,然后是一个循环,buffer3[k] = buffer2[k*3]
,数组长度要加2是因为字符串结尾有个\0
,以及避免特殊情况数组越界
第109~116
行,先把时间戳timestamp
转换成十六进制,然后跟buffer3
拼接起来,存进buffer4
第118~134
行跟刚刚很像,是对buffer4
的冒泡排序
最后就是调用charToJstring
然后返回
Frida Hook
这里就以验证为主,Hook了3个函数charToJstring
,strcpy
,strcat
,hook.js
如下:
console.log("脚本载入成功");
Java.perform(function() {
var charToJstringAddr = Module.findExportByName("libnative-lib.so", "charToJstring");
console.log(charToJstringAddr);
if (charToJstringAddr != null) {
Interceptor.attach(charToJstringAddr, {
onEnter: function(args) {
var arg0 = args[0];
var arg1 = args[1];
console.log('charToJstring - Enter');
console.log(Memory.readCString(arg0));
console.log(Memory.readCString(arg1))
},
onLeave: function(retval) {
console.log('======')
}
})
}
var strcat = Module.findExportByName("libc.so", "strcat");
console.log(strcat);
if (strcat != null) {
Interceptor.attach(strcat, {
onEnter: function(args) {
var arg0 = args[0];
var arg1 = args[1];
console.log('strcat - Enter');
console.log(Memory.readCString(arg0));
console.log(Memory.readCString(arg1))
},
onLeave: function(retval) {
console.log('======')
}
})
}
var strcpy = Module.findExportByName("libc.so", "strcpy");
console.log(strcpy);
if (strcpy != null) {
Interceptor.attach(strcpy, {
onEnter: function(args) {
var arg0 = args[0];
var arg1 = args[1];
console.log('strcpy - Enter');
console.log(Memory.readCString(arg0));
console.log(Memory.readCString(arg1))
},
onLeave: function(retval) {
console.log('======')
}
})
}
});
可以配合上面的分析一起看,还是比较好理解的
算法实现
C语言实现
半抄半改写出来的,buffer4
存放的是计算结果
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
// 输入1:/bbs/app/link/view/time
// 输入2:1597893810
// 输出1:$()++-/////01245789=CGKMPRUZ_aaabeefhiijklmnopqrtuvwxz (中间结果)
// 输出2:$()++-/////001223455789=CGKMPRUZ_aaabbcdeeeffhiijklmnopqrtuvwxxz
char buffer1[200] = "//Z1q/Gb/R///+9xZ561TtoHjPrv2ew0Ln8vZnI5oObw+++oa3zw++1yd7wMqU/eNKahfmji5/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time="; //密钥字符串
char buffer2[200] = "\0";
char buffer3[200] = "\0";
char buffer4[200] = "\0";
char buffer5[200] = "\0";
int timestamp = 1597893810;
char timestamp_s[] = "1597893810";
char urlpath[] = "/bbs/app/link/view/time";
int i, m = 0;
int count = 0;
int t1, t2 = 0;
int buffer2_len;
int buffer3_len;
strcpy(buffer2, urlpath);
strcat(buffer2, buffer1);
strcat(buffer2, timestamp_s); // buffer2内容是 url路径+buffer1+时间戳
buffer2_len = strlen(buffer2);
count = buffer2_len - 1;
for (i = 0; i < count; ++i)
{
m = 0;
while (m < count - i)
{
t1 = buffer2[m];
t2 = buffer2[m + 1];
if (t1 > t2)
{
buffer2[m] = t2;
buffer2[m + 1] = t1;
}
m++;
}
}
buffer3_len = buffer2_len / 3;
for (i = 0; i <= buffer3_len; ++i)
buffer3[i] = buffer2[3 * i];
sprintf(buffer5, "%#x", timestamp);
strcpy(buffer4, buffer3);
strcat(buffer4, buffer5);
count = strlen(buffer4) - 1;
for (i = 0; i < count; i++)
{
m = 0;
while (m < count - i)
{
t1 = buffer4[m];
t2 = buffer4[m + 1];
if (t1 > t2)
{
buffer4[m] = t2;
buffer4[m + 1] = t1;
}
m++;
}
}
puts(buffer4);
}
Python实现
import hashlib
# 密钥字符串
ENC_STATIC = '//Z1q/Gb/R///+9xZ561TtoHjPrv2ew0Ln8vZnI5oObw+++oa3zw++1yd7wMqU/eNKahfmji5/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time='
# 测试样例
test = [
('/account/data_report', 1597919951,
'$()++-////0112235555789=CGKMPRUZ__aaaccdeefffhijkmnooqrrtuvwxxz'),
('/game/get_game_name', 1597920084,
'$()++-////001233445555679=CGKMPRUZ__aaabeeeffghhjkmmnnortuvwxxz'),
('/bbs/app/feeds/news', 1597920132,
'$()++-/////011233345556789@DHKNQTXZ_aabbeeeffhhijlnnoprstuwwxxz'),
('/bbs/app/profile/subscribed/events', 1597920180,
'$()++-/////00113334555679=CGKMPRUZ_aaabbbdeeeffhhijkmnnopqrsstuvwxxz'),
('/bbs/app/link/view/time', 1597920216,
'$()++-/////00123335556789=CGKMPRUZ_aaabdeeeffhiijklmnopqrtuvwxxz')
]
# 计算MD5
def md5_calc(data: str) -> str:
md5 = hashlib.md5()
md5.update(data.encode('utf-8'))
result = md5.hexdigest()
return(result)
# encode实现
def encode(url: str, t: int) -> str:
enc = list(f'{url}{ENC_STATIC}{t}')
count = len(enc) - 1
for i in range(0, count):
for j in range(0, count-i):
if (enc[j] > enc[j+1]):
enc[j], enc[j+1] = enc[j+1], enc[j]
l = len(enc) // 3 + 1
enc += '\0'
enc = [enc[3*i] for i in range(0, l)]
if enc[-1] == '\0':
enc = enc[:-1]
enc += list(hex(t))
count = len(enc)-1
for i in range(0, count):
for j in range(0, count-i):
if (enc[j] > enc[j+1]):
enc[j], enc[j+1] = enc[j+1], enc[j]
result = ''.join(enc)
return(result)
# 算法验证
for i, j, k in test:
s = encode(i, j)
if (s == k):
print(i, j, '测试通过')
print(k)
else:
print(i, j, '测试失败')
print(k)
print(s)
后记
@> 全文完,完整项目在GitHub上开源:链接 (求Star)
本文链接:https://blog.chrxw.com/archives/2020/08/21/1383.html
转载请保留本文链接,谢谢
10 条评论
大佬还有更新么 求Hkey
同求OωO
继续求 小黑盒最新hkey算法 hkey=JC3CP16
弃坑了,求不到了
大佬有最新版本的hkey的Python实现方式吗?118旧版本的方法会提示 'msg': '您的应用版本过低,请升级到最新版本获取奖励',121版本的方法不会用……(´இ皿இ`)
现在还没有,以后大概率也没有了,新的加密方式还没弄明白
好吧那这个签到还是暂时放弃了,如果有新实现请务必告诉我
建议自己研究研究,研究出来了给我发个PR便是极好的
啊?所以说现在是用不了的吗?
我刚弄了个Github Actions的PR。。。
不再维护了,我自己都没在用了