某某行逆向分析
某某行逆向分析
前言
设备: pixel2 android10
app: 6IuPZeih{beihai}jDMuM{beihai}jMuMQ==
正文
抓包
抓登录包,下面分析signature,random,password字段的编码加密算法

脱壳
使用开发者助手查看app信息,发现存在网易加固

这个app里面没有运行frida检测机制,所以直接运行脱壳脚本就好了
(function() {
function klog(data) {
var message = {};
message["jsname"] = "dump_dex";
message["data"] = data;
send(message);
}
function klogData(data, key, value) {
var message = {};
message["jsname"] = "dump_dex";
message["data"] = data;
message[key] = value;
send(message);
}
function get_self_process_name() {
var openPtr = Module.getExportByName('libc.so', 'open');
var open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
var readPtr = Module.getExportByName("libc.so", "read");
var read = new NativeFunction(readPtr, "int", ["int", "pointer", "int"]);
var closePtr = Module.getExportByName('libc.so', 'close');
var close = new NativeFunction(closePtr, 'int', ['int']);
var path = Memory.allocUtf8String("/proc/self/cmdline");
var fd = open(path, 0);
if (fd != -1) {
var buffer = Memory.alloc(0x1000);
var result = read(fd, buffer, 0x1000);
close(fd);
result = ptr(buffer).readCString();
return result;
}
return "-1";
}
function mkdir(path) {
var mkdirPtr = Module.getExportByName('libc.so', 'mkdir');
var mkdir = new NativeFunction(mkdirPtr, 'int', ['pointer', 'int']);
var opendirPtr = Module.getExportByName('libc.so', 'opendir');
var opendir = new NativeFunction(opendirPtr, 'pointer', ['pointer']);
var closedirPtr = Module.getExportByName('libc.so', 'closedir');
var closedir = new NativeFunction(closedirPtr, 'int', ['pointer']);
var cPath = Memory.allocUtf8String(path);
var dir = opendir(cPath);
if (dir != 0) {
closedir(dir);
return 0;
}
mkdir(cPath, 755);
chmod(path);
}
function chmod(path) {
var chmodPtr = Module.getExportByName('libc.so', 'chmod');
var chmod = new NativeFunction(chmodPtr, 'int', ['pointer', 'int']);
var cPath = Memory.allocUtf8String(path);
chmod(cPath, 755);
}
function dump_dex() {
var libart = Process.findModuleByName("libart.so");
var addr_DefineClass = null;
var symbols = libart.enumerateSymbols();
for (var index = 0; index < symbols.length; index++) {
var symbol = symbols[index];
var symbol_name = symbol.name;
//这个DefineClass的函数签名是Android9的
//_ZN3art11ClassLinker11DefineClassEPNS_6ThreadEPKcmNS_6HandleINS_6mirror11ClassLoaderEEERKNS_7DexFileERKNS9_8ClassDefE
if (symbol_name.indexOf("ClassLinker") >= 0 &&
symbol_name.indexOf("DefineClass") >= 0 &&
symbol_name.indexOf("Thread") >= 0 &&
symbol_name.indexOf("DexFile") >= 0) {
console.log(symbol_name, symbol.address);
addr_DefineClass = symbol.address;
}
}
var dex_maps = {};
var dex_count = 1;
console.log("[DefineClass:]", addr_DefineClass);
if (addr_DefineClass) {
Interceptor.attach(addr_DefineClass, {
onEnter: function(args) {
var dex_file = args[5];
//ptr(dex_file).add(Process.pointerSize) is "const uint8_t* const begin_;"
//ptr(dex_file).add(Process.pointerSize + Process.pointerSize) is "const size_t size_;"
var base = ptr(dex_file).add(Process.pointerSize).readPointer();
var size = ptr(dex_file).add(Process.pointerSize + Process.pointerSize).readUInt();
if (dex_maps[base] == undefined) {
dex_maps[base] = size;
var magic = ptr(base).readCString();
if (magic.indexOf("dex") == 0) {
var process_name = get_self_process_name();
if (process_name != "-1") {
var dex_dir_path = "/data/data/" + process_name + "/files/dump_dex_" + process_name;
mkdir(dex_dir_path);
var dex_path = dex_dir_path + "/class" + (dex_count == 1 ? "" : dex_count) + ".dex";
console.log("[find dex]:", dex_path);
var fd = new File(dex_path, "wb");
if (fd && fd != null) {
dex_count++;
var dex_buffer = ptr(base).readByteArray(size);
fd.write(dex_buffer);
fd.flush();
fd.close();
console.log("[dump dex]:", dex_path);
}
}
}
}
},
onLeave: function(retval) {}
});
}
}
function main() {
klogData("", "init", "dump_dex.js init hook success")
dump_dex();
}
setImmediate(main);
})();运行后到处点点,dump出尽可能多的dex

signature和random字段分析
把结果用jadx反编译。直接祭出一手字符串的大法搜索关键字"signature",看到不少可疑位置,但是hook了一圈之后好像没找到真正有关的。

于是换一种方法,直接hook原生库HashMap和JsonObject,代码如下:
Java.perform(function () {
// JSONObject的定位
var jSONObject = Java.use("org.json.JSONObject");
jSONObject.put.overload('java.lang.String', 'java.lang.Object').implementation = function (a, b) {
console.log("jSONObject.put: ", a, b);
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
return this.put(a, b);
};
jSONObject.getString.implementation = function (a) {
console.log("jSONObject.getString: ", a);
var result = this.getString(a);
console.log("jSONObject.getString result: ", result);
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
return result;
};
// HashMap的定位
var hashMap = Java.use("java.util.HashMap");
hashMap.put.implementation = function (a, b) {
console.log("hashMap.put: ", a, b);
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
return this.put(a, b);
};
})attach模式运行并把结果保存到txt文件中便于搜索。在结果中搜索signature。

这不就有了嘛,直接在反编译代码中搜索函数com.bwton.metro.network.strategy.impl.MSXLocalSignatureStrategy.generateSignature
可以看到sign是函数signReqJsonBody用公钥加密str得到的
random是函数encryptByPublicKey用公钥加密str2得到的

进入函数signReqJsonBody,发现这个只是在encryptByPublicKey前对字符串取了md5哈希而已

那进入encryptByPublicKey,可以看到这就是ecb模式的rsa算法

至此signature和random字段的算法明确了,接着就要看公钥和加密的参数是什么了,先hook函数generateSignature
代码如下:
function dump_hashMap(map){
var result = "";
if (map!== null) {
var keyset = map.keySet();
var it = keyset.iterator();
var first = true; // 用于判断是否是第一个键值对
while (it.hasNext()) {
var keystr = it.next().toString();
var valuestr = map.get(keystr).toString();
if (!first) {
result += "&"; // 如果不是第一个键值对,添加 & 分隔符
}
result += keystr + "=" + valuestr; // 拼接键值对,用 = 连接键和值
first = false; // 标记已经不是第一个键值对了
}
}
return result;
}
function hook_generateSignature(){
Java.perform(
function () {
var aClass = Java.use("com.bwton.metro.network.strategy.impl.MSXLocalSignatureStrategy");
aClass.generateSignature.implementation = function (a,b,c) {
console.log("headers========>", a);
console.log("str========>", b);
console.log("str2========>", c);
var ret = this.generateSignature(a,b,c);
console.log("ret========>", dump_hashMap(ret));
return ret;
}
}
)
}
attach模式运行,可以看到这个headers跟str2其实是一个东西,每次都不一样。str则是固定的json数据,而且包含了password

接着要搞清楚str2和公钥是什么
hook函数getPublicKey,打印结果,公钥好像不变

那么就剩下str2了,根据前面的调用栈向上追溯

可以看到这个str2是在headers中HEADER_AES_KEY字段,点击查看到HEADER_AES_KEY的值是headerAesKey。

显然这个headers是在之前就初始化过的,如果要顺着okhttp realcall对象的chain一路找过去根据会比较复杂。于是这里想到了个取巧的方法。想着既然str2是从header里获取headerAesKey对应的值,可能还是会涉及hashmap类,于是又在前面hook得到的调用栈结果中搜索headerAesKey。结果真的找到了

这里确实是添加了headerAesKey键值对,继续往前追溯查看键对应的值是什么

好家伙,好像连password字段都找到了,不过先继续看headerAesKey键对应的值。这个值是genAesKey,它是由函数genAesKey生成的

查看函数genAesKey,逻辑相当简单,就是从自定的字符表中随机挑出8个字符

那么用生成random的参数str2也知道了。于是signature和random就都解决了
password字段分析
password字段同样可以在前面的调用栈结果搜到,正是前面碰到的函数

![]()
进入Aes.encryptByECBPKCS5,这里使用AES算法的ECB 模式和PKCS5填充方式对字符串进行加密。str是明文,str2是key,这个key就是前面分析过的genAesKey

hook一下,打印参数和返回值,发现前面的分析没毛病

于是password的加密也解决了

有实力的老铁