声明
本文章中所有内容仅供学习交流,不可用于任何商业用途和非法用途,否则后果自负,如有侵权,请联系作者立即删除!由于本人水平有限,如有理解或者描述不准确的地方,还望各位大佬指教!!
前言
今天我们要分析的app是安居客。样本下载
2025安居客v17.17.5老旧历史版本安装包官方免费下载_豌豆荚
java层分析
第一步抓包分析。通过抓包发现nsign是加密的。
使用jadx打开,搜索nsign
跟到最后发现是so层的
我们先逐个参数分析一下
参数1是url的path
参数2是请求头转成字节然后经过a函数处理
我们把a函数翻译成Python
def byte_to_str(bArr: bytes):
cArr = "0123456789abcdef"
digest = md5(bArr).digest()
sb = []
for byte in digest:
sb.append(cArr[(byte & 240) >> 4])
sb.append(cArr[byte & 15])
return ''.join(sb)
print(byte_to_str('{"condition":{"commId":"730320","key":"","value":""},"pageNum":1,"pageSize":15}'.encode("utf-8")))
参数3是params,把value转成了字节
参数3是uuid
参数4是参数2的长度
hook一下入参和结果看看
str=/ajkspec/esf/news/cross/community/comments, bArr=123,34,99,111,110,100,105,116,105,111,110,34,58,123,34,99,111,109,109,73,100,34,58,34,55,51,48,
51,50,48,34,44,34,107,101,121,34,58,34,34,44,34,118,97,108,117,101,34,58,34,34,125,44,34,112,97,103,101,78,117,109,34,58,49,44,34,112,97,103,101,83,105,122,101,34,58,49,5
3,125,
map={"app":"a-ajk","_guid":"74d1a658-d258-4fc0-a5b8-41582f01d04d","version_code":"322347","m":"Android-MI 9","uuid":"89749bd1-f403-4ac4-823b-fdd1dcbee7fa-KEe4VfU",
"openudid":"guest6c99fa49-a4d0-4e9b-824c-ab94eb041e68-rSOeUfU","manufacturer":"Xiaomi","o":"cepheus-user 11 RKQ1.200826.002 V12.5.6.0.RFACNXM release-keys","qtime":"20250
315164940","gmid":"guest6c99fa49-a4d0-4e9b-824c-ab94eb041e68-rSOeUfU","cv":"17.17.5","v":"11","58clientid":"8d8f7ddfb3d4767c754156d4f77383cc","ajk_city_id":"18","from":"mobile","pm":"b276","cid":""},
str2=4126b0de-0645-4ca7-8e59-9de4f5cd01fc
result=100043f8420e90baf10e220bafdd3c2fd42301a80011004f4126b0de
so分析
进入so层看看 这里应该就是加密的主流程了,接下来就用unidbg进行算法辅助分析,算法比较简单也可以直接frida进行hook
package com.anjuke.mobile.sign;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ArrayObject;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class nsign extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public nsign() {
emulator = AndroidEmulatorBuilder
.for64Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName("com.anjuke.android.app")
.build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("E:\\apk\\安居客\\anjuke_17.17.5.apk"));
vm.setJni(this);
vm.setVerbose(true);
DalvikModule dm = vm.loadLibrary(new File("E:\\apk\\安居客\\libsignutil.so"), true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
public void callSign() {
List<Object> list = new ArrayList<Object>();
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第⼆个参数,实例⽅法是jobject,静态⽅法是jclass,直接填0,⼀般⽤不到。
list.add(vm.addLocalObject(new StringObject(vm, "/ajkspec/esf/news/cross/community/comments")));
list.add(vm.addLocalObject(new StringObject(vm, "e17228f712db5be45a57987ca1730c1d")));
Map<String, String> paramMap = new HashMap<>() {{
put("app", "a-ajk");
put("_guid", "74d1a658-d258-4fc0-a5b8-41582f01d04d");
put("version_code", "322347");
put("m", "Android-MI 9");
put("uuid", "89749bd1-f403-4ac4-823b-fdd1dcbee7fa-KEe4VfU");
put("openudid", "guest6c99fa49-a4d0-4e9b-824c-ab94eb041e68-rSOeUfU");
put("manufacturer", "Xiaomi");
put("o", "cepheus-user 11 RKQ1.200826.002 V12.5.6.0.RFACNXM release-keys");
put("qtime", "20250315164940");
put("gmid", "guest6c99fa49-a4d0-4e9b-824c-ab94eb041e68-rSOeUfU");
put("cv", "17.17.5");
put("v", "11");
put("58clientid", "8d8f7ddfb3d4767c754156d4f77383cc");
put("ajk_city_id", "18");
put("from", "mobile");
put("pm", "b276");
put("cid", "");
}};
Map<String, byte[]> map = new HashMap<>();
for (String key : paramMap.keySet()) {
map.put(key, paramMap.get(key).getBytes(StandardCharsets.UTF_8));
}
list.add(vm.addLocalObject(ProxyDvmObject.createObject(vm, map)));
list.add(vm.addLocalObject(new StringObject(vm, "3b8ed218-5dd5-4190-b367-bbb83abdca60")));
list.add(79);
Number number = module.callFunction(emulator, 0x18F0, list.toArray());
String result = vm.getObject(number.intValue()).getValue().toString();
System.out.println("result=" + result);
}
@Override
public int callIntMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature) {
case "java/util/HashMap->size()I":
Map<?, ?> map = (Map<?, ?>) dvmObject.getValue();
return map.size();
}
return super.callIntMethod(vm, dvmObject, signature, varArg);
}
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature) {
case "java/util/HashMap->keySet()Ljava/util/Set;":
Map<?, ?> map = (Map<?, ?>) dvmObject.getValue();
return vm.resolveClass("java/util/Set").newObject(map.keySet());
case "java/util/Set->toArray()[Ljava/lang/Object;":
Set<?> set = (Set<?>) dvmObject.getValue();
Object[] array = set.toArray();
DvmObject<?>[] objects = new DvmObject[array.length];
for (int i = 0; i < array.length; i++) {
if (array[i] instanceof String) {
// 如果元素是字符串,包装为 StringObject
objects[i] = new StringObject(vm, (String) array[i]);
}
}
return new ArrayObject(objects);
case "java/util/HashMap->get(Ljava/lang/Object;)Ljava/lang/Object;":
HashMap<?, ?> map1 = (HashMap<?, ?>) dvmObject.getValue();
Object key = varArg.getObjectArg(0).getValue();
Object value = map1.get(key);
if (value instanceof byte[]) {
return new ByteArray(vm, (byte[]) value);
}
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
public static void main(String[] args) {
nsign nsign = new nsign();
nsign.callSign();
}
}
我们hook一下get_sign这个函数的入参和出参
这正是结果的前半段部分,那么结果就是:get_sign的结果+未知的10字节
我们跟进去get_sign函数
对md5进行hook
可以看到参数1就是加盐+path+str1+map,对其进行md5加密
1000 43f8420e90baf10e220bafdd3c2fd423 01a8 0011 004f 3b8ed218
1000 43f8920e90baf10e220bafdd3c2fd423
可以看到第3个字节的第一个字符串不同
回去看伪代码发现最后一行进行了处理 v37[*v37 & 0xF] = *v37;
*v37是v37的第一个字节,既v37[0] = '4',对应的ASCll值为0x34,0x34&0x0F=0x04,既v37[4] = '4',所以9变成了4
前面四个是固定字符串,可以写死,接下来就只剩下后面的10字节了
我们回到so看看伪代码
这部分转成python就是
def get_hex_char(value):
hex_chars = "0123456789abcdef"
char1 = hex_chars[value >> 12]
char2 = hex_chars[(value >> 8) & 0xF]
char3 = hex_chars[(value >> 4) & 0x0F]
char4 = hex_chars[value & 0xF]
return char1 + char2 + char3 + char4
最后四个字节就是uuid的前八位
到此整个算法就分析完成了。
结言
这是比较简单的app了,适合新手入门分析so,这里的源码已经放在了星球,欢迎大家跟我一起讨论,同时星球会丢更多的一些辅助工具。