2.2 贝壳

环境准备

com.lianjia.beike == 2.85.0

抓包准备

curl -H "x-req-id: acdb3a92-63da-48ea-add3-adf843bc7c09" -H "ketracetraceid: apps.api.ke.com-222f500a09e7edc2-16952-1676881616033-20" -H "Page-Schema: homepage1" -H "Cookie: lianjia_udid=222f500a09e7edc2;lianjia_ssid=b00b49d0-abc2-4ba3-a68f-4d55378a4cc7;algo_session_id=c72e2606-1828-4fb0-b93e-d85256aa82f2;lianjia_uuid=cc8265b0-7a4c-4f03-8eb9-302d4c5468b7" -H "Lianjia-Device-Id: 222f500a09e7edc2" -H "User-Agent: Beike2.95.0;google Nexus+6P; Android 8.1.0" -H "extension: lj_duid=DUcp7si9S27htrCC26jmOwkLwpZ-TtEzI0d9RFVjcDdzaTlTMjdodHJDQzI2am1Pd2tMd3BaLVR0RXpJMGQ5c2h1&ketoken=020102gXMQ1AhR57z+1dlhb1iUOT/WKh4DztUyFY27x/WpUvjpae8P2SBBPqjGhD3z1UbSKv265QoYqRvR5UcqrPVOpg==&lj_android_id=222f500a09e7edc2&lj_device_id_android=222f500a09e7edc2&mac_id=00:9A:CD:C8:CE:20" -H "Dynamic-SDK-VERSION: 1.1.0" -H "Lianjia-City-Id: 430100" -H "source-global: {}" -H "Lianjia-Channel: Android_ke_tencentd1" -H "Lianjia-Version: 2.95.0" -H "Lianjia-Im-Version: 2.34.0" -H "Lianjia-Recommend-Allowable: 1" -H "Authorization: MjAxODAxMTFfYW5kcm9pZDoyODljOWVmODNhOTZhMWE2ZGEzN2JhMThmYWEzMTJlZGYxZGZhYmY5" -H "ip: 127.0.0.1" -H "lat: 0" -H "lng: 0" -H "beikeBaseData: %7B%22appVersion%22%3A%222.95.0%22%2C%22duid%22%3A%22DUcp7si9S27htrCC26jmOwkLwpZ-TtEzI0d9RFVjcDdzaTlTMjdodHJDQzI2am1Pd2tMd3BaLVR0RXpJMGQ5c2h1%22%2C%22fpid%22%3A%22020102gXMQ1AhR57z%2B1dlhb1iUOT%2FWKh4DztUyFY27x%2FWpUvjpae8P2SBBPqjGhD3z1UbSKv265QoYqRvR5UcqrPVOpg%5Cu003d%5Cu003d%22%7D" -H "Device-id-s: 222f500a09e7edc2;DUcp7si9S27htrCC26jmOwkLwpZ-TtEzI0d9RFVjcDdzaTlTMjdodHJDQzI2am1Pd2tMd3BaLVR0RXpJMGQ5c2h1;020102gXMQ1AhR57z+1dlhb1iUOT/WKh4DztUyFY27x/WpUvjpae8P2SBBPqjGhD3z1UbSKv265QoYqRvR5UcqrPVOpg==" -H "Channel-s: Android_ke_tencentd1" -H "AppInfo-s: Beike;2.95.0;2950200" -H "Hardware-s: google;Nexus 6P" -H "SystemInfo-s: android;8.1.0" -H "WLL-KGSA: LJAPPVA accessKeyId=sjoe98HI099dhdD7; nonce=XFB9gJAVjavGI8Y1RyDlxmE6NRXNCrlQ; timestamp=1676881616; signedHeaders=Device-id-s,AppInfo-s,User-Agent,Hardware-s,Channel-s,SystemInfo-s; signature=A7oOxJJ2NpQ0TDqUVYzTgPh9RXjZWWfGKANquXl/nS0=" -H "Host: apps.api.ke.com" --compressed "https://apps.api.ke.com/config/recommend/home?city_id=430100&tab_id=secondHandV2&longitude=0.0&latitude=0.0&page=1&home_ab_group=A&is_first_entry=0"
image-20230220162927470

Hook

查看包名

adb -s 8XV5T15A30005451 shell dumpsys window | grep mCurrentFocus
image-20230220163526809

尝试直接 Hook 字符串,使用以下脚本

Java.perform(function () {

    // 获取 Java 的字符串
    const str = Java.use('java.lang.String');

    // 重载  getBytes 方法 (但是我也不知道为什么要这样, 是大佬的经验)
    str.getBytes.overload().implementation = function () {

        
        var response = this.getBytes()
        var str1 = this.toString();

        // 如果 string 里面包含了
        if (str1.indexOf("Authorization") >= 0) {
            // 输出找到了字符串
            console.log("find string:", str1);

            // 打印一下堆栈
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
        }

        return response;
    }

})

打开 App 之后发现输出了堆栈

image-20230220164822476

猜测可能出现的地方,因为 Authorizationheaders 里面,所以我认为最可能的地方就是:

  • com.bk.base.netimpl.interceptor.f.intercept(HeaderInterceptor.java:109)

  • com.bk.net.interceptor.AuthInterceptor.intercept(AuthInterceptor.java:26)

我们先尝试用 jadx 反编译一下 app, 找到第一个堆栈的地方,发现疑似就是加密的代码

image-20230220165134119
image-20230220165239489

我们直接尝试分析 Authorization 字段,查看源码得知加密的地方应该为:

com.bk.base.netimpl.a.a(newBuilder, com.google.common.net.b.AUTHORIZATION, signRequest(newBuilder, chain.request()));,所以加密函数应该为:``

我们尝试 hook 一下,直接复制 jadx 里面的代码就可以:

let f = Java.use("com.bk.base.netimpl.interceptor.f");
f["signRequest"].implementation = function (builder, request) {
    console.log('signRequest is called' + ', ' + 'builder: ' + builder + ', ' + 'request: ' + request);
    let ret = this.signRequest(builder, request);
    console.log('signRequest ret value is ' + ret);
    return ret;
};

再次查看堆栈发现这个函数的返回值确实与请求头里面的保持一致:

image-20230220165946295
image-20230220170136335

所以其实加签的源码如下:

    private String signRequest(Request.Builder builder, Request request) {
        HashMap hashMap = new HashMap();
        String uri = request.url().uri().toString();
        if (com.bk.base.netimpl.a.bV(request.url().toString()).booleanValue()) {
            return "";
        }
        if ("GET".equalsIgnoreCase(request.method())) {
            return com.bk.base.netimpl.a.mb().getSignString(uri, hashMap);
        }
        if ("POST".equalsIgnoreCase(request.method())) {
            if (request.body() instanceof FormBody) {
                return signPostFormBody(builder, request);
            }
            if (request.body() instanceof MultipartBody) {
                return signPostMultipartBody(builder, request);
            }
            return com.bk.base.netimpl.a.mb().getSignString(uri, null);
        }
        return "";
    }	

可以看到,如果是 GET 请求,使用 com.bk.base.netimpl.a.mb().getSignString(uri, hashMap) 加签;如果是 POST 请求,且 BodyFormBody, 则使用 signPostFormBody(builder, request) 加签;如果是 POST 请求,且 BodyMultipartBody, 则使用 signPostMultipartBody(builder, request) ;如果是 POST 请求,且 Body 不为 FormBody/MultipartBody,则使用 com.bk.base.netimpl.a.mb().getSignString(uri, null) 加签!

跟进堆栈,发现其实加密函数的源码是:

public String getSignString(String str, Map<String, String> map) {
        Map<String, String> urlParams = getUrlParams(str);
        HashMap hashMap = new HashMap();
        if (urlParams != null) {
            hashMap.putAll(urlParams);
        }
        if (map != null) {
            hashMap.putAll(map);
        }
        ArrayList arrayList = new ArrayList(hashMap.entrySet());
        Collections.sort(arrayList, new Comparator<Map.Entry<String, String>>() { // from class: com.bk.base.netimpl.a.1
            @Override // java.util.Comparator
            public int compare(Map.Entry<String, String> entry, Map.Entry<String, String> entry2) {
                return entry.getKey().compareTo(entry2.getKey());
            }
        });
        String httpAppSecret = ModuleRouterApi.MainRouterApi.getHttpAppSecret();
        String str2 = "";
        if (!a.e.notEmpty(httpAppSecret)) {
            try {
                httpAppSecret = JniClient.GetAppSecret(com.bk.base.config.a.getContext());
            } catch (Error e) {
                e.printStackTrace();
                httpAppSecret = "";
            }
        }
        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 + Constants.COLON_SEPARATOR + SHA1ToString).getBytes(), 2);
        StringBuilder sb2 = new StringBuilder();
        sb2.append("sign result=");
        sb2.append(encodeToString);
        LjLogUtil.d(str3, sb2.toString());
        return encodeToString;
    }	

分析一下加密逻辑:

  1. 解析 url 里面的 params 后将其追加到 map 里面,生成 hashMap

  2. hashMap 进行排序,将其转换为 arrayList

  3. 获取 httpAppSecret,生成 StringBuilder sb

  4. 获取 httpAppId 赋值给 str2

  5. 遍历 arrayList,添加至 sb

  6. sb 进行 sha1 加密,生成 SHA1ToString

  7. base64 编码 {str2}:{SHA1ToString}

获取 httpAppSecret

先尝试 Hook 一下 getHttpAppSecret , 来分析 httpAppSecret,可以知道其值为 d5e343d453aecca8b14b2dc687c381ca

    let MainRouterApi = Java.use("com.bk.base.router.ModuleRouterApi$MainRouterApi");
    MainRouterApi["getHttpAppSecret"].implementation = function () {
        console.log('getHttpAppSecret is called');
        let ret = this.getHttpAppSecret();
        console.log('getHttpAppSecret ret value is ' + ret);
        return ret;
    };
image-20230220171846211

获取 httpAppId

简单一点,知道原理之后直接 base64 解码就可以知道

httpAppId20180111_android

image-20230220173151770

算法还原

算法还原如下:

import base64
import copy
from hashlib import sha1
from urllib import parse


def encrypted(url: str, body: dict = None, secret_key: str = 'd5e343d453aecca8b14b2dc687c381ca'):
    parsed = parse.urlparse(url)
    querys = parse.parse_qs(parsed.query)
    querys = {k: v[0] for k, v in querys.items()}

    data = copy.deepcopy(querys)

    if body: data.update(body)

    str1 = secret_key
    for key in sorted(data.keys()):
        str1 += key + "=" + data[key]

    sha1str = sha1(str1.encode()).hexdigest()
    return base64.b64encode(f'20180111_android:{sha1str}'.encode()).decode()


测试一下 POST Form 请求,发现 hashMap 就是他的 body, 但是要当做 jaon 形式

image-20230220175312530
image-20230220175330153

最后更新于

这有帮助吗?