前言
大家好呀,欢迎来到我的博客.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(还原算法)
目录
前言
声明
抓包
sig分析
sp分析
总结
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关! 本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
抓包
反复抓包后确定了是这个包,只不过响应是加密的,params里有sp和sig参数,sp参数是一个长串,sig由V3.0拼接一个32位字符串,猜测是md5
sig分析
可以尝试搜索字符串sig或者hook hashmap等等方法定位,我这里hook hashmap
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);
}
})
搜索net.bosszhipin.base.m 这个类可以看到sp和sig,八成就是这里了,这里先hook sig
从h方法点进去看看
右键复制frida 片段
抓个包后确认就是这里了
接着从signature方法点进去
接下来方便还原算法我们需要固定好入参,写java层的主动调用,然后配合着ida静态分析来还原算法
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注册的函数
// 获取 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代码
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代码
// 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代码中分析还原算法?并且还有可能遇到魔改算法你又该如何处理?
sp分析
md5算法是hash算法,不可逆,作用是用来验签的,防止数据包被篡改,那就肯定有一个传递加密前明文的参数,从上面的抓包来看只有可能是sp参数,这就说的通了,明文加密成sp,并且和明文的MD5一起传给后台,后台接受数据包后解密sp得到明文,并再次加密明文和传来的sig对比以防止数据包被篡改
分析sig的时候已经提了sp的分析,和sig差不多,同理可以主动调用
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(两个_)