接口参数定位
目标接口:搜索 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 出来的



Hook 获取参数
通过 frida 获取传入的参数

主动调用,方便后续做测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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); })} |
1 | BitmapkitUtils.getSignFromJni result = st=1731485532078&sign=3c9820dce84fc15ceaf29bf0e0630306&sv=111 |
结果中就包含了我们今天的主角 ==sign== 。长度为 32 脑海中就冒出 MD5了。
确定 so
调用到了 native 层了。java 层的分析就到这里了。这里类中并没有看到加载 so 。需要通过 frida hook 导出的符号表,反查 so 的名字。

得到模板 so 的名字为 libjdbitmapkit.so
so 分析
IDA 打开可以看到 getSignFromJni 的导出函数 。通过 IDA frida-trace 先 trace 一份日志方便后面分析
工具:https://github.com/Pr0214/trace_natives
把下载到的 traceNatives.py 放到 ida 根目录的 plugin 目录下重启 IDA。点击 edit -> plugins -> traceNatives 会生成一个文件夹给frida-trace 使用。
1 | 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 查看入参。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | 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 查看参数
1 | hook_native(Module.findBaseAddress("libjdbitmapkit.so").add(0x1ADCC), 0x5); |

修改一下hook代码把第二参数输出一下

第二参数是字符串,第四个参数是传入的数据,第五个参数是传入数据的长度。所以可以让 hexdump 根据这个参数 dump 出完整的数据。


分析之后 1ADCC 就是最终的算法


下面是还原之后的算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #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 具体的算法逻辑。
第一次写文章,有什么表达的不清楚的希望大家多包涵。有什么问题,可以在评论区提出哈!