唯品会app请求头参数authorization的逆向分析与算法还原

声明:本文内容仅供学习交流,严禁用于商业用途,否则由此产生的一切后果均与作者无关。如有冒犯,请联系我删除。

一、说明

  • 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

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
在HttpServletRequest中途修改请求头信息Authorization可以通过以下步骤实现: 1. 获取HttpServletRequest对象中的请求头Authorization的值。 2. 构造一个新的请求头Authorization的值。 3. 使用setHeader()方法将新的请求头Authorization的值设置到HttpServletRequest对象中。 具体实现代码如下: ``` HttpServletRequest request = ...; // 获取HttpServletRequest对象 String oldAuthorization = request.getHeader("Authorization"); // 获取请求头Authorization的值 String newAuthorization = ...; // 构造新的请求头Authorization的值 request.setHeader("Authorization", newAuthorization); // 设置新的请求头Authorization的值 ``` 另外,如果你使用的是Spring框架中的ServerHttpRequest对象,也可以通过以下步骤实现: 1. 获取ServerHttpRequest对象中的请求头Authorization的值。 2. 构造一个新的请求头Authorization的值。 3. 使用mutate()方法创建一个新的ServerHttpRequest对象,并将新的请求头Authorization的值设置到其中。 具体实现代码如下: ``` ServerHttpRequest request = ...; // 获取ServerHttpRequest对象 String oldAuthorization = request.getHeaders().getFirst("Authorization"); // 获取请求头Authorization的值 String newAuthorization = ...; // 构造新的请求头Authorization的值 ServerHttpRequest newRequest = request.mutate().header("Authorization", newAuthorization).build(); // 创建一个新的ServerHttpRequest对象,并将新的请求头Authorization的值设置到其中 ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值