贝壳APP签名分析头部Authorization字段分析


0x01、 目标需求:

  .a) 贝壳APP的接口参数以及相关的签名获取的方式

  .b) 需要获取所有城市下的区域下的门店等数据

  .c) 好久没有写博客了,不知道关注我博客的朋友们是不是都快忘了我的博客了.

0x02、分析背景:

  .a) 版本2.66.0(目前最新版)

  .b) 软件无壳

  .c) 软件通讯过程中采取了头部签名验证的方式,

0x03、分析流程:

  .a) 通过数据包抓取或敏感函数hook方式获得接口功能

  .b) 通过函数内部参数的组装继续分析参数的来源以及加密的流程(Authorization)

请求分析

0x04、分析开始:

  .a) jadx-gui 分析签名

    0x001. 打开jadx-gui 直接搜索关键词_Authorization:

JADX分析



    0x002. Frida直接验证该拦截器是否是我们要的:

检测函数点



FridaHook验证



    0x003. POST和GET函数走的加密方法中间多了一个处理函数,来处理Body的内容(如下图):

加密过程



    0x004. 分析下后面的加密函数:

签名具体加密过程



    0x005. 获取AppSecret 和 AppId:

分析签名来源



    0x006. 接下来重要的一步就是 DeviceUtil.SHA1ToString 这个函数的参数是如何计算而来的(传入的类似一个MD5的值):


    let encrypt = Java.use("com.bk.base.util.bk.DeviceUtil");
    encrypt.SHA1ToString.implementation = function (v1) {
        let res = this.SHA1ToString(v1)
        console.log("加密参数为: " + v1 + "加密结果为: " + res)
        return res;
    }
//加密参数为: d5e343d453aecca8b14b2dc687c381ca 加密结果为:48b82883cf47ac02ed32aef24fea684851c7dab9


    0x007. 参数1是一个url,参数2是post数据,然后做了一下key的字典排序for循环拼接参数(key=value):


public String getSignString(String str, Map<String, String> map2) {
        Map<String, String> urlParams = getUrlParams(str);
        HashMap hashMap = new HashMap();
        if (urlParams != null) {
            hashMap.putAll(urlParams);
        }
        if (map2 != null) {
            hashMap.putAll(map2);
        }
        ArrayList arrayList = new ArrayList(hashMap.entrySet());
        Collections.sort(arrayList, new Comparator<Map.Entry<String, String>>() {
            /* class com.bk.base.netimpl.a.AnonymousClass1 */

            public int compare(Map.Entry<String, String> entry, Map.Entry<String, String> entry2) {
                return entry.getKey().compareTo(entry2.getKey());
            }
        });
        String httpAppSecret = ModuleRouterApi.MainRouterApi.getHttpAppSecret();
        boolean notEmpty = a.e.notEmpty(httpAppSecret);
        String str2 = BuildConfig.FLAVOR;
        if (!notEmpty) {
            try {
                httpAppSecret = JniClient.GetAppSecret(com.bk.base.config.a.getContext());
            } catch (Exception e) {
                e.printStackTrace();
                httpAppSecret = str2;
            }
        }
        String httpAppId = ModuleRouterApi.MainRouterApi.getHttpAppId();
        if (a.e.notEmpty(httpAppId)) {
            str2 = httpAppId;
        } else {
            try {
                str2 = JniClient.GetAppId(com.bk.base.config.a.getContext());
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        StringBuilder sb = new StringBuilder(httpAppSecret);
        for (int i = 0; i < arrayList.size(); i++) {
            Map.Entry entry = (Map.Entry) arrayList.get(i);
            sb.append(((String) entry.getKey()) + "=" + ((String) entry.getValue()));
        }
        String str3 = TAG;
        LjLogUtil.d(str3, "sign origin=" + ((Object) sb));
        String SHA1ToString = DeviceUtil.SHA1ToString(sb.toString());
        String encodeToString = Base64.encodeToString((str2 + ":" + SHA1ToString).getBytes(), 2);
        String str4 = TAG;
        LjLogUtil.d(str4, "sign result=" + encodeToString);
        return encodeToString;
    }


    0x008. 看到调用加密函数之前打印了一下 原始签名,我们hook一下他log的d函数看一下是啥东西:

分析参数来源



    0x009. 由于是get请求不携带参数的话不存在for循环拼接参数,所以这里有理由怀疑的是 origin=d5e343d453aecca8b14b2dc687c381ca 就是他的httpAppSecret(重启APP 看一下这个httpAppSecret 是否会变):

获取默认的appSecret



    0x010. 整个流程就分析完了,我们最终知道的是如果是get请求 默认的 appSecret d5e343d453aecca8b14b2dc687c381ca SHA1加密后 在前面拼接 "20180111_android:" 然后在Base64编码即可,如果get请求中携带了参数也需要参与加密
    如果是post请求 用默认的appSecret + key=value(这里要字典排序key)然后SHA1 后 在参数前面拼接 "20180111_android:" 得到最终的字符串后 Base64编码即可, 最终我们验证一下结果写一个get请求和一个post请求(目前没有post请求需求不写了)


GET请求结果





友情提示:本文只为技术分享交流,请勿非法用途.产生一切法律问题与本人无关.
本文中所有的调试代码均在gitee仓库中(包含很多app的关键点的hook代码) Git公开仓库地址 欢迎star or fork



在浏览的同时希望给予作者打赏,来支持作者的服务器维护费用.一分也是爱~