大家好呀,欢迎来到我的博客.2023年12月4日,boss web上线了最新的zp_token,环境检测点又增加了,与此同时app端的关键加密so从32位换成了64位,两者ida反编译so的时候都有反调试,无法直接f5,需要手动调整让ida重新识别.google了一下几乎找不到任何有关boss app的文章,所以这篇文章讲解app端的加密.篇幅较长,坐稳发车啦!
设备 pixel 4xl android10
版本: 11.240
下载地址: aHR0cHM6Ly93d3cud2FuZG91amlhLmNvbS9hcHBzLzYyMDIyMjIvaGlzdG9yeV92MTEyNDAxMA==
工具: charles(抓包) socksdroid(流量转发) jadx(反编译dex) ida(反编译so) frida(注入) frida-trace(还原算法)
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
反复抓包后确定了是这个包,只不过响应是加密的,params里有sp和sig参数,sp参数是一个长串,sig由V3.0拼接一个32位字符串,猜测是md5
可以尝试搜索字符串sig或者hook hashmap等等方法定位,我这里hook hashmap
1 2 3 4 5 6 7 | Java.perform(function (){ var hashMap = Java.use( "java.util.HashMap" ); hashMap.put.implementation = function (a, b) { console.log( "hashMap.put: " , a, b); return this.put(a, b); } }) |
可以看到sig参数和sp参数都hook到了,接下来调整下hook代码,把堆栈输出下
搜索net.bosszhipin.base.m 这个类可以看到sp和sig,八成就是这里了,这里先hook sig
从h方法点进去看看
右键复制frida 片段
抓个包后确认就是这里了
接着从signature方法点进去
接下来方便还原算法我们需要固定好入参,写java层的主动调用,然后配合着ida静态分析来还原算法
1 2 3 4 5 6 7 8 9 | function call(){ Java.perform(function (){ let YZWG = Java.use( "com.twl.signer.YZWG" ); var str = '/api/batch/batchRunV2batch_method_feed=%5B%22method%3DzpCommon.adActivity.getV2%26dataType%3D0%26expectId%3D802924422%26dataSwitch%3D1%22%2C+%22method%3Dzpgeek.app.f1.newgeek.jobcard%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%26expectId%3D802924422%22%2C+%22method%3Dzpgeek.app.geek.trait.tip%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%26expectId%3D802924422%22%2C+%22method%3Dzpgeek.cvapp.applystatus.change.tip%22%2C+%22method%3Dzpinterview.geek.interview.f1.complainTip%22%2C+%22method%3Dzpgeek.cvapp.geek.remind.warnexp%26entrance%3D1%26itemType%3D1%22%2C+%22method%3Dzpgeek.app.f1.banner.query%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%26expectId%3D802924422%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522switchCity%2522%253A%25220%2522%257D%26gpsCityCode%3D0%26jobType%3D0%26mixExpectType%3D0%26sortType%3D1%22%2C+%22method%3Dzpinterview.geek.interview.f1%22%2C+%22method%3Dzpgeek.app.f1.recommend.filter%26commute%3D%26distance%3D0%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%26expectPosition%3D%26filterFlag%3D0%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522switchCity%2522%253A%25220%2522%257D%26filterValue%3D%26jobType%3D0%26mixExpectType%3D0%26partTimeDirection%3D%26positionCode%3D%26sortType%3D1%22%2C+%22method%3Dzpgeek.app.bluecollar.topic.banner%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%22%2C+%22method%3Dzpgeek.cvapp.geek.homeexpectaddress.query%26cityCode%3D101010100%22%2C+%22method%3Dzpgeek.app.f1.interview.recjob.tip%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%26expectId%3D802924422%22%2C+%22method%3Dzpgeek.app.geek.recommend.joblist%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%26sortType%3D1%26expectPosition%3D100514%26pageSize%3D15%26expectId%3D802924422%26page%3D1%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522switchCity%2522%253A%25220%2522%257D%22%2C+%22method%3Dzpgeek.app.studyabroad.article.headlines%22%2C+%22method%3Dzpgeek.cvapp.geek.resume.queryquality%22%5D&client_info=%7B%22version%22%3A%2210%22%2C%22os%22%3A%22Android%22%2C%22start_time%22%3A%221703159294618%22%2C%22resume_time%22%3A%221703159294618%22%2C%22channel%22%3A%2227%22%2C%22model%22%3A%22google%7C%7CPixel+4+XL%22%2C%22dzt%22%3A0%2C%22loc_per%22%3A0%2C%22uniqid%22%3A%227fe541f3-a666-4845-9186-cff9d5429f77%22%2C%22oaid%22%3A%22NA%22%2C%22did%22%3A%22DUzpQpzBYtoakGWwhYSfr2VDxKhBVPnGWdbfRFV6cFFwekJZdG9ha0dXd2hZU2ZyMlZEeEtoQlZQbkdXZGJmc2h1%22%2C%22is_bg_req%22%3A0%2C%22network%22%3A%22wifi%22%2C%22operator%22%3A%22UNKNOWN%22%2C%22abi%22%3A0%7D&curidentity=0&req_time=1703162554818&uniqid=7fe541f3-a666-4845-9186-cff9d5429f77&v=11.200' var str2 = null var res = YZWG[ "signature" ]( str ,str2) console.log(res) }) } |
可以发现return 的值是来着nativeSignature方法,看名字就知道他应该是一个native方法
点进去后发现确实是native方法,并且上面还有许多native方法,包含sp的加密方法nativeEncodeRequest(sp的寻找方式后续就不介绍了,和sig差不多),而且从字面上看,里面有解密数据的方法,正好对应响应数据的解密
往上找可以发现加载自yzwg这个so文件
解包后可以看到只有64的so 11.230版本以前都是32的so,找到libyzwg.so并拖到ida64里反编译
在导出表里搜索jni发现是动态注册,这里可以直接上脚本找出这个so注册的函数
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 43 44 45 46 47 48 49 50 51 52 | / / 获取 RegisterNatives 函数的内存地址,并赋值给addrRegisterNatives。 var addrRegisterNatives = null; / / 列举 libart.so 中的所有导出函数(成员列表) var symbols = Module.enumerateSymbolsSync( "libart.so" ); for (var i = 0 ; i < symbols.length; i + + ) { var symbol = symbols[i]; / / console.log(symbol.name) / / _ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi if (symbol.name.indexOf( "art" ) > = 0 && symbol.name.indexOf( "JNI" ) > = 0 && symbol.name.indexOf( "RegisterNatives" ) > = 0 && symbol.name.indexOf( "CheckJNI" ) < 0 ) { addrRegisterNatives = symbol.address; console.log( "RegisterNatives is at " , symbol.address, symbol.name); break } } if (addrRegisterNatives) { / / RegisterNatives(env, 类型, Java和C的对应关系,个数) Interceptor.attach(addrRegisterNatives, { onEnter: function (args) { var env = args[ 0 ]; / / jni对象 var java_class = args[ 1 ]; / / 类 var class_name = Java.vm.tryGetEnv().getClassName(java_class); var taget_class = "com.twl.signer.YZWG" ; / / 111 某个类中动态注册的so if (class_name = = = taget_class) { / / 只找我们自己想要类中的动态注册关系 console.log( "\n[RegisterNatives] method_count:" , args[ 3 ]); var methods_ptr = ptr(args[ 2 ]); var method_count = parseInt(args[ 3 ]); for (var i = 0 ; i < method_count; i + + ) { / / Java中函数名字的 var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 )); / / 参数和返回值类型 var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize)); / / C中的函数内存地址 var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2 )); var name = Memory.readCString(name_ptr); var sig = Memory.readCString(sig_ptr); var find_module = Process.findModuleByAddress(fnPtr_ptr); / / 地址、偏移量、基地址 var offset = ptr(fnPtr_ptr).sub(find_module.base); console.log( "name:" , name, "sig:" , sig, 'module_name:' ,find_module.name , "offset:" , offset); } } } }); } 命令 frida - U - f com.hpbr.bosszhipin - l 文件名.js |
结果 nativeSignature也就是sig的加密,偏移0x21864 nativeEncodeRequest也就是sp的加密,偏移0x209a4,后续ida中按g就可以跳到制定函数处
可以看到text段是金色的,也就是说ida错误的把原本是代码的地方识别成了数据,这个时候需要手动帮ida一把,让他重新把数据识别成代码
选中金色段按c(code) 转为代码
转化后出现红色段就按p(create function)
重复这个过程直到把关键函数转为代码后就可以f5了
跳到0x21864位置转为伪c代码
点进去300多行代码,不太好分析,可以借助frida trace来跟踪native函数执行的时候调用了哪些函数
使用方法https://github.com/Pr0214/trace_natives
然后主动调用上面的java方法
把执行的函数复制到notepad里分析一下,调用了挺多函数,这里就没什么特殊的技巧了,只能凭借着经验猜测哪个是关键函数
在hook 1c714函数的时候我发现了结果,hook代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var soAddr = Module.findBaseAddress( "libyzwg.so" ); var funcAddr = soAddr.add( 0x1c714 ) / / 32 位 + 1 Interceptor.attach(funcAddr,{ onEnter: function(args){ console.log( 'onEnter arg[0]: ' ,hexdump(args[ 0 ],{length:args[ 1 ].toInt32()})) console.log( 'onEnter arg[1]: ' ,args[ 1 ]) this.arg0 = args[ 0 ] }, onLeave: function(retval){ / / console.log( 'onLeave arg[0]: ' ,hexdump(this.arg0.readPointer())) console.log( 'onLeave result: ' ,hexdump(retval)) } }); |
arg0是java传进来的明文,onleave的时候结果出来了,并且明文传进去的时候还加了一个salt
复制到CyberChef里加密一下
是标准的md5,笔者在分析这个sig的时候当时并没有直接尝试加密,而是hook了他下面的函数2a5b8
hook 2a5b8代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | / / 2A5B8 var soAddr = Module.findBaseAddress( "libyzwg.so" ); var funcAddr = soAddr.add( 0x2A5B8 ) / / 32 位 + 1 var num = 0 Interceptor.attach(funcAddr,{ onEnter: function(args){ num + = 1 console.log(`onEnter arg[ 0 ] ${num}次: ${args[ 2 ]} `,hexdump(args[ 0 ])) console.log( 'onEnter arg[1]: ' ,hexdump(args[ 1 ],{length: 512 })) / / console.log( 'onEnter arg[2]: ' ,args[ 2 ]) this.arg0 = args[ 0 ] }, onLeave: function(retval){ console.log( 'onLeave arg[0]: ' ,hexdump(this.arg0)) } }); |
可以看到在执行第8次2a5b8函数后结果也是出现了
再来看第一次调用,可以看到arg0像是md5的4个初始化魔数,只不过内存中是小端字节续,由于md5的分组处理长度是512bit,所以需要多次压入数据,正好对应调用多次2a5b8函数,这个函数类似c md5中的updata和final过程
这里可以修改c++中md5的最后填充数据和附加消息长度来验证是否是标准md5
可以发现是标准的md5,到此sig参数就分析完毕了.我为什么要提这个2a5b8函数的执行过程,有人会说,我直接把明文拼接salt后md5发现是结果了不就行了吗,是的,但是如果有一天你把明文和salt拼接后md5发现不是你要的结果你该如何处理?你不懂算法细节如何在不准确的伪c代码中分析还原算法?并且还有可能遇到魔改算法你又该如何处理?
md5算法是hash算法,不可逆,作用是用来验签的,防止数据包被篡改,那就肯定有一个传递加密前明文的参数,从上面的抓包来看只有可能是sp参数,这就说的通了,明文加密成sp,并且和明文的MD5一起传给后台,后台接受数据包后解密sp得到明文,并再次加密明文和传来的sig对比以防止数据包被篡改
分析sig的时候已经提了sp的分析,和sig差不多,同理可以主动调用
1 2 3 4 5 6 7 8 9 | function call(){ Java.perform(function (){ let a = Java.use( "com.twl.signer.a" ); var str = 'batch_method_feed=%5B%22method%3DzpCommon.adActivity.getV2%26dataType%3D0%26expectId%3D802924422%26dataSwitch%3D1%22%2C+%22method%3Dzpgeek.app.f1.newgeek.jobcard%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%26expectId%3D802924422%22%2C+%22method%3Dzpgeek.app.geek.trait.tip%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%26expectId%3D802924422%22%2C+%22method%3Dzpgeek.cvapp.applystatus.change.tip%22%2C+%22method%3Dzpinterview.geek.interview.f1.complainTip%22%2C+%22method%3Dzpgeek.cvapp.geek.remind.warnexp%26entrance%3D1%26itemType%3D1%22%2C+%22method%3Dzpgeek.app.f1.banner.query%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%26expectId%3D802924422%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522switchCity%2522%253A%25220%2522%257D%26gpsCityCode%3D0%26jobType%3D0%26mixExpectType%3D0%26sortType%3D1%22%2C+%22method%3Dzpinterview.geek.interview.f1%22%2C+%22method%3Dzpgeek.app.f1.recommend.filter%26commute%3D%26distance%3D0%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%26expectPosition%3D%26filterFlag%3D0%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522switchCity%2522%253A%25220%2522%257D%26filterValue%3D%26jobType%3D0%26mixExpectType%3D0%26partTimeDirection%3D%26positionCode%3D%26sortType%3D1%22%2C+%22method%3Dzpgeek.app.bluecollar.topic.banner%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%22%2C+%22method%3Dzpgeek.cvapp.geek.homeexpectaddress.query%26cityCode%3D101010100%22%2C+%22method%3Dzpgeek.app.f1.interview.recjob.tip%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%26expectId%3D802924422%22%2C+%22method%3Dzpgeek.app.geek.recommend.joblist%26encryptExpectId%3Def7d7c83e4017a4233R40t-5FFBS%26sortType%3D1%26expectPosition%3D100514%26pageSize%3D15%26expectId%3D802924422%26page%3D1%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522switchCity%2522%253A%25220%2522%257D%22%2C+%22method%3Dzpgeek.app.studyabroad.article.headlines%22%2C+%22method%3Dzpgeek.cvapp.geek.resume.queryquality%22%5D&client_info=%7B%22version%22%3A%2210%22%2C%22os%22%3A%22Android%22%2C%22start_time%22%3A%221703222473770%22%2C%22resume_time%22%3A%221703222473770%22%2C%22channel%22%3A%2228%22%2C%22model%22%3A%22google%7C%7CPixel+4+XL%22%2C%22dzt%22%3A0%2C%22loc_per%22%3A0%2C%22uniqid%22%3A%22b99e5c38-858b-4097-8b4d-084a6e75ec62%22%2C%22oaid%22%3A%22NA%22%2C%22did%22%3A%22DUzpQpzBYtoakGWwhYSfr2VDxKhBVPnGWdbfRFV6cFFwekJZdG9ha0dXd2hZU2ZyMlZEeEtoQlZQbkdXZGJmc2h1%22%2C%22is_bg_req%22%3A0%2C%22network%22%3A%22wifi%22%2C%22operator%22%3A%22UNKNOWN%22%2C%22abi%22%3A1%7D&curidentity=0&req_time=1703222656138&uniqid=b99e5c38-858b-4097-8b4d-084a6e75ec62&v=11.240' var str2 = null var res = a[ "d" ]( str , str2) console.log(res) }) } |
同sig的frida-trace方法一样,trace后有几千行,这时需要分析哪些函数是关键函数并hook
最终的sp结果是魔改的base64,码表从A-Za-z0-9+/=替换成了A-Za-z0-9-_~ 并且这个结果是可以被des解密的,并且解密出来的raw也是不可见的,只能转为hex看看,这里埋个坑,尚不清楚传进去的明文和des解密后的密文有什么联系,先写到这里,后续再看看
1出于安全考虑,本章未提供完整流程,调试环节省略较多,只提供大致思路,具体细节要你自己还原,相信你也能调试出来.
2本人写作水平有限,如有讲解不到位或者讲解错误的地方,还请各位大佬在评论区多多指教,共同进步,也可加本人微信lyaoyao_i(两个\)