篇幅有限
完整内容及源码关注公众号:ReverseCode,发送 冲
抓包
最右5.7.3.apk 启动charles,开始socks抓包
postern配置代理
postern配置规则
开启vpn开始抓包
Frida
jadx反编译搜索sign=
主动调用a方法,构造字符串和字节数组参数,打印返回结果
1 | function callSign(){ |
堆栈跟踪
1 | objection -g cn.xiaochuankeji.tieba explore -P ~/.objection/plugins |
除了使用objection,也可以通过frida脚本打印堆栈
1 | setImmediate(function(){ |
通过objection和frida堆栈跟踪,jeb打开搜索k46
byte[] v1_2 = NetCrypto.encodeAES(v3_4.toString().getBytes(Charset.forName("UTF-8")));
接下来hook NetCrypto.encodeAES并打印出入参
1 | setImmediate(function () { |
图中圈出的字符串即为V1_2
,拿到了协议的明文,主要逻辑就是通过aes加密后的字符串结合请求地址利用so中的sign函数完成签名校验。
rpc调用
1 | byte[] encodeAES = NetCrypto.encodeAES(jSONObject.toString().getBytes(Charset.forName("UTF-8"))); |
以上需要关注的函数有NetCrypto.encodeAES
,NetCrypto.a
,NetCrypto.getProtocolKey
,在NetCrypto.a
还有NetCrypto.sign
,返回结果是加密的需要NetCrypto.decodeAES
解密
zuiyouSign.js 对NetCrypto.sign
进行hook打印出入参
1 | function hookSign() { |
frida -UF cn.xiaochuankeji.tieba -l zuiyouSign.js 根据返回结果可以得知同一个请求每次返回的sign是一样的,第一个参数为请求地址。
callSignFunZy.js 主动调用入参中的的byte数组返回sign
1 | function callSignFunZy(str, buf) { |
hookSign.py
1 | from flask import Flask, jsonify |
python hookSign.py 发起请求127.0.0.1:1234/sign
获取sign
完整实现
Frida结合 Magisk Hide报错:try disabling Magisk Hide in case it is active
Magisk Manager > Settings >Magisk > Magisk Hide,用来隐藏ROOT,避免部分app检测ROOT
zuiyouSign.js 实现加密解密签名等函数的定义和导出
1 | // 加密 encodeAES函数的参数和返回值都是byte数组,转成hex字符串 |
python hookSign.py hook 通过frida rpc方式调用js中的hook
1 | #!/usr/bin/env python |
在源码中encodeAES的参数中jSONObject
的值由mt8.Z返回,所以我们通过objection内存搜索打印返回即可
1 | objection -g cn.xiaochuankeji.tieba explore -P ~/.objection/plugins |
python run.py
1 | from __future__ import print_function |
Xposed
Android Studio新建Empty Activity–OneJaneXposed,删除默认MainActivity和AndroidManifest.xml中的activity
AndroidManifest.xml
1 | <meta-data |
app\build.gradle
1 | dependencies { |
app\src\main\java\com\example\onejanexposed\HookLoader.java
1 | public class HookLoader implements IXposedHookLoadPackage { |
app\src\main\assets\xposed_init 配置xposed入口类
1 | com.example.onejanexposed.HookLoader |
在xposed中启动该模块即可
完整xposed实现
1 | public class HookLoader implements IXposedHookLoadPackage { |
build.gradle
1 | implementation 'org.apache.commons:commons-lang3:3.7' |
结合run.py修改请求地址为172.20.103.67:8888
,以上只是通过objection内存漫游拿到的参数借助xposed或者frida主动调用so层函数,实现在python的rpc调用sign参数的加密。
Unidbg
1 | public class ZuiYouSign extends AbstractJni { |
在JNIOnLoad时对函数进行了动态注册,当loadLibrary时不执行init相关函数,即第二个参数为false时输出结果乱码,因为so对字符串做了混淆加密,一般在Init array节或者JNIOnLoad,总之在字符串使用前的任何一个时机点都有可能。IDA打开libnet_crypto.so,shift+F7
找到节区中的init_array,解密逻辑就在图中的函数中,如果模拟执行时加载so不执行init相关函数,导致so字符串没有被解密,输出乱码。
public static { we5.a(ContextProvider.get(), "net_crypto"); NetCrypto.native_init(); }
加载完net_crypto后,执行native_init()
1 | runZyObj.initCall(); |
或者通过地址调用
1 | DalvikModule dm = vm.loadLibrary(new File("libnet_crypto.so"), true); // 加载so到虚拟内存 |
报错如下:
java.lang.UnsupportedOperationException: com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:426)
补环境重写 callStaticObjectMethodV
1 | @Override |
调用sign
1 | // 3、jadx中smali invoke-static {p0, p1}, Lcom/izuiyou/network/NetCrypto;->sign(Ljava/lang/String;[B)Ljava/lang/String; |
或者通过地址调用
1 | private String callSign(){ |
报错如下:
java.lang.UnsupportedOperationException: android/content/Context->getClass()Ljava/lang/Class;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:373)
补环境重写getClass
1 | @Override |
报错如下:
java.lang.UnsupportedOperationException: java/lang/Class->getSimpleName()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:373)getSimpleName 是个类名,根据之前的报错逻辑,
com/izuiyou/common/base/BaseApplication->getAppContext()
获取到Context后,通过android/content/Context->getClass()
获取到类,最终获取类名,之前补环境虽然只补了类型,只要最终返回的结果正确即可。根据objection内存漫游拿到完整类名就是cn.xiaochaunkeji.tieba.AppController
,getSimpleName就是AppController
或者通过frida主动调用静态函数获取getSimpleName的返回结果
1
2
3
4
5
6
7
8
9
10 setImmediate(function(){
Java.perform(function(){
var BaseApplication = Java.use("com.izuiyou.common.base.BaseApplication").getAppContext().getClass();
var methods = BaseApplication.class.getDeclaredMethods();
for(var j = 0; j < methods.length; j++){
var methodName = methods[j].getName();
console.log(methodName);
}
});
})遍历该类中的方法确实存在一个叫getSimpleName()
1
2
3
4
5
6 setImmediate(function(){
Java.perform(function(){
var BaseApplication = Java.use("com.izuiyou.common.base.BaseApplication").getAppContext().getClass();
console.log(BaseApplication.getSimpleName())
});
})返回结果就是
AppController
补环境
1 | @Override |
报错如下:
java.lang.UnsupportedOperationException: cn/xiaochuankeji/tieba/common/debug/AppLogReporter->reportAppRuntime(Ljava/lang/String;Ljava/lang/String;)V
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticVoidMethodV(AbstractJni.java:612)reportAppRuntime没有返回值,直接return
1 | @Override |
报错如下:
java.lang.UnsupportedOperationException: android/content/Context->getFilesDir()Ljava/io/File;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:373)读取文件直接返回java/io/File
1 | @Override |
报错如下:
java.lang.UnsupportedOperationException: java/lang/Class->getAbsolutePath()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:373)获取绝对路径,直接返回/sdcard
1 | @Override |
报错如下:
java.lang.UnsupportedOperationException: android/os/Debug->isDebuggerConnected()Z
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticBooleanMethodV(AbstractJni.java:169)判断是否调试,直接return false
1 | @Override |
报错如下:
java.lang.UnsupportedOperationException: android/os/Process->myPid()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticIntMethodV(AbstractJni.java:189)需要pid
1 | @Override |
算法还原
Hook
由于返回值固定为32位,且输入不变输出不变,可能是哈希算法中的md5算法,不过libnet_crypto.so
做了一定的OLLVM混淆,借助IDA-Edit-Plugins-FindHash对哈希算法进行正则匹配,对函数逐个反编译。
1
2
3
4
5
6
7 PYTHON_CONFIGURE_OPTS="--disable-ipv6" proxychains pyenv install 3.9.1
pyenv local 3.9.1
proxychains4 pip install objection==1.11.0
adb push frida-server-15.1.12-android-arm64 /data/local/tmp
mv frida-server-15.1.12-android-arm64 fs15112arm64
chmod 777 fs15112arm64
./fs15112arm64
frida -UF cn.xiaochuankeji.tieba -l libnet_crypto_findhash_1637807043.js 刷新页面触发sign生成
IDA快捷键 G 跳转到65540,F5跳转到伪C代码,
尝试hook该函数,这三个参数不确定是指针还是数值,所以先全部做为数值处理,作为long类型看待,防止整数溢出
1 | public void hook65540(){ |
打印入参
3221223188
7
3221223060
参数2应该是数组,参数1和3则像是地址
1 | public void hook65540(){ |
打印地址所指向的内存,类似frida中的dexdump,第二个参数总是和入参的长度一致,默认作为长度。
第一个参数就是我们传入的onejane
,即public static native String sign(String str, byte[] bArr)
第二个参数
HookZz在执行前,push保存参数,在后面再pop取出参数,我们观察下参数3
1 | public void hook65540(){ |
md5有四个iv,该函数中确实也有四个数值,通过H
键转成十六进制,和默认的iv值不一致,说明md5算法被魔改了。
将md5.py中的四个iv改成样本中的值
1 | # codeing=utf-8 |
msg>>>onejane
md5>>>abb01439f7c91b4a4aa431a4dee8aba0
返回的结果和unidbg中的Arg3的结果一致,AB B0 14 39 F7 C9 1B 4A 4A A4 31 A4 DE E8 AB A0 ...9...JJ.1.....
,因此可以断定,此处魔改确实只魔改了IV的md5算法。
主动调用
Unidbg
1 | public void callMd5(){ |
Frida
1 | function call_65540(base_addr){ |
总结
本文针对最右app的sign进行抽丝剥茧的分析,利用Frida的Hook及RPC主动调用,Xposed函数调用,Unidbg补环境模拟执行so等手段实现so逆向分析,最终得出核心算法通过魔改md5实现算法还原。