简单记录一下~
版本信息:2.71.0
目标API:
用于查询二手房信息
1
| https://apps.api.ke.com/house/ershoufang/detailpart0v1?houseCode={houseCode}&cityId={city_id}&fb_expo_id={fb_expo_id}
|
去除掉无用字段的headers
如下:
字段名称 | 示例 | 释义 |
---|
Host | apps.api.ke.com | |
Cookie | lianjia_udid=0123456789123456; | cookie中测试必须包含lianjie_udid,一个随机的16长度的字符串就可以 |
referer | homepage1 | |
lianjia-version | 2.71.0 | |
user-agent | Beike2.71.0;google Pixel+3; Android 12 | |
authorization | | 签名字段 |
device-id-s | 65e11ee695f…. | 设备ID |
channel-s | Android_ke_oppo | |
appinfo-s | Beike;2.71.0;2710100 | |
hardware-s | google;Pixel 3 | |
systeminfo-s | android;12 | |
wll-kgsa | LJAPPVA accessKeyId=sjoe98HI099dhdD7; nonce=Xvuq0VOC6JJ7s4Qwz7ZRtI2OEocaEkfF; timestamp=1745485231; signedHeaders=Device-id-s,AppInfo-s,User-Agent,Hardware-s,Channel-s,SystemInfo-s; signature=6Bc6i9srrjNug3w3Wt5xv4MzDs29zOdXkW2g+BKv0lM= | 签名字段 |
核心字段有两个:
wll-kgsa
这个参数之前是不校验的,大概是25.3月份左右开始检验了
authorization
最早只校验这个参数
二、wll-kgsa分析
老规矩,准备好jadx-gui以及frida和objection,涉及到so层的算法,还需要准备unidbg以及ida pro。
2.0 初步分析wll-kgsa值的组成
这是一个完整的wll-kgsa值示例,我在;
位置做了换行处理,看起来容易些。
1 2 3 4 5
| LJAPPVA accessKeyId=sjoe98HI099dhdD7; nonce=Xvuq0VOC6JJ7s4Qwz7ZRtI2OEocaEkfF; timestamp=1745485231; signedHeaders=Device-id-s,AppInfo-s,User-Agent,Hardware-s,Channel-s,SystemInfo-s; signature=6Bc6i9srrjNug3w3Wt5xv4MzDs29zOdXkW2g+BKv0lM=
|
看到signature和signedHeaders就可以大胆猜测一下signature这就是算法的核心计算结果,signedheaders和算法入参有关。
2.1 反编译APP寻找wll-kgsa生成位置
这里浪费了不少时间,因为直接在jadx中全局搜索wll-kgsa是找不到的,可以通过搜索LJAPPVA
来定位,如图先定位到:

然后定位到:

最后定位到:

很显然,这是so层的算法,java层的定位只能到这了,注意这里它返回值是一个字符串数组,我们回过头来再看一下调用secmanager.sign
的调用的位置com.ke.infrastructure.app.signature.algorithm.V1SignAlgorithm.sign
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public String[] sign(String str, String str2, Long l) { ... try { String[] sign = SecManager.sign(str, str2, l.toString()); if (sign != null && sign.length > 0) { String lowerCase = sign[0].toLowerCase(); ... byte[] hexStrToByteArray = hexStrToByteArray(lowerCase); ... sign[0] = Base64.encodeToString(hexStrToByteArray, 2); ... } return sign; ... }
|
我省略了部分代码,可以看到SecManager.sign
调用后结果的第一个字符串是一个16进制字符串,然后又进行了base64编码。
然后objection hook一下com.ke.infrastructure.app.signature.algorithm.V1SignAlgorithm.sign
,看一下调用参数以及返回值:
1 2 3
| (agent) [271610] Called com.ke.infrastructure.app.signature.algorithm.V1SignAlgorithm.sign(java.lang.String, java.lang.String, java.lang.Long) (agent) [271610] Arguments com.ke.infrastructure.app.signature.algorithm.V1SignAlgorithm.sign(accessKeyId=sjoe98HI099dhdD7&appinfo-s=Beike;2.71.0;2710100&channel-s=Android_ke_oppo&device-id-s=65e11ee695fe28b9;;020102kHHw3zUzxashorD9kotvrTgmN3s4pBpRzP5X89WHGGYwiMirFdoTwCnFFmq0Q1Qm7RJQc81PfJEu7cpNRq/dpQ==&hardware-s=google;Pixel 3&host=skdg.ke.com&method=POST&nonce=2Z6lmwTxCM5jcIrwkOqgZaPU7BNRp3O3&path=/up/q&signedHeaders=Device-id-s,AppInfo-s,User-Agent,Hardware-s,Channel-s,SystemInfo-s&systeminfo-s=android;12×tamp=1742462622&user-agent=okhttp/3.12.6, sjoe98HI099dhdD7, 1742462622) (agent) [271610] Return Value: q99+bfKC6EV/sD+kbewV4tGB7ehYLIufjFBbaOqGnOc=,28B22DAC5AAD6BFBDBFD2DBB4F40E878,accessKeyId=sjoe98HI099dhdD7&appinfo-s=Beike;2.71.0;2710100&channel-s=Android_ke_oppo&device-id-s=65e11ee695fe28b9;;020102kHHw3zUzxashorD9kotvrTgmN3s4pBpRzP5X89WHGGYwiMirFdoTwCnFFmq0Q1Qm7RJQc81PfJEu7cpNRq/dpQ==&hardware-s=google;Pixel 3&host=skdg.ke.com&method=POST&nonce=2Z6lmwTxCM5jcIrwkOqgZaPU7BNRp3O3&path=/up/q&signedHeaders=Device-id-s,AppInfo-s,User-Agent,Hardware-s,Channel-s,SystemInfo-s&systeminfo-s=android;12×tamp=1742462622&user-agent=okhttp/3.12.6,sjoe98HI099dhdD7,1742462622++q99+bfKC6EV/sD+kbewV4tGB7ehYLIufjFBbaOqGnOc=,sjoe98HI099dhdD7uioefhli9847684328B22DAC5AAD6BFBDBFD2DBB4F40E8781742462622,FA760E3A2B4DDC499B55EA31001EF21D++abdf7e6df282e8457fb03fa46dec15e2d181ede8582c8b9f8c505b68ea869ce7++-85-33126109-14-126-2469127-8063-92109-2021-30-47-127-19-248844-117-97-1168091104-22-122-100-25,FA760E3A2B4DDC499B55EA31001EF21D,ABDF7E6DF282E8457FB03FA46DEC15E2D181EDE8582C8B9F8C505B68EA869CE7
|
得到一组入参和出参:
- 入参
1 2 3
| accessKeyId=sjoe98HI099dhdD7&appinfo-s=Beike;2.71.0;2710100&channel-s=Android_ke_oppo&device-id-s=65e11ee695fe28b9;;020102kHHw3zUzxashorD9kotvrTgmN3s4pBpRzP5X89WHGGYwiMirFdoTwCnFFmq0Q1Qm7RJQc81PfJEu7cpNRq/dpQ==&hardware-s=google;Pixel 3&host=skdg.ke.com&method=POST&nonce=2Z6lmwTxCM5jcIrwkOqgZaPU7BNRp3O3&path=/up/q&signedHeaders=Device-id-s,AppInfo-s,User-Agent,Hardware-s,Channel-s,SystemInfo-s&systeminfo-s=android;12×tamp=1742462622&user-agent=okhttp/3.12.6 sjoe98HI099dhdD7 1742462622
|
- 出参
1
| q99+bfKC6EV/sD+kbewV4tGB7ehYLIufjFBbaOqGnOc=,28B22DAC5AAD6BFBDBFD2DBB4F40E878,accessKeyId=sjoe98HI099dhdD7&appinfo-s=Beike;2.71.0;2710100&channel-s=Android_ke_oppo&device-id-s=65e11ee695fe28b9;;020102kHHw3zUzxashorD9kotvrTgmN3s4pBpRzP5X89WHGGYwiMirFdoTwCnFFmq0Q1Qm7RJQc81PfJEu7cpNRq/dpQ==&hardware-s=google;Pixel 3&host=skdg.ke.com&method=POST&nonce=2Z6lmwTxCM5jcIrwkOqgZaPU7BNRp3O3&path=/up/q&signedHeaders=Device-id-s,AppInfo-s,User-Agent,Hardware-s,Channel-s,SystemInfo-s&systeminfo-s=android;12×tamp=1742462622&user-agent=okhttp/3.12.6,sjoe98HI099dhdD7,1742462622++q99+bfKC6EV/sD+kbewV4tGB7ehYLIufjFBbaOqGnOc=,sjoe98HI099dhdD7uioefhli9847684328B22DAC5AAD6BFBDBFD2DBB4F40E8781742462622,FA760E3A2B4DDC499B55EA31001EF21D++abdf7e6df282e8457fb03fa46dec15e2d181ede8582c8b9f8c505b68ea869ce7++-85-33126109-14-126-2469127-8063-92109-2021-30-47-127-19-248844-117-97-1168091104-22-122-100-25,FA760E3A2B4DDC499B55EA31001EF21D,ABDF7E6DF282E8457FB03FA46DEC15E2D181EDE8582C8B9F8C505B68EA869CE7
|
可以看出这个函数的返回值的一个值就是我们要的wll-kgsa中的signature
,下面就可以使用unidb模拟调用一下secmanager.sign
函数
2.2 使用unidbg模拟调用核心函数
前人栽树后人乘凉,大多时候我们能搞定某些样本不是自己有多厉害,而是站了在前辈的肩膀上。
unidbg具体用法就不多说了,搭好架子就可以开始patch了,常规的检测点,哪里报错补哪里就行,以下是一部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| @Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature) { case "android/content/Context->getApplicationInfo()Landroid/content/pm/ApplicationInfo;": { return new ApplicationInfo(vm); } case "java/lang/Class->getPackage()Ljava/lang/Package;": { DvmObject<?> e = vm.resolveClass("java/lang/Package").newObject(null); return e; } case "java/lang/Package->getName()Ljava/lang/String;": { return new StringObject(vm, "com.lianjia.beike"); } case "java/io/File->getCanonicalPath()Ljava/lang/String;":{ return new StringObject(vm, "/data/data/com.lianjia.beike/files"); } } return super.callObjectMethodV(vm, dvmObject, signature, vaList); }
@Override public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature){ case "com/ke/securitylib/SecManager->getCertificateMD5Digest()Ljava/lang/String;":{ // frida hook一下真机的值 return new StringObject(vm, "28B22DAC5AAD6BFBDBFD2DBB4F40E878"); } // 这里调用了java层的MD5函数,需要自己实现一下 case "com/ke/infrastructure/app/signature/util/MD5Utils->getMd5(Ljava/lang/String;)Ljava/lang/String;":{ String s = (String) vaList.getObjectArg(0).getValue(); MD5Util md5Util = new MD5Util(); String md5Result = md5Util.getMd5(s); return new StringObject(vm, md5Result); } } return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList); } }
|
有个比较麻烦的地方单独说一下,它内部调用了libc的free函数,这个函数得hook掉,我这里用的是xhook框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| private void handleFree(){ IxHook xHook = XHookImpl.getInstance(emulator); xHook.register("libkmsec.so", "free", new ReplaceCallback() { @Override public HookStatus onCall(Emulator<?> emulator, long originFunction) { // return new HookStatus(0, 0, false); // e =HookStatus.LR(emulator, 0); // return HookStatus.LR(emulator, 0); System.out.println("hook free");
return HookStatus.LR(emulator, 0); } @Override public void postCall(Emulator<?> emulator, HookContext context) { } }); xHook.refresh();
}
|

最后看一下结果:
结果的第一个值和之前分析的一样,是一个16进制字符串,转成base64就是最后的signature
了

2.5 还原算法
前面补unidbg的时候我们补过一个md5函数,还有一个getCertificateMD5Digest
函数调用,那就接着大胆猜测这是hmac md5算法。接着调出新时代逆向神器deepseek
。直接把unidbg运行时的日志输出丢给deepseek
就可以得到完整的算法了

三、authorization分析
说实话,这个我没这个版本中找到生成位置,但之前有搞过这个APP,试一了一下之前的算法也是可以用的,就是把请求参数排序后加个salt进行sha1,然后base64就出来了。

四、展示结果
