声明:本文内容仅供学习交流,严禁用于商业用途,否则由此产生的一切后果均与作者无关。如有冒犯,请联系我删除。
一、说明
- app版本: v7.45.6
- 下载地址:aHR0cHM6Ly93d3cud2FuZG91amlhLmNvbS9hcHBzLzMxNTgzL2hpc3Rvcnlfdjc0NTA2
- 使用的工具 jadx + ida + brook + charles
二、抓包与加密定位
2.1 抓包
这里采用的是vpn抓包方式,通过brook配置socks5代理,将流经的所有tcp流量直接导到charles抓包软件上查看,很Nice! 类似postern +charles。 brook配置更简单一些。根据个人习惯来即可。具体的环境配置我这里就略过啦,不熟悉的朋友,多面向搜索引擎,也能很快解决, 这里我就略过啦。废话少说,开始整活。
我们在app上随便点击一个商品,进入商品详情,在charles Ctrl + F
搜索一下标题,过滤下目标请求,如下图所示
可以看到有两个结果,但由于我们的目标是详情数据,故detail更符合我们的预期,点进去看一眼,可以发现就是我们的目标请求。
看下请求参数,有几个疑似加密参数,如下图所示,
经多个商品的抓包对比和重放攻击,不难发现,主要的加密参数是请求头里的authorization
, 其他参数均可写死。
2.2 加密函数定位
下面我们开始分析目标参数authorization参数是如何生成的。
打开jadx, 反编译我们的apk, 搜索一下字符串OAuth api_sign
, 很幸运,只有两个结果,非常舒服!!
点第一个进去,不难发现,基本锁定请求authorization
的值,就是从这里生成的。
下面开始一层层剥洋葱,定位真正的加密函数。我依次截图放上, 复现定位的过程。(jadx中,ctrl + 鼠标左键点击函数名,即可快速跳转到函数定义处,很方便)
我们在jadx中搜一下com.vip.vcsp.KeyInfo
这个类,点进去,如下:gs()方法最终调用的是gsNav()这个native方法。
洋葱就这样一层层剥到底了。现在,赶紧掏出心爱的Frida Hook验证一下吧。(Objection 做Java层Hook更方便, 自行尝试哈,这里不展开讲)
三、Hook 分析 与 算法还原
3.1 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。
3.2. 主动调用
为了进一步分析加密结果的特征,主动调用往往是一个好思路,可以控制函数的输入,看看是否能得到相同的输出,同时可减少干扰。
修改下代码,最终的代码如下:
// 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)。
3.3 native 层hook分析
究竟是不是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, 打印效果不错,清晰明了。
3.4 算法还原
至此,可以得出结论了,请求头authorization的值,是这样生成的:
- 第一步:sha1(固定盐值 + 请求参数拼接后的字符串) , 得到第一次哈希值;
- 第二步:sha1(固定盐值 + 第一步计算的结果), 得到第二次哈希值。
- 第三步:拼接OAuth api_sign= 和 第二次哈希值,就是最终的authorization的值。
我们还有个疑问,该样本的sha1是否标准算法呢?很简单,我们随便找个在线工具验证即可
与第二次调用的hook结果完全一致, 证明没魔改。
最后,再写个小脚本验证一下
完美手工!感谢观看!!!
需要交流可v我:vx: Coder007_NoBug