免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动!

内容参考于:图灵Python学院

工具下载:

链接:https://pan.baidu.com/s/1bb8NhJc9eTuLzQr39lF55Q?pwd=zy89

提取码:zy89

复制这段内容后打开百度网盘手机App,操作更方便哦

上一个内容:33.安卓逆向2-frida hook技术-逆向分析加密方式(一)(api-sign java层)

上一个内容中找api-sign找到的native方法,本次就开始从so文件中找native方法的实现,然后如下图,解压apk,然后找到下图红框的文件,把它拖到ida里面

然后使用ida打开,然后如下图红框,打开导出函数

然后按CTRL+F,搜索上一个内容找到的native方法gsNav

然后双击下图红框位置打开它

双击之后是下图的样子

image-20250720100911018

或这样

然后按F5,如下图生成伪代码,首先整理一下参数 a3是context(上下文),a4的值是请求参数,a5的值是tokenSecret是一个空值,a6是false

gsNav代码分析

// __fastcall是一个调用约定,指示函数使用寄存器快速传递参数
// 函数名遵循JNI命名规范:Java_包名_类名_方法名
int __fastcall Java_com_vip_vcsp_KeyInfo_gsNav(int a1, int a2, int a3, int a4, int a5, int a6)
{
  int v9; // r5  // 定义一个局部变量v9,用于存储返回值,r5可能是寄存器分配注释

  // 调用j_Utils_ima()函数进行条件检查
  if ( j_Utils_ima() )
    // 如果条件为真,调用j_Functions_gs函数并将结果赋给v9
    // 注意这里跳过了第三个参数a3,直接传递a1,a2,a4,a5,a6
    // 传递了 a4请求参数,a5token a6false
    v9 = j_Functions_gs(a1, a2, a4, a5, a6);
  else
    // 如果条件为假,将v9赋值为0
    v9 = 0;
    
  // 调用j_Utils_checkJniException函数检查JNI调用过程中是否发生异常
  // 传入的参数a1是JNIEnv指针,用于异常检查
  j_Utils_checkJniException(a1);
  
  // 返回计算结果
  return v9;
}

然后j_Utils_checkJniException是异常检查,我们现在找的是加密,所以j_Utils_checkJniException可以直接忽略了,然后在ida中双击 j_Functions_gs 进入 j_Functions_gs 函数中,如下图它又调用了Functions_gs函数

双击 Functions_gs 进入 Functions_gs函数,如下图 Functions_gs函数就很长了

然后啥也别管,直接全选,复制给ai,让ai写详细注释,a3是请求参数 a4是token a5是false

// 假值设定:
// a3 = Java中的请求参数Map(内容:{"id":"1001", "name":"test"})
// a4 = token字符串("tk_887766",身份令牌)
// a5 = false(布尔值,无实际数据)
// 函数功能:处理请求参数+token,生成哈希验证结果
int __fastcall Functions_gs(int a1, int a2, int a3, int a4, int a5)
{
  const char *strData; // r5  从a5(false)获取的初始数据:因a5=false,j_get_strData返回NULL
  size_t v10; // r0        strData的长度:strData为NULL,v10=0
  const char *v11; // r1   j_Utils_gsigds处理结果:输入为空,返回默认字符串"default"(假设函数行为)
  char *v12; // r6         a4(token)转换为C字符串:"tk_887766"
  int DataSize; // r0      计算总数据大小:包含请求参数+token,返回100(足够存储所有数据)
  signed int v14; // r8    复用DataSize=100
  char *v15; // r0         动态分配内存:100+2=102字节,地址0x7f1234(假设)
  char *v16; // r6         指向分配的内存:0x7f1234
  int v17; // r4           获取a3的entrySet结果:Java对象引用0x40001(假设)
  int v18; // r6           获取迭代器:Java对象引用0x40002(假设)
  int v19; // r0           "java/util/Set"类的Class对象:Java对象引用0x40003(假设)
  int v20; // r4           复用v19=0x40003
  int v21; // r0           "java/util/Iterator"类的Class对象:Java对象引用0x40004(假设)
  int v22; // r5           复用v21=0x40004
  int v23; // r2           Iterator.hasNext方法ID:0x1001(假设)
  int v24; // r4           hasNext返回值:true(1,表示有下一个元素)
  int v25; // r0           "java/util/Map$Entry"类的Class对象:Java对象引用0x40005(假设)
  int v26; // r6           复用v25=0x40005
  signed int v27; // r9    v69字符串长度:初始为7("default"),后续动态变化
  int v28; // r0           迭代器next返回的Entry对象:Java对象引用0x40006(假设,第一个键值对"id=1001")
  int v29; // r6           复用v28=0x40006
  int v30; // r1           Entry.getKey返回的键:Java对象引用0x40007(字符串"id")
  int StringByteArray; // r0 键"id"转换的字节数组:Java对象引用0x40008(假设)
  int v32; // r5           复用StringByteArray=0x40008
  int v33; // r4           字节数组长度:2("id"的字节长度)
  const void *v34; // r0   字节数组首地址:指向"id"的内存(假设地址0x80001)
  signed int v35; // r2    复用v33=2
  const void *v36; // r4   复用v34=0x80001
  signed int v37; // r1    复用v27=7(当前数据长度)
  int v38; // r1           Entry.getValue返回的值:Java对象引用0x40009(字符串"1001")
  int v39; // r0           值"1001"转换的字节数组:Java对象引用0x40010(假设)
  int v40; // r5           复用v39=0x40010
  signed int v41; // r4    值字节数组长度:4("1001"的字节长度)
  int v42; // r0           值字节数组首地址:指向"1001"的内存(假设地址0x80002)
  const void *v43; // r3   复用v42=0x80002
  char *v44; // r0         指向当前数据拼接位置:0x7f1234 + 7 = 0x7f123b(动态计算)
  const void *v45; // r9   复用v43=0x80002
  int v46; // r0           下一次hasNext返回值:true(1,表示还有元素)
  int v47; // r5           复用v46=1
  const char *ByteHash; // r4 哈希计算结果:"0x1a2b3c4d"(假设)
  size_t v49; // r0        v67字符串长度:7("default") + 8(哈希值) = 15
  int v50; // r4           最终返回值:1(成功)
  void *v52; // r0         指向动态分配的内存:0x7f1234
  signed int v53; // [sp+8h] [bp-458h]  临时存储数据长度计算结果:2+4=6
  int v54; // [sp+Ch] [bp-454h]        存储Map.Entry类的Class对象:0x40005
  int v55; // [sp+10h] [bp-450h]       存储Set类的Class对象:0x40003
  int v56; // [sp+14h] [bp-44Ch]       存储Iterator类的Class对象:0x40004
  int v57; // [sp+18h] [bp-448h]       存储Set对象:0x40001
  int v58; // [sp+20h] [bp-440h]       存储Map.Entry.getValue方法ID:0x1002(假设)
  int v59; // [sp+24h] [bp-43Ch]       存储当前值对象:0x40009
  int v60; // [sp+28h] [bp-438h]       存储当前键对象:0x40007
  int v61; // [sp+2Ch] [bp-434h]       存储Map.Entry.getKey方法ID:0x1003(假设)
  int v62; // [sp+30h] [bp-430h]       存储Iterator.hasNext方法ID:0x1001
  int v63; // [sp+34h] [bp-42Ch]       存储Iterator.next方法ID:0x1004(假设)
  int v64; // [sp+38h] [bp-428h]       存储entrySet方法的返回结果:0x40001
  int v65; // [sp+3Ch] [bp-424h]       存储迭代器对象:0x40002
  char *p; // [sp+3Ch] [bp-424h]       指向动态分配的内存:0x7f1234
  char v67[256]; // [sp+40h] [bp-420h] BYREF  用于存储核心数据+哈希结果:"default0x1a2b3c4d"
  _BYTE v68[256]; // [sp+140h] [bp-320h] BYREF 用于存储哈希计算的输出:[0x1a, 0x2b, 0x3c, 0x4d, ...]
  char v69[256]; // [sp+240h] [bp-220h] BYREF  用于存储处理后的核心数据:"default" → "default&tk_887766"
  _BYTE v70[256]; // [sp+340h] [bp-120h] BYREF 用于存储j_Utils_gsigds函数的输出缓冲区:"default"

  // 初始化v70缓冲区:256字节全为0
  memset(v70, 0, sizeof(v70)); // v70 = [0,0,0,...,0]

  // 初始化v69缓冲区:256字节全为0
  memset(v69, 0, sizeof(v69)); // v69 = [0,0,0,...,0]

  // 从a5(false)获取初始数据:假设j_get_strData返回NULL(因a5=false)
  strData = (const char *)j_get_strData(a5); // strData = NULL

  // 计算strData长度:strData为NULL,strlen返回0
  v10 = strlen(strData); // v10 = 0

  // 处理strData(NULL):假设j_Utils_gsigds对NULL输入返回默认字符串"default"
  v11 = (const char *)j_Utils_gsigds(v70, strData, v10); // v11 = "default"(存储在v70中)

  // 若v11为NULL则返回0:v11="default",非NULL,继续执行
  if ( !v11 )
    return 0; // 不执行

  // 复制v11到v69:v69 = "default"
  strcpy(v69, v11); // v69 = "default"(长度=7)

  // a4(token)非空,处理附加数据
  if ( a4 ) // a4 = "tk_887766"(非空),条件成立
  {
    // 将a4(Java字符串)转换为C字符串:成功得到"tk_887766"
    v12 = (char *)j_Utils_jstringtochar(a1, a4); // v12 = "tk_887766"(长度=10)

    if ( v12 ) // v12非空,执行
    {
      // 在v69末尾添加'&':v69 = "default&"
      *(_WORD *)&v69[strlen(v69)] = 38; // v69 = "default&"(长度=8)

      // 拼接v12到v69:v69 = "default&tk_887766"
      strcat(v69, v12); // v69 = "default&tk_887766"(长度=8+10=18)

      // 释放v12内存:避免内存泄漏
      free(v12); // v12 = NULL
    }
  }

  // 计算所需数据总大小:包含v69(18字节)和请求参数(假设估算为82字节),共100字节
  DataSize = j_Utils_getDataSize(a1, a2, a3, v69); // DataSize = 100

  // 保存DataSize到v14
  v14 = DataSize; // v14 = 100

  // 若数据大小<1则返回0:100≥1,继续执行
  if ( DataSize < 1 )
    return 0; // 不执行

  // 分配DataSize+2字节的内存:102字节,地址0x7f1234(假设)
  v15 = (char *)malloc(DataSize + 2); // v15 = 0x7f1234

  // 保存分配的内存地址到v16
  v16 = v15; // v16 = 0x7f1234

  // 若内存分配失败则返回0:分配成功,继续执行
  if ( !v15 )
    return 0; // 不执行

  // 初始化分配的内存为0:0x7f1234开始的102字节全为0
  memset(v15, 0, v14 + 2); // 内存状态:[0,0,0,...,0](102字节)

  // ---------------------------
  // 处理a3(请求参数Map:{"id":"1001", "name":"test"})
  // ---------------------------

  // 获取a3的entrySet:返回Java对象引用0x40001(表示键值对集合)
  v17 = (*(int (__fastcall **)(int, int))(*(_DWORD *)a1 + 124))(a1, a3); // v17 = 0x40001

  // 检查entrySet方法调用是否成功:假设成功,继续执行
  if ( !(*(int (__fastcall **)(int, int, const char *, const char *))(*(_DWORD *)a1 + 132))(
          a1, v17, "entrySet", "()Ljava/util/Set;") )
    return 0; // 不执行

  // p指向分配的内存:p = 0x7f1234
  p = v16; // p = 0x7f1234

  // 获取迭代器:返回Java对象引用0x40002
  v18 = (*(int (__fastcall **)(int, int))(*(_DWORD *)a1 + 136))(a1, a3); // v18 = 0x40002

  // 若迭代器为空则返回0:迭代器非空,继续执行
  if ( !v18 )
    return 0; // 不执行

  // 获取"java/util/Set"类的Class对象:返回Java对象引用0x40003
  v19 = (*(int (__fastcall **)(int, const char *))(*(_DWORD *)a1 + 24))(a1, "java/util/Set"); // v19 = 0x40003

  // 保存v19到v20
  v20 = v19; // v20 = 0x40003

  // 若Class对象获取失败则返回0:成功,继续执行
  if ( !v19 )
    return 0; // 不执行

  // 检查iterator方法调用是否成功:假设成功,继续执行
  if ( !(*(int (__fastcall **)(int, int, const char *, const char *))(*(_DWORD *)a1 + 132))(
          a1, v19, "iterator", "()Ljava/util/Iterator;") )
    return 0; // 不执行

  // 获取迭代器对象:返回Java对象引用0x40002
  v65 = (*(int (__fastcall **)(int, int))(*(_DWORD *)a1 + 136))(a1, v18); // v65 = 0x40002

  // 若迭代器为空则返回0:迭代器非空,继续执行
  if ( !v65 )
    return 0; // 不执行

  // 获取"java/util/Iterator"类的Class对象:返回Java对象引用0x40004
  v21 = (*(int (__fastcall **)(int, const char *))(*(_DWORD *)a1 + 24))(a1, "java/util/Iterator"); // v21 = 0x40004

  // 保存v21到v22
  v22 = v21; // v22 = 0x40004

  // 若Class对象获取失败则返回0:成功,继续执行
  if ( !v21 )
    return 0; // 不执行

  // 获取Iterator.hasNext方法ID:返回0x1001
  v23 = (*(int (__fastcall **)(int, int, const char *, const char *))(*(_DWORD *)a1 + 132))(a1, v21, "hasNext", "()Z"); // v23 = 0x1001

  // 若方法ID获取失败则返回0:成功,继续执行
  if ( !v23 )
    return 0; // 不执行

  // 保存v18到v57
  v57 = v18; // v57 = 0x40002

  // 保存v20到v55
  v55 = v20; // v55 = 0x40003

  // 保存v23到v62
  v62 = v23; // v62 = 0x1001

  // 调用hasNext方法:返回true(1,表示有下一个元素)
  v24 = (*(int (__fastcall **)(int, int))(*(_DWORD *)a1 + 148))(a1, v65); // v24 = 1

  // 保存v22到v56
  v56 = v22; // v56 = 0x40004

  // 获取Iterator.next方法ID:返回0x1004
  v63 = (*(int (__fastcall **)(int, int, const char *, const char *))(*(_DWORD *)a1 + 132))(
          a1, v22, "next", "()Ljava/lang/Object;"); // v63 = 0x1004

  // 若方法ID获取失败则返回0:成功,继续执行
  if ( !v63 )
    return 0; // 不执行

  // 获取"java/util/Map$Entry"类的Class对象:返回Java对象引用0x40005
  v25 = (*(int (__fastcall **)(int, const char *))(*(_DWORD *)a1 + 24))(a1, "java/util/Map$Entry"); // v25 = 0x40005

  // 保存v25到v26
  v26 = v25; // v26 = 0x40005

  // 若Class对象获取失败则返回0:成功,继续执行
  if ( !v25 )
    return 0; // 不执行

  // 获取Map.Entry.getValue方法ID:返回0x1002
  v58 = (*(int (__fastcall **)(int, int, const char *, const char *))(*(_DWORD *)a1 + 132))(
          a1, v25, "getValue", "()Ljava/lang/Object;"); // v58 = 0x1002

  // 保存v26到v54
  v54 = v26; // v54 = 0x40005

  // 获取Map.Entry.getKey方法ID:返回0x1003
  v61 = (*(int (__fastcall **)(int, int, const char *, const char *))(*(_DWORD *)a1 + 132))(
          a1, v26, "getKey", "()Ljava/lang/Object;"); // v61 = 0x1003

  // 计算v69的长度:v69 = "default&tk_887766",长度=18
  v27 = strlen(v69); // v27 = 18

  // 复制v69到p指向的内存:p[0~17] = "default&tk_887766"
  qmemcpy(p, v69, v27); // 内存状态:p = "default&tk_887766"(18字节)

  // v24=1(有下一个元素),进入循环
  if ( v24 )
  {
    // 循环遍历所有键值对
    do
    {
      // ------------ 处理第一个键值对:"id=1001" ------------
      
      // 获取下一个Entry对象:返回Java对象引用0x40006
      v28 = (*(int (__fastcall **)(int, int, int))(*(_DWORD *)a1 + 136))(a1, v65, v63); // v28 = 0x40006

      // 保存v28到v29
      v29 = v28; // v29 = 0x40006

      // 若Entry为空则跳出循环:非空,继续执行
      if ( !v28 )
        break; // 不执行

      // 获取键:返回Java对象引用0x40007(字符串"id")
      v30 = (*(int (__fastcall **)(int, int, int))(*(_DWORD *)a1 + 136))(a1, v28, v61); // v30 = 0x40007

      // 若键为空则跳转到LABEL_43:非空,继续执行
      if ( !v30 )
        goto LABEL_43; // 不执行

      // 保存键对象到v60
      v60 = v30; // v60 = 0x40007

      // 将键转换为字节数组:返回Java对象引用0x40008
      StringByteArray = j_Utils_getStringByteArray(a1); // StringByteArray = 0x40008

      // 保存字节数组对象到v32
      v32 = StringByteArray; // v32 = 0x40008

      // 字节数组非空,继续执行
      if ( StringByteArray )
      {
        // 获取字节数组长度:"id"的长度=2
        v33 = (*(int (__fastcall **)(int, int))(*(_DWORD *)a1 + 684))(a1, StringByteArray); // v33 = 2

        // 获取字节数组首地址:假设为0x80001
        v34 = (const void *)(*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)a1 + 736))(a1, v32, 0); // v34 = 0x80001

        // 保存v33到v35
        v35 = v33; // v35 = 2

        // 保存v34到v36
        v36 = v34; // v36 = 0x80001

        // 字节数组长度≥1,继续执行
        if ( v35 >= 1 )
        {
          // 保存v27到v37:v27=18
          v37 = v27; // v37 = 18

          // 字节数组首地址有效,且拼接后不超过总长度(18+2=20 ≤ 100)
          if ( v34 && (v27 += v35, v35 + v37 <= v14) )
            // 复制字节数组内容到p的对应位置:p[18~19] = "id"
            qmemcpy(&p[v37], v34, v35); // 内存状态:p = "default&tk_887766id"(20字节)
          else
            v27 = v37; // 不执行
        }

        // 释放字节数组资源
        if ( v36 )
          (*(void (__fastcall **)(int, int, const void *, _DWORD))(*(_DWORD *)a1 + 768))(a1, v32, v36, 0); // 释放0x80001

        // 释放字节数组对象引用
        (*(void (__fastcall **)(int, int))(*(_DWORD *)a1 + 92))(a1, v32); // 释放0x40008
      }

      // 当前长度<总长度(20 < 100),添加'=':p[20] = '='
      if ( v27 < v14 )
        p[v27++] = 61; // 内存状态:p = "default&tk_887766id="(21字节)

      // 获取值:返回Java对象引用0x40009(字符串"1001")
      v38 = (*(int (__fastcall **)(int, int, int))(*(_DWORD *)a1 + 136))(a1, v29, v58); // v38 = 0x40009

      // 值非空,继续执行
      if ( v38 )
      {
        // 保存值对象到v59
        v59 = v38; // v59 = 0x40009

        // 将值转换为字节数组:返回Java对象引用0x40010
        v39 = j_Utils_getStringByteArray(a1); // v39 = 0x40010

        // 保存字节数组对象到v40
        v40 = v39; // v40 = 0x40010

        // 字节数组非空,继续执行
        if ( v39 )
        {
          // 获取字节数组长度:"1001"的长度=4
          v41 = (*(int (__fastcall **)(int, int))(*(_DWORD *)a1 + 684))(a1, v39); // v41 = 4

          // 获取字节数组首地址:假设为0x80002
          v42 = (*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)a1 + 736))(a1, v40, 0); // v42 = 0x80002

          // 保存v42到v43
          v43 = (const void *)v42; // v43 = 0x80002

          // 字节数组长度≥1,继续执行
          if ( v41 >= 1 )
          {
            // 字节数组首地址有效
            if ( v42 )
            {
              // 计算拼接后的长度:21+4=25
              v53 = v41 + v27; // v53 = 25

              // 拼接后不超过总长度(25 ≤ 100)
              if ( v41 + v27 <= v14 )
              {
                // 指向当前拼接位置:p[21]
                v44 = &p[v27]; // v44 = 0x7f1234 + 21 = 0x7f1249

                // 保存v43到v45
                v45 = v43; // v45 = 0x80002

                // 复制字节数组内容到p的对应位置:p[21~24] = "1001"
                qmemcpy(v44, v43, v41); // 内存状态:p = "default&tk_887766id=1001"(25字节)

                // 恢复v43
                v43 = v45; // v43 = 0x80002

                // 更新当前数据长度:v27 = 25
                v27 = v53; // v27 = 25
              }
            }
          }

          // 释放字节数组资源
          if ( v43 )
            (*(void (__fastcall **)(int, int, const void *, _DWORD))(*(_DWORD *)a1 + 768))(a1, v40, v43, 0); // 释放0x80002

          // 释放字节数组对象引用
          (*(void (__fastcall **)(int, int))(*(_DWORD *)a1 + 92))(a1, v40); // 释放0x40010
        }

        // 检查是否有下一个元素:返回true(1,表示还有"name=test")
        v46 = (*(int (__fastcall **)(int, int, int))(*(_DWORD *)a1 + 148))(a1, v65, v62); // v46 = 1

        // 保存v46到v47
        v47 = v46; // v47 = 1

        // 当前长度<总长度(25 < 100)且有下一个元素,添加'&':p[25] = '&'
        if ( v27 < v14 && v46 )
          p[v27++] = 38; // 内存状态:p = "default&tk_887766id=1001&"(26字节)

        // 释放Entry对象引用
        (*(void (__fastcall **)(int, int))(*(_DWORD *)a1 + 92))(a1, v29); // 释放0x40006

        // 释放值对象引用
        (*(void (__fastcall **)(int, int))(*(_DWORD *)a1 + 92))(a1, v59); // 释放0x40009

        // 释放键对象引用
        (*(void (__fastcall **)(int, int))(*(_DWORD *)a1 + 92))(a1, v60); // 释放0x40007
      }
      else
      {
        // 值为空时的处理(不执行)
LABEL_43:
        // 检查是否有下一个元素
        v47 = (*(int (__fastcall **)(int, int, int))(*(_DWORD *)a1 + 148))(a1, v65, v62); // v47 = 1
      }

      // ------------ 处理第二个键值对:"name=test" ------------
      // (循环继续,逻辑与第一个键值对类似,简略说明)
      
      // 获取下一个Entry对象:返回Java对象引用0x40011
      v28 = (*(int (__fastcall **)(int, int, int))(*(_DWORD *)a1 + 136))(a1, v65, v63); // v28 = 0x40011

      // ... 省略中间步骤(与第一个键值对处理逻辑相同) ...

      // 最终拼接结果:p = "default&tk_887766id=1001&name=test"(总长度=37字节)
      // 此时v27 = 37

      // 检查是否有下一个元素:返回false(0,表示没有更多元素)
      v47 = (*(int (__fastcall **)(int, int, int))(*(_DWORD *)a1 + 148))(a1, v65, v62); // v47 = 0

    // 循环条件:v47=0(无更多元素),退出循环
    }
    while ( v47 );
  }

  // 释放Set对象引用
  (*(void (__fastcall **)(int, int))(*(_DWORD *)a1 + 92))(a1, v57); // 释放0x40001

  // 释放迭代器对象引用
  (*(void (__fastcall **)(int, int))(*(_DWORD *)a1 + 92))(a1, v65); // 释放0x40002

  // 释放entrySet方法返回结果的引用
  (*(void (__fastcall **)(int, int))(*(_DWORD *)a1 + 92))(a1, v64); // 释放0x40001

  // 释放Set类Class对象的引用
  (*(void (__fastcall **)(int, int))(*(_DWORD *)a1 + 92))(a1, v55); // 释放0x40003

  // 释放Iterator类Class对象的引用
  (*(void (__fastcall **)(int, int))(*(_DWORD *)a1 + 92))(a1, v56); // 释放0x40004

  // 释放Map.Entry类Class对象的引用
  (*(void (__fastcall **)(int, int))(*(_DWORD *)a1 + 92))(a1, v54); // 释放0x40005

  // 初始化v68缓冲区:256字节全为0
  memset(v68, 0, sizeof(v68)); // v68 = [0,0,0,...,0]

  // 初始化v67缓冲区:256字节全为0
  memset(v67, 0, sizeof(v67)); // v67 = [0,0,0,...,0]

  // 计算p中数据的哈希值:假设结果为"0x1a2b3c4d"
 // 拼接后的完整数据(包含token和请求参数)
  // p = "default&tk_887766id=1001&name=test"(长度37字节)
  // v14 = 100(j_Utils_getDataSize计算的总数据大小)
  
  // 第一次哈希计算:对完整拼接数据生成哈希值
  // 参数说明:
  // a1 = JNIEnv*(JNI环境指针,用于Java交互)
  // a2 = jobject(调用该函数的Java对象实例,如KeyInfo对象)
  // p = "default&tk_887766id=1001&name=test"(待计算哈希的数据)
  // v14 = 100(数据最大长度,防止越界)
  // v68 = 用于存储哈希结果的缓冲区(256字节,初始全0)
  ByteHash = (const char *)j_getByteHash(a1, a2, p, v14, v68, 256); // 假设结果为"0x1a2b3c4d"

  // 哈希计算成功(ByteHash非空),继续执行
  if ( ByteHash )
  {
    // 复制v69到v67:v69 = "default&tk_887766" → v67 = "default&tk_887766"
    strcpy(v67, v69); // v67 = "default&tk_887766"

    // 拼接哈希结果到v67:v67 = "default&tk_8877660x1a2b3c4d"
    strcat(v67, ByteHash); // v67 = "default&tk_8877660x1a2b3c4d"

    // 初始化v68缓冲区:256字节全为0
    memset(v68, 0, sizeof(v68)); // v68 = [0,0,0,...,0]

    // 计算v67的长度:24("default&tk_887766") + 10("0x1a2b3c4d") = 34
    v49 = strlen(v67); // v49 = 34

      // 第二次哈希计算:对"核心数据+第一次哈希结果"进行验证
    // 参数说明:
    // a1 = JNIEnv*(同上)
    // a2 = jobject(同上)
    // v67 = "default&tk_8877660x1a2b3c4d"(拼接后的验证数据)
    // v49 = 34(v67的实际长度)
    // v68 = 用于存储二次哈希结果的缓冲区(256字节)
    if ( j_getByteHash(a1, a2, v67, v49, v68, 256) )
      // 获取返回值:假设返回1(成功)
      v50 = (*(int (__fastcall **)(int))(*(_DWORD *)a1 + 668))(a1); // v50 = 1
    else
      v50 = 0; // 不执行

    // 保存p到v52
    v52 = p; // v52 = 0x7f1234
  }
  else
  {
    // 哈希计算失败(不执行)
    v50 = 0;
    v52 = p;
  }

  // 释放动态分配的内存:释放0x7f1234
  free(v52); // 内存释放

  // 返回最终结果:1(成功)
  return v50; // 返回1
}

总结:它使用了两次哈希加密,也就是 j_getByteHash 函数调用了两次

第一次j_getByteHash的值

p = "default&tk_887766id=1001&name=test"

第二次j_getByteHash的值

v67 = "default&tk_8877660x1a2b3c4d"

第一次哈希值的说明

bool值+token+ 请求参数(使用&拼接)

第二次哈希值的说明

bool值+token+第一次哈希的加密

然后hook一下 j_getByteHash 函数,hook需要函数地址,然后找一下它的地址,首先如下图点进 j_getByteHash 函数中

getByteHash函数的说明,它使用的是SHA-1算法进行的加密,现在加密算法找到了

// 函数功能:对输入数据计算SHA-1哈希值,并转换为十六进制字符串
// 参数含义(结合假值场景):
// a1:JNIEnv*(JNI环境指针,值为0x7b5c0000,用于JNI交互)
// a2:jobject(调用者Java对象,值为0x7b6d0000,即KeyInfo实例)
// a3:输入数据的内存地址(值为0x7b7e0000,指向"default&tk_887766id=1001&name=test")
// a4:输入数据的长度(值为37,即上述字符串的字节数)
// a5:存储哈希结果的缓冲区(值为0x7b8f0000,256字节,初始全0)
char *__fastcall getByteHash(int a1, int a2, int a3, int a4, char *a5)
{
  char *v7; // r4  保存哈希结果缓冲区的地址(最终返回值,即a5)
  int i; // r5    循环计数器(0~4,用于处理5个32位哈希值)
  int v9; // r2   临时存储单个32位哈希值(v12[i])
  char v11[68]; // [sp+0h] [bp-D8h] BYREF  临时缓冲区,用于存储单个哈希值的十六进制字符串
  _DWORD v12[26]; // [sp+44h] [bp-94h] BYREF  SHA-1算法的上下文缓冲区(存储中间计算结果)

  // 检查输入数据地址是否有效(a3=0x7b7e0000,非空,继续执行)
  if ( !a3 )
    return 0; // 若a3为空,返回0(哈希失败)

  // 保存哈希结果缓冲区地址(v7=0x7b8f0000)
  v7 = a5;

  // 初始化SHA-1上下文(v12清空,准备计算)
  j_SHA1Reset(v12);

  // 将输入数据写入SHA-1上下文:
  // 数据地址a3=0x7b7e0000,长度a4=37(即"default&tk_887766id=1001&name=test")
  j_SHA1Input(v12, a3, a4);

  // 计算SHA-1哈希结果(v12存储160位哈希值,分5个32位整数)
  if ( j_SHA1Result(v12) ) // 假设计算成功,返回true
  {
    // 循环处理5个32位哈希值(i=0到4)
    for ( i = 0; i != 5; ++i )
    {
      // 取出第i个32位哈希值(假设v12[0]=0x1a2b3c4d,v12[1]=0x5e6f7a8b,等)
      v9 = v12[i];

      // 清空临时缓冲区v11(避免残留数据)
      memset(v11, 0, 64);

      // 将32位哈希值转换为8位十六进制字符串(例如0x1a2b3c4d → "1a2b3c4d")
      sprintf(v11, "%08x", v9);

      // 将十六进制字符串拼接到结果缓冲区a5(0x7b8f0000)
      // 循环5次后,a5的值为"1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a"(40字符)
      strcat(a5, v11);
    }
  }

  // 返回哈希结果缓冲区地址(0x7b8f0000,即a5)
  return v7;
}

然后按g,然后复制下图选中的地址

然后来到下图红框窗口

然后按g,然后粘贴上方复制的地址,然后点击ok

然后就会跳到下图位置69954

找到地址之后,要注意,当前apk是一个32位的,如下图红框,它是没有arm64标记的,这种就是32位,32位的地址需要进行+1,也就是 69954 + 1 这样,64位就可以直接 69954 这样

FridaHook代码

function hook(){
    // 得到libkeyinfo.so的首地址
    var add = Module.findBaseAddress("libkeyinfo.so");
    // 根据通过ida得到的地址找方法,如果是32位需要进行+1否侧会失败
    var fuaddr = add.add(0x69954 + 1);

    Interceptor.attach(fuaddr, {
        onEnter: function(args) {
            console.log("generate_signature 被调用");
            console.log("参数1: " + args[1].readCString());// jobject
            console.log("参数2: " + args[2].readCString());// 要加密的数据
            console.log("参数3: " + args[3].toInt32());// 要加密数据的长度
            console.log("参数4: " + args[4].readCString());// 缓冲区
        },
        onLeave: function(retval) {
            console.log("generate_signature 返回值: " + Memory.readUtf8String(retval));
        }
    });
}
hook();

打印参数:

然后抓一个包,分析一下,复制下图红框的数据

然后来到Frida控制台搜索(注意,Windows自带的命令窗口没办法搜索,要使用Trae CN或PyCharm这种工具才可以,如果没有这种工具,就把Frida打印的内容复制到一个可以搜索的文件里,然后搜索),如下图红框找到之后复制出来进一步分析

然后验证sha1算法,下图是通过百度搜索sha1在线加密,找到的网址,它是一个标准sha1算法

然后通过分析参数再次总结加密

之前,这是使用ai分析的

第一次j_getByteHash的值

p = "default&tk_887766id=1001&name=test"

第二次j_getByteHash的值

v67 = "default&tk_8877660x1a2b3c4d"

第一次哈希值的说明

bool值+token+ 请求参数(使用&拼接)

第二次哈希值的说明

bool值+token+第一次哈希的加密

=========================================================

第二次分析

经过观察加密参数,跟ai分析的差不多,就这个 default&tk_887766 有差异,这个是一个固定的字符串(加密字符串,解密的钥匙,salt加盐 ),应该是

第一次哈希值的说明

加密字符串+请求参数(使用&拼接)

第二次哈希值的说明

加密字符串+第一次哈希的加密

如下图,加密字符串+请求参数

Python还原代码

from lib2to3.pygram import pattern_grammar  # 导入lib2to3模块中的模式语法(此导入未使用,可能是冗余)

import requests  # 导入requests库用于发送HTTP请求
import hashlib  # 导入hashlib库用于生成哈希值


def calc_sign(params):
    """
    生成API请求签名

    签名算法流程:
    1. 将参数按字典序排序并拼接为key=value&key=value格式的字符串
    2. 对拼接后的字符串进行两次SHA-1哈希计算
    3. 第一次使用固定盐值前缀+参数字符串
    4. 第二次使用相同盐值前缀+第一次哈希结果

    参数:
        params: 字典、列表或元组形式的请求参数

    返回:
        str: 最终生成的签名值
    """
    # 处理不同类型的参数输入
    if isinstance(params, (list, tuple, dict)):
        if hasattr(params, 'items'):  # 如果是字典,转换为键值对列表
            params = params.items()
        # 按key排序并拼接为字符串
        params = '&'.join(f'{k}={v}' for k, v in sorted(params))

    # 将参数字符串编码为字节
    if isinstance(params, str):
        params = params.encode()

    # 固定盐值,用于签名生成
    salt = b'a84c5883206309ad076deea939e850dc'

    # 第一次SHA-1哈希:盐值 + 参数
    s1 = hashlib.sha1(salt + params).hexdigest()

    # 第二次SHA-1哈希:盐值 + 第一次哈希结果
    s2 = hashlib.sha1(salt + s1.encode()).hexdigest()

    return s2


# 构造请求参数
data = {
    'api_key': '23e7f28019e8407b98b84cd05b5aef2c',
    'app_name': 'shop_android',
    'app_version': '7.45.6',
    'channel_flag': '0_1',
    'client': 'android',
    'client_type': 'android',
    'context': '{"615":"1","872":"1"}',
    'darkmode': '0',
    'deeplink_cps': '',
    'did': '0.0.58fdfb4bc72790446cd2e3c21aff9a50.2e59f7',
    'extParams': '{"showSellPoint":"1","adsPids":"6920223326667242251,6920114641319248459,6921349460463011607,6920497241532422795","mclabel":"1","cmpStyle":"1","ic2label":"1","reco":"1","vreimg":"1","floatwin":"1","preheatTipsVer":"4","exclusivePrice":"1","stdSizeVids":"","rank":"2","couponVer":"v2","live":"1"}',
    'fdc_area_id': '104104101',
    'mars_cid': '629f921d-f491-36a0-9f5a-7b0cabfd388b',
    'mobile_channel': '8rdlo74r:::',
    'mobile_platform': '3',
    'other_cps': '',
    'page_id': 'page_te_commodity_search_1752984823757',
    'phone_model': 'Pixel 4',
    'productIds': '6921204268919700310,6921408896791478610,6921375397673015699,6920223326667242251,6921337665776311384,6921344955805410115,6920939356172588998,6921420448746661788,6920114641319248459,6921466613541632786,6921450939339626515,6921423469517681117,3910107803058900,6921349460463011607,3850943103604185,6921204268919675734,6921422021191015122,6921309131053318675,6920497241532422795,6921395944782045710',
    'province_id': '104104',
    'referer': 'com.xxx.xxx.search.activity.xxx',
    'rom': 'Dalvik/2.1.0 (Linux; U; Android 10; Pixel 4 Build/QQ3A.200705.002)',
    'scene': 'search',
    'sd_tuijian': '0',
    'session_id': '629f921d-f491-36a0-9f5a-7b0cabfd388b_shop_android_1752980618054',
    'skey': '2d30297ff20ec9b7442dc4f3c335abdc',
    'source_app': 'android',
    'standby_id': '8rdlo74r:::',
    'sys_version': '29',
    'timestamp': '1752984825',  # 请求时间戳,用于标识请求的时效性
    'warehouse': 'VIP_NH',
}

# 计算请求签名
sign = calc_sign(data)

# 构造HTTP请求头
headers = {
    'Host': 'mapi.xxx.com',  # 请求的目标主机
    'authorization': 'OAuth api_sign=' + sign,  # 使用计算的签名进行认证
    'x-vip-host': 'mapi.xxx.com',  # VIP专用请求头,指定目标主机
    'content-type': 'application/x-www-form-urlencoded',  # 请求体格式
    'user-agent': 'okhttp/3.12.13',  # 模拟Android客户端的HTTP库
}

# 发送POST请求到API接口
response = requests.post(
    'https://mapi.xxxx.com/xxxx/rest/xxx/xxx/module/list/v2',
    headers=headers,
    data=data,  # 使用form-encoded格式发送参数
)

# 打印响应内容和状态码
print(response.text)  # 打印API返回的原始数据
print(response)  # 打印响应对象,包含状态码等信息

效果图:可以正常请求得到数据


img

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐