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"

Hook
查看包名
adb -s 8XV5T15A30005451 shell dumpsys window | grep mCurrentFocus

尝试直接 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
之后发现输出了堆栈

猜测可能出现的地方,因为 Authorization
在 headers
里面,所以我认为最可能的地方就是:
com.bk.base.netimpl.interceptor.f.intercept(HeaderInterceptor.java:109)
com.bk.net.interceptor.AuthInterceptor.intercept(AuthInterceptor.java:26)
我们先尝试用 jadx
反编译一下 app
, 找到第一个堆栈的地方,发现疑似就是加密的代码


我们直接尝试分析 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;
};
再次查看堆栈发现这个函数的返回值确实与请求头里面的保持一致:


所以其实加签的源码如下:
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
请求,且 Body
为 FormBody
, 则使用 signPostFormBody(builder, request)
加签;如果是 POST
请求,且 Body
为 MultipartBody
, 则使用 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;
}
分析一下加密逻辑:
解析
url
里面的params
后将其追加到map
里面,生成hashMap
对
hashMap
进行排序,将其转换为arrayList
获取
httpAppSecret
,生成StringBuilder sb
获取
httpAppId
赋值给str2
遍历
arrayList
,添加至sb
上将
sb
进行sha1
加密,生成SHA1ToString
base64
编码{str2}:{SHA1ToString}
获取 httpAppSecret
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;
};

获取 httpAppId
httpAppId
简单一点,知道原理之后直接 base64
解码就可以知道
httpAppId
是 20180111_android

算法还原
算法还原如下:
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
形式


最后更新于
这有帮助吗?