赞
踩
声明:本文内容仅供学习交流,严禁用于商业用途,否则由此产生的一切后果均与作者无关。如有冒犯,请联系我删除。
这里采用的是vpn抓包方式,通过brook配置socks5代理,将流经的所有tcp流量直接导到charles抓包软件上查看,很Nice! 类似postern +charles。 brook配置更简单一些。根据个人习惯来即可。具体的环境配置我这里就略过啦,不熟悉的朋友,多面向搜索引擎,也能很快解决, 这里我就略过啦。废话少说,开始整活。
我们在app上随便点击一个商品,进入商品详情,在charles Ctrl + F
搜索一下标题,过滤下目标请求,如下图所示
可以看到有两个结果,但由于我们的目标是详情数据,故detail更符合我们的预期,点进去看一眼,可以发现就是我们的目标请求。
看下请求参数,有几个疑似加密参数,如下图所示,
经多个商品的抓包对比和重放攻击,不难发现,主要的加密参数是请求头里的authorization
, 其他参数均可写死。
下面我们开始分析目标参数authorization参数是如何生成的。
打开jadx, 反编译我们的apk, 搜索一下字符串OAuth api_sign
, 很幸运,只有两个结果,非常舒服!!
点第一个进去,不难发现,基本锁定请求authorization
的值,就是从这里生成的。
下面开始一层层剥洋葱,定位真正的加密函数。我依次截图放上, 复现定位的过程。(jadx中,ctrl + 鼠标左键点击函数名,即可快速跳转到函数定义处,很方便)
我们在jadx中搜一下com.vip.vcsp.KeyInfo
这个类,点进去,如下:gs()方法最终调用的是gsNav()这个native方法。
洋葱就这样一层层剥到底了。现在,赶紧掏出心爱的Frida Hook验证一下吧。(Objection 做Java层Hook更方便, 自行尝试哈,这里不展开讲)
hook代码如下:
// hook_code.js function printMap(param_hm){ // hashmap 整体打印 var HashMap = Java.use('java.util.TreeMap'); var arg_map = Java.cast(param_hm, HashMap); // console.log('Map转字符串: ' + arg_map.toString()); return arg_map.toString() } function java_hook_gsNav(){ Java.perform(function () { var KeyInfo= Java.use("com.vip.vcsp.KeyInfo"); KeyInfo.gsNav.implementation = function (context, map, str, boolean) { console.log("------------------------------------", "java hook KeyInfo.gsNav", "------------------------------------"); // console.log("gsNav map:", printMap(map)); console.log("gsNav map:", printMap(map)); console.log("gsNav str:", str); console.log("gsNav boolean:", boolean); var result = this.gsNav(context, map, str, boolean); console.log("gsNav result: " + result + "\n"); return result } }) } // frida -U -l hook_code.js com.achievo.vipshop
这里采用命令行交互模式注入的方式, 进行hook验证。
然后,我们手机重新进入商品详情页,并抓包,把抓包结果的请求头authorization的值复制出来,在Hook结果中搜索,发现命中,一炮就怀上了… 参数一就是请求参数的拼接,参数二是Null, 参数三是false。
为了进一步分析加密结果的特征,主动调用往往是一个好思路,可以控制函数的输入,看看是否能得到相同的输出,同时可减少干扰。
修改下代码,最终的代码如下:
// hook_code.js function printMap(param_hm){ // hashmap 整体打印 var HashMap = Java.use('java.util.TreeMap'); var arg_map = Java.cast(param_hm, HashMap); // console.log('Map转字符串: ' + arg_map.toString()); return arg_map.toString() } function java_hook_gsNav(){ Java.perform(function () { var KeyInfo= Java.use("com.vip.vcsp.KeyInfo"); KeyInfo.gsNav.implementation = function (context, map, str, boolean) { console.log("------------------------------------", "java hook KeyInfo.gsNav", "------------------------------------"); // console.log("gsNav map:", printMap(map)); console.log("gsNav map:", printMap(map)); console.log("gsNav str:", str); console.log("gsNav boolean:", boolean); var result = this.gsNav(context, map, str, boolean); console.log("gsNav result: " + result + "\n"); return result } }) } function java_call_gsNav(){ Java.perform(function () { console.log("------------------------------------", "java call KeyInfo.gsNav", "------------------------------------"); var currentApplication = Java.use("android.app.ActivityThread").currentApplication(); var context = currentApplication.getApplicationContext(); var map = Java.use("java.util.TreeMap").$new(); map.put("api_key", "aaaa"); map.put("app_name", "shop_android"); var string = null; var boolean = false; var KeyInfo= Java.use("com.vip.vcsp.KeyInfo"); var result = KeyInfo.gsNav(context, map, string, boolean); console.log("call gsNav result: " + result + "\n"); }) } // frida -U -l hook_code.js com.achievo.vipshop
命令行交互模式下,修改完的Hook 代码,是即时生效的,我们直接在命令行调用即可,如下所示:
我们发现,多次主动调用函数,计算结果是固定的,且长度恒为40个字符和数字的组合。对哈希算法有所了解的朋友,就会开始猜测是否sha1了,因为sha1的计算结果正是40个16进制数(即160bit,一个16进制数等于4bit)。
究竟是不是sha1, 那还得掏出神器ida, 分析下libkeyinfo.so. 在Export 导出函数窗口搜gsNav,发现是静态注册,顺利点击进去
先处理下JNiEnv, 以及变量重命名,继续跟进j_Functions_gs函数
很快就进到Functions_gs(JNIEnv *env, int jobject, int map, int str, int boolean)函数,往下拉,从返回值倒着分析,发现有个可疑函数名j_getByteHash
跟进去看看,发现又调用了getByteHash
再跟进getByteHash, 发现有惊喜,函数符号没抹去,就是sha1相关.
我们不妨hook 一下getByteHash的几个参数,其中参数一是env 参数二是jobject , 参数四是(参数3)长度, 这三个参数从前面的函数定位过程可以直接分析出来,我们主要看下参数3和参数5,hook一下看看:
强调一下,这里为了减少干扰,我们先让手机息屏(尽量避免触发网络请求,减少干扰),然后采用java层主动调用和native层hook相互打配合的方式,进行分析。修改hook代码如下:
// hook_code.js function printMap(param_hm){ // hashmap 整体打印 var HashMap = Java.use('java.util.TreeMap'); var arg_map = Java.cast(param_hm, HashMap); // console.log('Map转字符串: ' + arg_map.toString()); return arg_map.toString() } function java_hook_gsNav(){ Java.perform(function () { var KeyInfo= Java.use("com.vip.vcsp.KeyInfo"); KeyInfo.gsNav.implementation = function (context, map, str, boolean) { console.log("------------------------------------", "java hook KeyInfo.gsNav", "------------------------------------"); // console.log("gsNav map:", printMap(map)); console.log("gsNav map:", printMap(map)); console.log("gsNav str:", str); console.log("gsNav boolean:", boolean); var result = this.gsNav(context, map, str, boolean); console.log("gsNav result: " + result + "\n"); return result } }) } function java_call_gsNav(){ Java.perform(function () { console.log("------------------------------------", "java call KeyInfo.gsNav", "------------------------------------"); var currentApplication = Java.use("android.app.ActivityThread").currentApplication(); var context = currentApplication.getApplicationContext(); var map = Java.use("java.util.TreeMap").$new(); map.put("api_key", "aaaa"); map.put("app_name", "shop_android"); var string = null; var boolean = false; var KeyInfo= Java.use("com.vip.vcsp.KeyInfo"); var result = KeyInfo.gsNav(context, map, string, boolean); console.log("call gsNav result: " + result + "\n"); }) } function native_hook_getByteHash(){ var fun_addr = Module.findExportByName("libkeyinfo.so", "getByteHash"); Interceptor.attach(fun_addr, { onEnter: function (args) { console.log("------------------------------------", "native hook getByteHash", "------------------------------------"); console.log("参数3", args[2]); console.log("参数4", args[3]); console.log("参数5", args[4]); }, onLeave: function (retval) { // console.log("return value: ", Java.cast(retval, Java.use("java.lang.String"))); } }) } function main() { native_hook_getByteHash() ; // 注意hook 在前 java_call_gsNav() // java call 在后 } // frida -U -l hook_code.js com.achievo.vipshop
我们发现,一次主动调用会触发三次getByteHash方法,参数3和参数5看起来像是内存地址,我们修改下hook代码
function native_hook_getByteHash(){ var fun_addr = Module.findExportByName("libkeyinfo.so", "getByteHash"); Interceptor.attach(fun_addr, { onEnter: function (args) { console.log("------------------------------------", "native hook getByteHash", "------------------------------------"); this.arg3 = args[2]; this.arg5 = args[4]; this.length = args[3].toInt32(); console.log("length", this.length); console.log("参数3"); console.log(hexdump(this.arg3, {length: this.length})); console.log("参数5"); console.log(hexdump(this.arg5)) }, onLeave: function (retval) { console.log("--- onLeave ---"); console.log("参数3"); console.log(hexdump(this.arg3, {length: this.length})); console.log("参数5"); console.log(hexdump(this.arg5)) // console.log("return value: ", Java.cast(retval, Java.use("java.lang.String"))); } })
运行
我们发现getByteHash()函数依然是调用了三次,第一次调用看不出啥,第二次调用时参数三包含了我们的入参, 且前面加了个字符串a84c5883206309ad076deea939e850dc
, 应该是盐值;参数五都是0,熟悉native 开发的大佬,知道大概率是个buffer, 用来存储计算结果的。为了更好地呈现打印效果,我们再次优化下hook代码,如下:
function native_hook_getByteHash(){ var fun_addr = Module.findExportByName("libkeyinfo.so", "getByteHash"); Interceptor.attach(fun_addr, { onEnter: function (args) { console.log("------------------------------------", "native hook getByteHash", "------------------------------------"); this.arg3 = args[2]; this.arg5 = args[4]; this.length = args[3].toInt32(); console.log("length", this.length); console.log("plainText:"); console.log(hexdump(this.arg3, {length: this.length})); // console.log("buffer"); // console.log(hexdump(this.arg5, {length: 0x28})) }, onLeave: function (retval) { console.log("--- onLeave ---"); console.log("计算结果:"); console.log(hexdump(this.arg5, {length: 0x28})) } }) }
再次运行,进行hook, 打印效果不错,清晰明了。
至此,可以得出结论了,请求头authorization的值,是这样生成的:
我们还有个疑问,该样本的sha1是否标准算法呢?很简单,我们随便找个在线工具验证即可
与第二次调用的hook结果完全一致, 证明没魔改。
最后,再写个小脚本验证一下
完美手工!感谢观看!!!
需要交流可v我:vx: Coder007_NoBug
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。