目标接口:搜索https://api.m.ooxx.com/client.action?functionId=search
版本:13.1.0
目标参数:body 中的 x-api-eid-token 和 sign
定位接口参数的方法
◆搜索大发
◆hook Hashmap
◆Hook StringBuilder
这里我们直接使用最朴素的搜索大法,一次就中。
继续跟踪,进最下面这个看看。
下面拼接了 urlEncodeUTF82 ,看英文就是一个 utf 编码之后的数据。继续向上跟踪
进入第一个,函数返回的是一个字符串。还不是加密的地方。
JDHttpTookit.getEngine().getSignatureHandlerImpl().signature 这就是签名函数。
接口函数一般是 implement 或者直接 new 出来。这里跟进 new 出来的
通过 frida 获取传入的参数
主动调用,方便后续做测试
function call_by_java() {
Java.perform(function(){
let BitmapkitUtils = Java.use("com.ooxx.common.utils.BitmapkitUtils");
let context = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext();
let str = 'search'
let str2 = '{"addrFilter":"1","addressId":"0","articleEssay":"1","attrRet":"0","buriedExpLabel":"","deviceidTail":"38","exposedCount":"0","filterServiceIds":"1468131091","first_search":"1","frontExpids":"F_001","gcAreaId":"1,72,55674,0","gcLat":"39.944093","gcLng":"116.482276","imagesize":{"gridImg":"531x531","listImg":"358x358","longImg":"531x708"},"insertArticle":"1","insertScene":"1","insertedCount":"0","isCorrect":"1","jdv":"0|kong|t_2018512525_cpv_nopay|tuiguang|17303608941925019140008|1730360893","keyword":"空气加湿器","localNum":"2","newMiddleTag":"1","newVersion":"3","oneBoxMod":"1","orignalSearch":"1","orignalSelect":"1","page":"1","pageEntrance":"1","pagesize":"10","populationType":"232","pvid":"","searchVersionCode":"10110","secondInsedCount":"0","showShopTab":"yes","showStoreTab":"1","show_posnum":"0","sourceRef":[{"action":"","eventId":"MyJD_WordSizeResult","isDirectSearch":"0","logid":"","pageId":"Home_Main","pvId":""},{"action":"","eventId":"Search_History","isDirectSearch":"0","logid":"","pageId":"Search_Activity","pvId":"632ba208e4854bb1839e6e32a5e6b841"}],"stock":"1","ver":"142"}'
let str3 = "bd132c578e85c7cd";
let str4 = "android";
let str5 = "13.1.0"; let result = BitmapkitUtils.getSignFromJni(context, str, str2, str3, str4, str5);
console.log("BitmapkitUtils.getSignFromJni result = " + result);
})
}
BitmapkitUtils.getSignFromJni result = st=1731485532078&sign=3c9820dce84fc15ceaf29bf0e0630306&sv=111
结果中就包含了我们今天的主角 ==sign== 。长度为 32 脑海中就冒出 MD5了。
调用到了 native 层了。java 层的分析就到这里了。这里类中并没有看到加载 so 。需要通过 frida hook 导出的符号表,反查 so 的名字。
得到模板 so 的名字为 libjdbitmapkit.so
IDA 打开可以看到 getSignFromJni 的导出函数 。通过 IDA frida-trace 先 trace 一份日志方便后面分析
工具:https://github.com/Pr0214/trace_natives
把下载到的 traceNatives.py 放到 ida 根目录的 plugin 目录下重启 IDA。点击 edit -> plugins -> traceNatives 会生成一个文件夹给frida-trace 使用。
frida-trace -H iP:port -F -O D:\ooxx\libjdbitmapkit_1731482430.txt
frida 就会开始trace 此时不要关闭命令行,直接调用刚才的主动触发的函数 call_by_java() 就会生成追踪数据,复制保存成一个 log 文件方便后面分析。
再用 findHash 插件看看能不能找到什么有用的信息
把两个数据做对比发现了 sub_27A4 这个函数在两边都有出现。
IDA 中按 g 跳转过去看看
看上去很像 MD5 哦,点击数字按 h 转换成十六进制然后去搜索一下
有点意思!应该是MD5。查看 trace 日志,sub_8134() 是最开始的地方,IDA 中看看是什么内容。
经过对比发现,我们的入口函数也就是 8134 。那还有什么说的,盘他咯!
在 trace 日志中 8134 这层调用的最后一个函数是 33b4,进入 33b4 之后就是 MD5算法了。我们先 hook 7e08 查看入参。
function print_arg(addr) {
try {
var module = Process.findRangeByAddress(addr);
if (module != null) return "\n"+hexdump(addr) + "\n";
return ptr(addr) + "\n";
} catch (e) {
return addr + "\n";
}
}function hook_native(funptr,paramsNum) {
var md = Process.findModuleByAddress(funptr);
console.log("hook func ");
try {
//hook 指定函数
Interceptor.attach(funptr,{
onEnter: function(args){
this.logs =""
this.params = [];
this.logs = this.logs.concat("So: "+md.name +" Method: " + ptr(funptr).sub(md.base) + "\n")
for (var i = 0; i < paramsNum; i++) {
//参数
this.params.push(args[i]);
this.logs = this.logs.concat("this.args "+i+" onEnter: " +print_arg(args[i])+"\n")
}
},
onLeave: function(retval){
for (let i = 0; i < paramsNum; i++) {
this.logs=this.logs.concat("this.args" + i + " onLeave: " + print_arg(this.params[i]));
}
this.logs=this.logs.concat("retval onLeave: " + print_arg(retval) + "\n");
console.log(this.logs);
}
});
} catch (error) {
console.log(error);
}
}
hook_native(Module.findBaseAddress("libjdbitmapkit.so").add(0x33B4), 0x3);
传入的参数很像 base64 但是解密之后还是乱码。传入之前做了处理。向上追踪,在 trace 日志,上一个被调用的函数是 2698
hook 2698 看看返回值,刚好就是 33B4 的入参
跟踪 2698 第二个参数,它来自 v37 , v37 又是来自 7E08 这个函数 。而且 在 trace 日志中也有它的身影。
经过分析可以发现 7E08 最后两个参数是两个随机数,就是这两个随机数决定了之后使用不同的分支算法。
进入 7E08 。先判断最后一个参数的随机数,用不同方式在生成一个随机数,然后与倒数第二参数的随机数相加产生新的随机数。
使用新的随机数来决定最终进入那个分支的算法。特别注意一下 gen_str_44e这是一个固定的字符串。在进入不同分支的时候,根据新的随机数取了这个字符串中不同的数据
hook 7E08 得到的参数
在我们的 trace log 中 进入了 case2 分支,就先分析这个分支。
这里要特别注意一下,因为这里是根据随机数来进入不同的分支的。所以在测试的时候不是每次都进入到这个 8cc8 分支。多调用几次,总会有一个进入的。
通过代码看到各参数都进入到 1ADCC这个函数,优先分析这个函数。hook 查看参数
hook_native(Module.findBaseAddress("libjdbitmapkit.so").add(0x1ADCC), 0x5);
修改一下hook代码把第二参数输出一下
第二参数是字符串,第四个参数是传入的数据,第五个参数是传入数据的长度。所以可以让 hexdump 根据这个参数 dump 出完整的数据。
分析之后 1ADCC 就是最终的算法
下面是还原之后的算法
#include <stdio.h>
void alg2(char* rawdata, int input_len){
int v4,v10;
char v12 ;
// 分支 2 的算法
char* key = "80306f4370b39fd5630ad0529f77adb6";
unsigned char table[0x10] = {0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB,0xF, 0xD9, 0x88, 0xEE, 0x9A, 0xE9, 0x5A};
for (int i = 0; i != input_len; ++i) {
v4 = i&7;
// v10 = byte_FA0[v9 & 0xF];
v10 = table[i& 0xF];
// v12 = ((v10 ^ *(_BYTE *)(rawdata + v9) ^ *(_BYTE *)(randondata + (v9 & 7))) + v11) ^ v10;
v12 = ((v10 ^ *(unsigned char *)(rawdata + i) ^ *(unsigned char *)(key + (i & 7))) + v10) ^ v10; // *(_BYTE *)(rawdata + v9) = v12;
*(unsigned char *)(rawdata + i) = v12;
// *(_BYTE *)(rawdata + v9) = v12 ^ *(_BYTE *)(randondata + (v9 & 7));
*(unsigned char *)(rawdata + i) = v12 ^ *(unsigned char *)(key + (i & 7));
}
}
int main(int argc, char const *argv[])
{
char input [] ="functionId=search&body={\"addrFilter\":\"1\",\"addressId\":\"0\",\"articleEssay\":\"1\",\"attrRet\":\"0\",\"buriedExpLabel\":\"\",\"deviceidTail\":\"38\",\"exposedCount\":\"0\",\"filterServiceIds\":\"1468131091\",\"first_search\":\"1\",\"frontExpids\":\"F_001\",\"gcAreaId\":\"1,72,55674,0\",\"gcLat\":\"39.944093\",\"gcLng\":\"116.482276\",\"imagesize\":{\"gridImg\":\"531x531\",\"listImg\":\"358x358\",\"longImg\":\"531x708\"},\"insertArticle\":\"1\",\"insertScene\":\"1\",\"insertedCount\":\"0\",\"isCorrect\":\"1\",\"jdv\":\"0|kong|t_2018512525_cpv_nopay|tuiguang|17303608941925019140008|1730360893\",\"keyword\":\"空æ°å 湿å¨\",\"localNum\":\"2\",\"newMiddleTag\":\"1\",\"newVersion\":\"3\",\"oneBoxMod\":\"1\",\"orignalSearch\":\"1\",\"orignalSelect\":\"1\",\"page\":\"1\",\"pageEntrance\":\"1\",\"pagesize\":\"10\",\"populationType\":\"232\",\"pvid\":\"\",\"searchVersionCode\":\"10110\",\"secondInsedCount\":\"0\",\"showShopTab\":\"yes\",\"showStoreTab\":\"1\",\"show_posnum\":\"0\",\"sourceRef\":[{\"action\":\"\",\"eventId\":\"MyJD_WordSizeResult\",\"isDirectSearch\":\"0\",\"logid\":\"\",\"pageId\":\"Home_Main\",\"pvId\":\"\"},{\"action\":\"\",\"eventId\":\"Search_History\",\"isDirectSearch\":\"0\",\"logid\":\"\",\"pageId\":\"Search_Activity\",\"pvId\":\"632ba208e4854bb1839e6e32a5e6b841\"}],\"stock\":\"1\",\"ver\":\"142\"}&uuid=bd132c578e85c7cd&client=android&clientVersion=13.1.0&st=1731550738362&sv=102\"";
alg2(input,sizeof(input)-1);
for (int i = 0; i < sizeof(input)-1; i++){
printf("%02x",(unsigned char)input[i]);
}
return 0;
}
可以看到是一致的
最后把这段数据 base64 之后再进行 md5 就是最终的 sign 值。最终拼接上 sv 和 t 值返回到 java 层
今天就先分析case2 的分支,以后有机会再分析其他分支啦!
以 x-api-eid-token 作为入口点,分析 java 层的最终点在 BitmapkitUtils.getSignFromJni。结合 frida-trace 和 findHash 找到关键点 sub_27A4 。在这个函数内部使用了随机数的方式进入不同的分支。使用 CASE2 分支作为今天的入口点,最终成功得到 sign 具体的算法逻辑。
看雪ID:绿豆粥
https://bbs.kanxue.com/user-home-791353.htm
# 往期推荐
球分享
球点赞
球在看
点击阅读原文查看更多