前言

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

上文



前情提要

就在我写完上一篇文章后没过一个礼拜,小黑盒就又改 hkey 计算方法了,断断续续花了一礼拜终于搞清楚它的算法。

在这里分享一下逆向的过程。

Smali 分析

使用Jadx打开安装包,找到hkey的计算函数

1.3.121和1.3.119计算方式明显不同

可以看到两个版本的 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;
}

第1段

撇开变量部分,稍微整理了一下变量名,以_p结尾的都是指针,具体变量类型参考原始代码

首先可以看到调用了_stack_chk_guard,看名字应该是栈溢出保护,不管他

58~61行看起来复杂实际上是新建了一个数组,然后把字符串KWYTWO/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time=拼起来存进数组buffer1

最后有两个__fastcall,根据变量名称,猜测跟传入的参数有关

第2段

首先检查了是否传入了urlpathtimestamp这两个变量,如果传入值为空就跳出

77~84行看起来挺复杂,实际上就是计算出urlpath+buffer1+timestamp的长度(用于开辟内存空间),然后把这三个字符串拼起来,存进buffer2

最后是一个循环,看上去蛮复杂,实际上就是冒泡排序,让buffer2中的字符按照Ascii顺序排序

第3段

首先取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,strcathook.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('======')
            }
        })
    }
});

Hook结果

可以配合上面的分析一起看,还是比较好理解的

算法实现

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)

后记

最后修改:2020 年 09 月 10 日
Null