moji天气逆向及unidbg实现
Java层

还挺多结果的,不过忽略掉meizu,huawei这些第三方sdk里面的东西后,还是很容易看出来的。
com.moji.requestcore.encrypt.DefaultEncryptParamImpl.encrypt
com.moji.requestcore.encrypt.DefaultEncryptParamImpl.a

com.moji.mjweather.library.Digest.encodeParams

lte.NCall.IL

其实libGameVMP.so对我来说不陌生,之前在看某物 dewu app so newSign 参数分析破解这篇文章的时候,因为它奇葩的特性所以记住了。所以在moji这个app是不是也一样?
先hook一下com.moji.mjweather.library.Digest这个类
android hooking watch class com.moji.mjweather.library.Digest

可以看到每次调用encodeParams的时候,nativeEncodeParams都被调用了。
再hook一下nativeEncodeParams这个函数。
android hooking watch class_method com.moji.mjweather.library.Digest.nativeEncodeParams --dump-args --dump-return
从hook结果可以看到最后确实是调用它生成sign。

这么多个so,哪个才是调用加密的呢?从名字看libencrypt.so和libEncryptor.so最可疑。
jnitrace看看。
jnitrace.exe -l libencrypt.so -l libEncryptor.so com.moji.mjweather|tee trace-moji.txt
然后尝试在记录里搜索某个签名

可以看到最终是在libencrypt.so生成sign的。
so层
从地址可以看出so是64位的,因为手机和apk都支持64位指令,接下来的分析也是针对64位。
打开arm64-v8a下的libencrypt.so,函数窗口搜索java

说明是动态注册的,看看JNI_OnLoad

跳转过去

继续跳转

所以真实函数名是parseParams
第二种方法是frida_hook_libart
frida -U -f com.moji.mjweather -l hook_RegisterNatives.js --no-pause

然后跳转到0x3d1a0

另一种方法就是用jnitrace
jnitrace -l libencrypt.so -i RegisterNatives com.moji.mjweather

函数地址0x728b2861a0减去so基地址0x728b249000等于0x3d1a0,跳转过去同样也是parseParams。

算法其实挺简单的,就是后面拼接个字符串,然后做个MD5就出结果了,可以自行hook MD5Update函数进行验证,这里就省略了。
unidbg实现
由于unidbg可以自由选择32还是64位,这里选择的是32位的。先搭个框架
public class Moji extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public static String pkgName = "com.moji.mjweather";
public static String apkPath = "unidbg-android/src/test/java/com/moji/moji90300.apk";
public static String soPath = "";
public Moji() {
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName(pkgName).build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File(apkPath));
vm.setJni(this);
vm.setVerbose(true);
DalvikModule dm = vm.loadLibrary("encrypt", true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
public static void main(String[] args) {
Moji test = new Moji();
}
}
开始报错和补函数。

@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "com/moji/tool/AppDelegate->getAppContext()Landroid/content/Context;": {
return vm.resolveClass("android/content/Context").newObject(null);
}
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}

@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature) {
case "android/app/ActivityThread->sCurrentActivityThread:Landroid/app/ActivityThread;": {
return vm.resolveClass("android/app/ActivityThread").newObject(null);
}
}
return super.getStaticObjectField(vm, dvmClass, signature);
}

不知道怎么补,开始摆烂
public void setStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature, DvmObject<?> value) {
switch (signature) {
case "android/app/ActivityThread->sPackageManager:Landroid/content/pm/IPackageManager;": {
}
}
}

case "android/os/ServiceManager->getService(Ljava/lang/String;)Landroid/os/IBinder;": {
return vm.resolveClass("android/os/IBinder").newObject(null);
}

越来越离谱,一怒之下,决定不补了,尝试直接patch。

跳转到之前获取PackageManager的地方,也就是0x11315


可以看出getPublicKey这个函数主要起获取签名的作用,X查看查看引用

看看checkPubKey

可以看出校验成功会返回1,失败返回0。
查看checkPubKey的引用

可以看到在关键函数中都有调用,先看看checkKeyOnLoad(因为对它查看引用的话,可以看到它在JNI_OnLoad被调用了)


直接修改0x117A2这条指令,把返回值,也就是r0的值改为1

public void patchVerify(){
emulator.getMemory().pointer(module.base+0x117A2).setInt(0, 0x4ff00100);
}
public Moji() {
// ...
this.patchVerify();
dm.callJNI_OnLoad(emulator);
}
再次跑起来。

JNI_OnLoad完成了,接下来是正式调用。
public void call_sign() {
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(0);
list.add(vm.addLocalObject(new StringObject(vm, "1")));
Number ret = module.callFunction(emulator, 0x21a9d, list.toArray());
System.out.println(vm.getObject(ret.intValue()).getValue().toString());
}
public static void main(String[] args) {
Moji test = new Moji();
test.call_sign();
}

又报错了,这是因为之前的checkPubKey在parseParams中也被调用了


同样的,patch这条指令
public void patchVerify(){
emulator.getMemory().pointer(module.base+0x117A2).setInt(0, 0x4ff00100);
emulator.getMemory().pointer(module.base+0x21AF4).setInt(0, 0x4ff00100);
}
重新运行,结果就出来了

完整代码
package com.moji;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
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.memory.Memory;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class Moji extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public static String pkgName = "com.moji.mjweather";
public static String apkPath = "unidbg-android/src/test/java/com/moji/moji90300.apk";
public static String soPath = "";
public Moji() {
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName(pkgName).build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File(apkPath));
vm.setJni(this);
vm.setVerbose(true);
DalvikModule dm = vm.loadLibrary("encrypt", true);
module = dm.getModule();
this.patchVerify();
dm.callJNI_OnLoad(emulator);
}
public void patchVerify(){
emulator.getMemory().pointer(module.base+0x117A2).setInt(0, 0x4ff00100);
emulator.getMemory().pointer(module.base+0x21AF4).setInt(0, 0x4ff00100);
}
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "com/moji/tool/AppDelegate->getAppContext()Landroid/content/Context;": {
return vm.resolveClass("android/content/Context").newObject(null);
}
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}
public void call_sign() {
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(0);
list.add(vm.addLocalObject(new StringObject(vm, "1")));
Number ret = module.callFunction(emulator, 0x21a9d, list.toArray());
System.out.println(vm.getObject(ret.intValue()).getValue().toString());
}
public static void main(String[] args) {
Moji test = new Moji();
test.call_sign();
}
}
总结
- patch一时爽,一直patch一直爽。不过有哪位补函数的高手能完整补出来吗,想学习学习
- 有没有方法能让64位手机,64位app运行在32位环境,习惯了32位环境,分析64位不太习惯
Update 2022-02-09
Android adb安装时强制应用App以32位或者64位运行
指定应用在64位终端下以32位方式模式下运行
adb install --abi armeabi-v7a <path to apk>