34.安卓逆向2-frida hook技术-逆向分析加密方式(一)(api-sign so层)-利用ai大模型分析代码
安卓逆向 安卓协议分析 app协议分析 保姆级攻略 app逆向 Android apk逆向
免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动!
内容参考于:图灵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

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

双击之后是下图的样子

或这样

然后按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) # 打印响应对象,包含状态码等信息
效果图:可以正常请求得到数据


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