环境准备
复制 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
将 sb
进行 sha1
加密,生成 SHA1ToString
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;
};
获取 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
形式