文章目录

  • 一、前言
  • 二、准备
  • 三、Unidbg模拟执行
  • 四、算法还原
  • 五、尾声

一、前言

这是SO逆向入门实战教程的第三篇,上篇的重心是Unidbg的Hook使用,本篇的重点是如何在Unidbg中补齐JAVA环境以及哈希算法的魔改。

  • 侧重新工具、新思路、新方法的使用,算法分析的常见路子是Frida Hook + IDA ,在本系列中,会淡化Frida 的作用,采用Unidbg Hook + IDA 的路线。
  • 主打入门,但并不限于入门,你会在样本里看到有浅有深的魔改加密算法、以及OLLVM、SO对抗等内容。
  • 对样本的分析仅限于学习和研究,坚决抵制黑灰产。
  • 一共十三篇,1-2天更新一篇。每篇的资料放在文末的百度网盘中。

二、准备


sign方法就是我们的目标方法,参数1是字符串,参数2是字符串的字节数组。我们设参数1是为“12345”,参数2为 “r0ysue”,在Frida中主动调用测试返回结果:

function callSign(){Java.perform(function () {var NetCrypto = Java.use("com.izuiyou.network.NetCrypto");var JavaString = Java.use("java.lang.String");var plainText = "r0ysue";var plainTextBytes = JavaString.$new(plainText).getBytes("UTF-8");var result = NetCrypto.a("12345", plainTextBytes);console.log(result);});
}


多次变换入参可以验证,输出有如下特征

  • 输出:参数1+"?"+“sign=v2-”+32位字符串
  • 输入不变则输出不变

三、Unidbg模拟执行

需要注意的是,在执行sign函数前需要先执行native_init函数。

老规矩,先搭一下架子

package com.right;import com.github.unidbg.linux.android.dvm.AbstractJni;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;public class zuiyou extends AbstractJni{private final AndroidEmulator emulator;private final VM vm;private final Module module;zuiyou() {emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.xiaochuankeji.tieba").build(); // 创建模拟器实例final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\zuiyou\\right573.apk")); // 创建Android虚拟机DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\zuiyou\\libnet_crypto.so"), true); // 加载so到虚拟内存module = dm.getModule(); //获取本SO模块的句柄vm.setJni(this);vm.setVerbose(true);dm.callJNI_OnLoad(emulator);};public static void main(String[] args) throws Exception {zuiyou test = new zuiyou();}
}


可以看到,在JNIOnLoad中做了函数的动态注册。

此处有个值得一提的问题,如果在加载so到虚拟内存的步骤中,参数二设为false(即不执行init相关函数),会出现有趣的一幕。

我们发现,输出竟然”乱码“了,如果参数2为false即”乱码“,true就”不乱码“,这是为什么呢?甚至有人在论坛发帖求助类似问题:

[求助]Unidbg的Jnionload 加载出的类是乱码-¥付费问答-看雪论坛-安全社区|安全招聘|bbs.pediy.com 。

其实其中的道理并不复杂,甚至可以说很简单——SO样本做了字符串的混淆或加密,以此来对抗分析人员,但字符串总是要解密的,不然怎么用呢?这个解密一般发生在Init array节或者JNI OnLoad中,又或者是该字符串使用前的任何一个时机,而本例呢,就发生在Init array节中,Shift+F7快捷键查看节区验证这一点

我们可以看到,Init array节内有大量函数,解密就发生在其中。当我们使用Unidbg模拟执行时,如果加载SO时配置为不执行Init相关函数,这导致整个SO中的字符串都没有被解密,自然输出就是一团”乱码“。

由此还可以衍生出一个小话题——如果样本中的字符串被加密了,如何还原?使得分析者可以愉快的用IDA静态分析?

  • 从内存中Dump出解密后的SO或者字符串(可以用Frida/IDA 脚本/ adb 等),将结果回填或者说修复本身SO。
  • 使用Unicorn或基于Unicorn的模拟执行工具(Unidbg、ExAndroidNativeemu等)运行SO,dump解密后的虚拟内存,回填修复SO。

言归正传,接下来执行我们的目标函数,如图这两个函数。

首先是native_init函数,有过前两篇的基础,就不在此处多费口舌了,看一下更新后的代码

package com.right;import com.github.unidbg.linux.android.dvm.AbstractJni;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;public class zuiyou extends AbstractJni{private final AndroidEmulator emulator;private final VM vm;private final Module module;zuiyou() {emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.xiaochuankeji.tieba").build(); // 创建模拟器实例final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\zuiyou\\right573.apk")); // 创建Android虚拟机DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\zuiyou\\libnet_crypto.so"), true); // 加载so到虚拟内存module = dm.getModule(); //获取本SO模块的句柄vm.setJni(this);vm.setVerbose(true);dm.callJNI_OnLoad(emulator);};public void native_init(){// 0x4a069List<Object> list = new ArrayList<>(10);list.add(vm.getJNIEnv()); // 第一个参数是envlist.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。module.callFunction(emulator, 0x4a069, list.toArray());};public static void main(String[] args) throws Exception {zuiyou test = new zuiyou();test.native_init();}
}

运行,肉眼可见的报错

让我们用Unidbg的口吻来翻译一下这个报错:
我在通过 callStaticObjectMethodV 方法调用JAVA函数时,遇到一个签名叫做**com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;**的函数,我不知道怎么处理,你可以立刻到AbstractJni.java:401行上面进行查看和处理。

可以看到,一些常见的、系统的Java类和方法,Unidbg作开发者已经做了处理,但不常使用的类库以及自定义Java类显然不在此列,所以需要我们像它内置的这些方法一样,把报错的方法补进去。

接下来开始补环境,考虑两个问题

  • 怎么补
  • 补什么

关于第一点,我们既可以根据报错提示,在AbstractJni对应的函数体内,依葫芦画瓢,case "xxx“。


也可以在我们的 zuiyou 类中补,因为zuiyou类继承了AbstractJNI。

关于补法,有两种实践方法都很有道理

  • 全部在用户类中补,防止项目迁移或者Unidbg更新带来什么问题,这样做代码的移植性比较好。
  • 样本的自定义JAVA方法在用户类中补,通用的方法在AbstractJNI中补,这样做的好处是,之后运行的项目如果调用通用方法,就不用做重复的修补工作。

读者可以自行选择,我这边全部写在用户类中,方便演示, 在zuiyou类中重写callStaticObjectMethodV方法

    @Overridepublic DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {switch (signature) {case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;":System.out.println("TODO");}return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);}

第二个问题是补什么,从签名中可以看出,返回值是Landroid/content/Context;,即一个context对象,那我们传入一个最基本的context。

    @Overridepublic DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {switch (signature) {case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;":return vm.resolveClass("android/content/Context").newObject(null);}return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);}

这肯定是不够用的,但没办法,只能一步一步来,就好比贵公子需要出去度假,Android系统可以提供给他一条豪华游轮,但我们的虚拟系统没法给他那么多,我们就先提供一条木船。这条小船和尊贵的客人一起出发,客人会不断去船里索取物资,他要什么,我们再补什么!我们只关注最后贵公子取的东西是什么,这个东西一定要按照豪华游轮的标准去给他,前面的汤汤水水应付完事。

看一下完整代码和运行效果

package com.right;import com.github.unidbg.linux.android.dvm.AbstractJni;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;public class zuiyou extends AbstractJni{private final AndroidEmulator emulator;private final VM vm;private final Module module;zuiyou() {emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.xiaochuankeji.tieba").build(); // 创建模拟器实例final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\zuiyou\\right573.apk")); // 创建Android虚拟机DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\zuiyou\\libnet_crypto.so"), true); // 加载so到虚拟内存module = dm.getModule(); //获取本SO模块的句柄vm.setJni(this);vm.setVerbose(true);dm.callJNI_OnLoad(emulator);};public void native_init(){// 0x4a069List<Object> list = new ArrayList<>(10);list.add(vm.getJNIEnv()); // 第一个参数是envlist.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。module.callFunction(emulator, 0x4a069, list.toArray());};@Overridepublic DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {switch (signature) {case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;":return vm.resolveClass("android/content/Context").newObject(null);}return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);}public static void main(String[] args) throws Exception {zuiyou test = new zuiyou();test.native_init();}
}


似乎一切顺利,接下来执行sign方法。字节数组以及字符串类型都是前两节遇到过的,不做赘述。

    private String callSign(){// 准备入参List<Object> list = new ArrayList<>(10);list.add(vm.getJNIEnv()); // 第一个参数是envlist.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。list.add(vm.addLocalObject(new StringObject(vm, "12345")));ByteArray plainText = new ByteArray(vm, "r0ysue".getBytes(StandardCharsets.UTF_8));list.add(vm.addLocalObject(plainText));Number number = module.callFunction(emulator, 0x4a28D, list.toArray())[0];return vm.getObject(number.intValue()).getValue().toString();};

看一下整体代码和运行效果

package com.right;import com.github.unidbg.linux.android.dvm.AbstractJni;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;public class zuiyou extends AbstractJni{private final AndroidEmulator emulator;private final VM vm;private final Module module;zuiyou() {emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.xiaochuankeji.tieba").build(); // 创建模拟器实例final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\zuiyou\\right573.apk")); // 创建Android虚拟机DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\zuiyou\\libnet_crypto.so"), true); // 加载so到虚拟内存module = dm.getModule(); //获取本SO模块的句柄vm.setJni(this);vm.setVerbose(true);dm.callJNI_OnLoad(emulator);};public void native_init(){// 0x4a069List<Object> list = new ArrayList<>(10);list.add(vm.getJNIEnv()); // 第一个参数是envlist.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。module.callFunction(emulator, 0x4a069, list.toArray());};private String callSign(){// 准备入参List<Object> list = new ArrayList<>(10);list.add(vm.getJNIEnv()); // 第一个参数是envlist.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。list.add(vm.addLocalObject(new StringObject(vm, "12345")));ByteArray plainText = new ByteArray(vm, "r0ysue".getBytes(StandardCharsets.UTF_8));list.add(vm.addLocalObject(plainText));Number number = module.callFunction(emulator, 0x4a28D, list.toArray())[0];return vm.getObject(number.intValue()).getValue().toString();};@Overridepublic DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {switch (signature) {case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;":return vm.resolveClass("android/content/Context").newObject(null);}return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);}public static void main(String[] args) throws Exception {zuiyou test = new zuiyou();test.native_init();System.out.println(test.callSign());}
}

运行结果

提示调用Context的getClass方法,找不到,所以报错了。不用怀疑,正如你想的那样,这儿的Context就是我们上面传入的Context。破罐子破摔,先重写callObjectMethodV,返回一个空的类,看贵公子下一步干什么,我们只需要最后补正确就行。

    @Overridepublic DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {switch (signature) {case "android/content/Context->getClass()Ljava/lang/Class;":{return dvmObject.getObjectType();}}return super.callObjectMethodV(vm, dvmObject, signature, vaList);};

完整代码以及运行效果

package com.right;import com.github.unidbg.linux.android.dvm.AbstractJni;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;public class zuiyou extends AbstractJni{private final AndroidEmulator emulator;private final VM vm;private final Module module;zuiyou() {emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.xiaochuankeji.tieba").build(); // 创建模拟器实例final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\zuiyou\\right573.apk")); // 创建Android虚拟机DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\zuiyou\\libnet_crypto.so"), true); // 加载so到虚拟内存module = dm.getModule(); //获取本SO模块的句柄vm.setJni(this);vm.setVerbose(true);dm.callJNI_OnLoad(emulator);};public void native_init(){// 0x4a069List<Object> list = new ArrayList<>(10);list.add(vm.getJNIEnv()); // 第一个参数是envlist.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。module.callFunction(emulator, 0x4a069, list.toArray());};private String callSign(){// 准备入参List<Object> list = new ArrayList<>(10);list.add(vm.getJNIEnv()); // 第一个参数是envlist.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。list.add(vm.addLocalObject(new StringObject(vm, "12345")));ByteArray plainText = new ByteArray(vm, "r0ysue".getBytes(StandardCharsets.UTF_8));list.add(vm.addLocalObject(plainText));Number number = module.callFunction(emulator, 0x4a28D, list.toArray())[0];return vm.getObject(number.intValue()).getValue().toString();};@Overridepublic DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {switch (signature) {case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;":return vm.resolveClass("android/content/Context").newObject(null);}return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);}@Overridepublic DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {switch (signature) {case "android/content/Context->getClass()Ljava/lang/Class;":{return dvmObject.getObjectType();}}return super.callObjectMethodV(vm, dvmObject, signature, vaList);};public static void main(String[] args) throws Exception {zuiyou test = new zuiyou();test.native_init();System.out.println(test.callSign());}
}

这次报错,在找这个类的getSimpleName,getSimpleName是类名,比如类:com.R0ysue.test.abc,类名就是abc。

让我们捋一下完整的流程,在com/izuiyou/common/base/BaseApplication中调用getAppContext方法,获得一个Context上下文,然后getClass获取它的类,最后查看它的类名。类名就是这一系列操作的最终目的,我们前面几步都只浅浅的补了一下,只能说类型给对了,别的都没给。但只要最后的类名给它返回正确的字符串,就没问题。

使用Objection的插件 Wallbreaker查看相关类(BaseApplication的getAppContext其结果以及类名)



完整类名,cn.xiaochaunkeji.tieba.AppController,getSimpleName即AppController

修复后完整代码如下

package com.right;import com.github.unidbg.linux.android.dvm.AbstractJni;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;public class zuiyou extends AbstractJni{private final AndroidEmulator emulator;private final VM vm;private final Module module;zuiyou() {emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.xiaochuankeji.tieba").build(); // 创建模拟器实例final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\zuiyou\\right573.apk")); // 创建Android虚拟机DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\zuiyou\\libnet_crypto.so"), true); // 加载so到虚拟内存module = dm.getModule(); //获取本SO模块的句柄vm.setJni(this);vm.setVerbose(true);dm.callJNI_OnLoad(emulator);};public void native_init(){// 0x4a069List<Object> list = new ArrayList<>(10);list.add(vm.getJNIEnv()); // 第一个参数是envlist.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。module.callFunction(emulator, 0x4a069, list.toArray());};private String callSign(){// 准备入参List<Object> list = new ArrayList<>(10);list.add(vm.getJNIEnv()); // 第一个参数是envlist.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。list.add(vm.addLocalObject(new StringObject(vm, "12345")));ByteArray plainText = new ByteArray(vm, "r0ysue".getBytes(StandardCharsets.UTF_8));list.add(vm.addLocalObject(plainText));Number number = module.callFunction(emulator, 0x4a28D, list.toArray())[0];return vm.getObject(number.intValue()).getValue().toString();};@Overridepublic DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {switch (signature) {case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;":return vm.resolveClass("android/content/Context").newObject(null);}return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);}@Overridepublic DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {switch (signature) {case "android/content/Context->getClass()Ljava/lang/Class;":{return dvmObject.getObjectType();}case "java/lang/Class->getSimpleName()Ljava/lang/String;":{return new StringObject(vm, "AppController");}}return super.callObjectMethodV(vm, dvmObject, signature, vaList);};public static void main(String[] args) throws Exception {zuiyou test = new zuiyou();test.native_init();System.out.println(test.callSign());}
}

继续运行


可以看到,接下来获取了类的路径,这一步是什么意思呢?

实际上,这依然是签名校验的一部分,不管是获取类名,还是此处获取类的文件路径,都是在做校验——校验SO是否在本App内执行。”补“+”修复“循环往复,下面一连补两个签名,返回值都根据实际APP情况。

   @Overridepublic DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {switch (signature) {case "android/content/Context->getClass()Ljava/lang/Class;":{return dvmObject.getObjectType();}case "java/lang/Class->getSimpleName()Ljava/lang/String;":{return new StringObject(vm, "AppController");}case "android/content/Context->getFilesDir()Ljava/io/File;":case "java/lang/String->getAbsolutePath()Ljava/lang/String;": {return new StringObject(vm, "/data/user/0/cn.xiaochuankeji.tieba/files");}}return super.callObjectMethodV(vm, dvmObject, signature, vaList);};

继续运行

检测是否有调试,如法炮制

   @Overridepublic boolean callStaticBooleanMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {switch (signature){case "android/os/Debug->isDebuggerConnected()Z":{return false;}}throw new UnsupportedOperationException(signature);}


使用Unidbg 的 API返回PID

    @Overridepublic int callStaticIntMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {switch (signature){case "android/os/Process->myPid()I":{return emulator.getPid();}}throw new UnsupportedOperationException(signature);}

继续运行


结果与Frida主动调用结果完全一致,大功告成!但是,关于JNI环境的补充这一块,想必大家还有很多疑惑,整个过程滞涩感比较重,读者恐怕很难感受到其中的连续感。其实这是补JNI环境时都会出现的感觉,个人建议使用Frida主动调用+JNItrace实现一次完整的JNI trace。然后依照着trace做补环境的工作。但实际使用时,会遇到不少问题。比如JNItrace的attach模式有问题,spawn模式容易崩溃,且输出过多难以辨识。所以建议写Demo加载SO,然后使用JNItrace trace 结果,这是一个妥善的方法,但记得时常需要处理JNI层的签名校验,在之后我们完整的展示这个过程(事实上,还挺费事和曲折)

四、算法还原

因为返回值总是32位长度,且明文不变时输出也不变,很容易让人想到哈希算法,尤其是MD5算法。但是,样本经过了一定程度的OLLVM混淆,很难自上而下或者自下而上逐个模块分析代码逻辑,所以我们需要借助一下工具,当当当, FIndHash试一下。

FindHash需要运行数分钟,因为其原理是对哈希算法中的运算特征进行正则匹配,需要对函数逐个反编译,运行结束后,根据提示运行Frida脚本


IDA快捷键 G 跳转到65540

编写对该函数的Hook,因为不确定三个参数是指针还是数值,所以先全部做为数值处理,作为long类型看待,防止整数溢出。

public void hook65540(){// 加载HookZzIHookZz hookZz = HookZz.getInstance(emulator);hookZz.wrap(module.base + 0x65540 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数@Override// 类似于 frida onEnterpublic void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {// 类似于Frida args[0]System.out.println(ctx.getR0Long());System.out.println(ctx.getR1Long());System.out.println(ctx.getR2Long());};@Override// 类似于 frida onLeavepublic void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {}});}public static void main(String[] args) throws Exception {zuiyou test = new zuiyou();test.hook65540();test.native_init();System.out.println(test.callSign());}


可以看到,参数2应该是数组,参数1和3则像是地址。

采用如下方式打印地址所指向的内存,其效果类似于frida中hexdump。

    public void hook65540(){// 加载HookZzIHookZz hookZz = HookZz.getInstance(emulator);hookZz.wrap(module.base + 0x65540 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数@Override// 类似于 frida onEnterpublic void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {// 类似于Frida args[0]Inspector.inspect(ctx.getR0Pointer().getByteArray(0, 0x10), "Arg1");System.out.println(ctx.getR1Long());Inspector.inspect(ctx.getR2Pointer().getByteArray(0, 0x10), "Arg3");};@Override// 类似于 frida onLeavepublic void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {}});}


不要管“md5=xxx,hex=xxx”,这是Unidbg中日志输出的固定格式,千万不要当成某种hook的结果。

可以发现,参数1就是我们JAVA层传入的参数2,而参数3,意义未知。事实上,参数3大概率是Buffer,它用于存放运算的结果,这是C常用的开发习惯,大家记住就好。而参数2,长度总是和入参的字符串长度一致,所以就是长度。

在Frida中,onEnter中使用到的arg,onLeave中无法获取到,因此我们用this.xxx = args[n]的方式保存它,然后在onLeave中查看这个buffer在函数运行完后的结果。

HookZz也提供了类似的功能,在执行前,push保存,在后面再pop取出,用法如下

    public void hook65540(){// 加载HookZzIHookZz hookZz = HookZz.getInstance(emulator);hookZz.wrap(module.base + 0x65540 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数@Override// 类似于 frida onEnterpublic void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {// 类似于Frida args[0]Inspector.inspect(ctx.getR0Pointer().getByteArray(0, 0x10), "Arg1");System.out.println(ctx.getR1Long());Inspector.inspect(ctx.getR2Pointer().getByteArray(0, 0x10), "Arg3");// push ctx.push(ctx.getR2Pointer());};@Override// 类似于 frida onLeavepublic void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {// pop 取出Pointer output = ctx.pop();Inspector.inspect(output.getByteArray(0, 0x10), "Arg3 after function");}});}


Hook结果验证了我们的说法,参数1是输入,参数2是长度,参数3是buffer,用于存储结果。

接下来我们就要好好分析这个算法了,它疑似MD5算法,按H键将这四个数转成十六进制


说它疑似MD5主要有两个依据

  • 输出结果是32位,MD5恰好也是32位长度。
  • 有四个IV,MD5就有四个IV

但是呢,它不是标准MD5,看一下标准MD5的四个IV

可以发现IV不一致,我们也可以在Cyberchef中验证是否是标准MD5的结果。


结果不一致,那么我们很可能遇到了魔改哈希算法。但不必感到惊慌,不熟悉算法原理的可以看一下SO基础课的算法部分,对原理的讲解非常深刻细致,我们这里关注于实战的部分。

哈希算法的魔改,最简单的修改点就是修改IV,此处似乎采用了这种。如下是一份python版本带注释的MD5源码,我们对应着修改一下IV,测试一下结果。

import binasciiSV = [0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf,0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af,0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e,0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,0xd62f105d, 0x2441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6,0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8,0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122,0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05, 0xd9d4d039,0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244, 0x432aff97,0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d,0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391]# 根据ascil编码把字符转成对应的二进制
def binvalue(val, bitsize):binval = bin(val)[2:] if isinstance(val, int) else bin(ord(val))[2:]if len(binval) > bitsize:raise ("binary value larger than the expected size")while len(binval) < bitsize:binval = "0" + binvalreturn binvaldef string_to_bit_array(text):array = list()for char in text:binval = binvalue(char, 8)array.extend([int(x) for x in list(binval)])return array# 循环左移
def leftCircularShift(k, bits):bits = bits % 32k = k % (2 ** 32)upper = (k << bits) % (2 ** 32)result = upper | (k >> (32 - (bits)))return (result)# 分块
def blockDivide(block, chunks):result = []size = len(block) // chunksfor i in range(0, chunks):result.append(int.from_bytes(block[i * size:(i + 1) * size], byteorder="little"))return result# F函数作用于“比特位”上
# if x then y else z
def F(X, Y, Z):compute = ((X & Y) | ((~X) & Z))return compute# if z then x else y
def G(X, Y, Z):return ((X & Z) | (Y & (~Z)))# if X = Y then Z else ~Z
def H(X, Y, Z):return (X ^ Y ^ Z)def I(X, Y, Z):return (Y ^ (X | (~Z)))# 四个F函数
def FF(a, b, c, d, M, s, t):result = b + leftCircularShift((a + F(b, c, d) + M + t), s)return (result)def GG(a, b, c, d, M, s, t):result = b + leftCircularShift((a + G(b, c, d) + M + t), s)return (result)def HH(a, b, c, d, M, s, t):result = b + leftCircularShift((a + H(b, c, d) + M + t), s)return (result)def II(a, b, c, d, M, s, t):result = b + leftCircularShift((a + I(b, c, d) + M + t), s)return (result)# 数据转换
def fmt8(num):bighex = "{0:08x}".format(num)binver = binascii.unhexlify(bighex)result = "{0:08x}".format(int.from_bytes(binver, byteorder='little'))return (result)# 计算比特长度
def bitlen(bitstring):return len(bitstring) * 8def md5sum(msg):# 计算比特长度,如果内容过长,64个比特放不下。就取低64bit。msgLen = bitlen(msg) % (2 ** 64)# 先填充一个0x80,其实是先填充一个1,后面跟对应个数的0,因为一个明文的编码至少需要8比特,所以直接填充 0b10000000即0x80msg = msg + b'\x80'  # 0x80 = 1000 0000# 似乎各种编码,即使是一个字母,都至少得1个字节,即8bit才能表示,所以不会出现原文55bit,pad1就满足的情况?可是不对呀,要是二进制文件呢?# 填充0到满足要求为止。zeroPad = (448 - (msgLen + 8) % 512) % 512zeroPad //= 8msg = msg + b'\x00' * zeroPad + msgLen.to_bytes(8, byteorder='little')# 计算循环轮数,512个为一轮msgLen = bitlen(msg)iterations = msgLen // 512# 初始化变量# 算法魔改的第一个点,也是最明显的点# A = 0x67452301# B = 0xefcdab89# C = 0x98badcfe# D = 0x10325476# 魔改IVA = 0x67552301B = 0xEDCDAB89C = 0x98BADEFED = 0x16325476# MD5的主体就是对abcd进行n次的迭代,所以得有个初始值,可以随便选,也可以用默认的魔数,这个改起来毫无风险,所以大家爱魔改它,甚至改这个都不算魔改。# main loopfor i in range(0, iterations):a = Ab = Bc = Cd = Dblock = msg[i * 64:(i + 1) * 64]# 明文的处理,顺便调整了一下端序M = blockDivide(block, 16)# Roundsa = FF(a, b, c, d, M[0], 7, SV[0])d = FF(d, a, b, c, M[1], 12, SV[1])c = FF(c, d, a, b, M[2], 17, SV[2])b = FF(b, c, d, a, M[3], 22, SV[3])a = FF(a, b, c, d, M[4], 7, SV[4])d = FF(d, a, b, c, M[5], 12, SV[5])c = FF(c, d, a, b, M[6], 17, SV[6])b = FF(b, c, d, a, M[7], 22, SV[7])a = FF(a, b, c, d, M[8], 7, SV[8])d = FF(d, a, b, c, M[9], 12, SV[9])c = FF(c, d, a, b, M[10], 17, SV[10])b = FF(b, c, d, a, M[11], 22, SV[11])a = FF(a, b, c, d, M[12], 7, SV[12])d = FF(d, a, b, c, M[13], 12, SV[13])c = FF(c, d, a, b, M[14], 17, SV[14])b = FF(b, c, d, a, M[15], 22, SV[15])a = GG(a, b, c, d, M[1], 5, SV[16])d = GG(d, a, b, c, M[6], 9, SV[17])c = GG(c, d, a, b, M[11], 14, SV[18])b = GG(b, c, d, a, M[0], 20, SV[19])a = GG(a, b, c, d, M[5], 5, SV[20])d = GG(d, a, b, c, M[10], 9, SV[21])c = GG(c, d, a, b, M[15], 14, SV[22])b = GG(b, c, d, a, M[4], 20, SV[23])a = GG(a, b, c, d, M[9], 5, SV[24])d = GG(d, a, b, c, M[14], 9, SV[25])c = GG(c, d, a, b, M[3], 14, SV[26])b = GG(b, c, d, a, M[8], 20, SV[27])a = GG(a, b, c, d, M[13], 5, SV[28])d = GG(d, a, b, c, M[2], 9, SV[29])c = GG(c, d, a, b, M[7], 14, SV[30])b = GG(b, c, d, a, M[12], 20, SV[31])a = HH(a, b, c, d, M[5], 4, SV[32])d = HH(d, a, b, c, M[8], 11, SV[33])c = HH(c, d, a, b, M[11], 16, SV[34])b = HH(b, c, d, a, M[14], 23, SV[35])a = HH(a, b, c, d, M[1], 4, SV[36])d = HH(d, a, b, c, M[4], 11, SV[37])c = HH(c, d, a, b, M[7], 16, SV[38])b = HH(b, c, d, a, M[10], 23, SV[39])a = HH(a, b, c, d, M[13], 4, SV[40])d = HH(d, a, b, c, M[0], 11, SV[41])c = HH(c, d, a, b, M[3], 16, SV[42])b = HH(b, c, d, a, M[6], 23, SV[43])a = HH(a, b, c, d, M[9], 4, SV[44])d = HH(d, a, b, c, M[12], 11, SV[45])c = HH(c, d, a, b, M[15], 16, SV[46])b = HH(b, c, d, a, M[2], 23, SV[47])a = II(a, b, c, d, M[0], 6, SV[48])d = II(d, a, b, c, M[7], 10, SV[49])c = II(c, d, a, b, M[14], 15, SV[50])b = II(b, c, d, a, M[5], 21, SV[51])a = II(a, b, c, d, M[12], 6, SV[52])d = II(d, a, b, c, M[3], 10, SV[53])c = II(c, d, a, b, M[10], 15, SV[54])b = II(b, c, d, a, M[1], 21, SV[55])a = II(a, b, c, d, M[8], 6, SV[56])d = II(d, a, b, c, M[15], 10, SV[57])c = II(c, d, a, b, M[6], 15, SV[58])b = II(b, c, d, a, M[13], 21, SV[59])a = II(a, b, c, d, M[4], 6, SV[60])d = II(d, a, b, c, M[11], 10, SV[61])c = II(c, d, a, b, M[2], 15, SV[62])b = II(b, c, d, a, M[9], 21, SV[63])A = (A + a) % (2 ** 32)B = (B + b) % (2 ** 32)C = (C + c) % (2 ** 32)D = (D + d) % (2 ** 32)result = fmt8(A) + fmt8(B) + fmt8(C) + fmt8(D)return resultif __name__ == "__main__":data = str("r0ysue").encode("UTF-8")print("plainText: ", data)print("result: ", md5sum(data))

结果与样本结果一致,因此可以断定,此处就是魔改且只魔改了IV的MD5算法。但我并不打算在此处结束这篇文章,我们还可以讨论更多的话题。

1.如何主动调用一个Native函数

在Frida中可以使用NativeFunction API 主动调用

function call_65540(base_addr){// 函数在内存中的地址var real_addr = base_addr.add(0x65541)var md5_function = new NativeFunction(real_addr, "int", ["pointer", "int", "pointer"])// 参数1 明文字符串的指针var input = "r0ysue";var arg1 = Memory.allocUtf8String(input);// 参数2 明文长度var arg2 = input.length;// 参数3,存放结果的buffervar arg3 = Memory.alloc(16);md5_function(arg1, arg2, arg3);console.log(hexdump(arg3,{length:0x10}));
}function callMd5(){// 确定SO 的基地址var base_addr = Module.findBaseAddress("libnet_crypto.so");call_65540(base_addr);
}// frida -UF -l path\hookright.js


在Unidbg也是类似的,只不过换一下API罢了,让我们来看一下

    public void callMd5(){List<Object> list = new ArrayList<>(10);// arg1String input = "r0ysue";// malloc memoryMemoryBlock memoryBlock1 = emulator.getMemory().malloc(16, false);// get memory pointerUnidbgPointer input_ptr=memoryBlock1.getPointer();// write plainText on itinput_ptr.write(input.getBytes(StandardCharsets.UTF_8));// arg2int input_length = input.length();// arg3 -- bufferMemoryBlock memoryBlock2 = emulator.getMemory().malloc(16, false);UnidbgPointer output_buffer=memoryBlock2.getPointer();// 填入参入list.add(input_ptr);list.add(input_length);list.add(output_buffer);// runmodule.callFunction(emulator, 0x65540 + 1, list.toArray());// print arg3Inspector.inspect(output_buffer.getByteArray(0, 0x10), "output");};

需要注意,在Unidbg中,同样的功能有至少两种实现和写法——Unicorn的原生方法以及Unidbg封装后的方法,在阅读别人代码时需要灵活变通。就好比 getR0longemulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R0),它们都是获取寄存器R0的数值。


在上面,我们演示了Unidbg和Frida主动调用单个Native函数的代码,千万不要小瞧它,这是很有用的技巧,尤其在Unidbg中。举个例子,一个样本较为复杂,其中包含大量JNI交互,使用Unicorn补环境使得整体跑起来非常麻烦,那我们就可以静态分析出关键函数,只模拟执行关键函数,或者从算法还原的角度上讲,单独执行待分析的函数以便减少干扰也是有用的。

第二个问题

哈希算法的IV是一个常见且简单的魔改点,在大量样本中都可以看到,事实上,它对分析者的阻挡程度很小,那么如果样本做了更深层的魔改呢?比如当我们对应着修改完IV,发现结果依然对不上,那么该怎么分析更深的魔改哈希算法呢?

这就是下一篇的样本和内容喽!

五、尾声

凭心而论,在补JNI环境那块儿讲的有点含糊,想把此处讲清实在不容易,JNItrace是补JNI环境的利器,但它的实操体验并不顺畅。在额外的文章中,我们把这个问题讲清楚,下一篇是深度魔改哈希算法,敬请期待。

资料链接:https://pan.baidu.com/s/1_ydXiPKgG-zpTYu8xwWG8A
提取码:bm0b

SO逆向入门实战教程三:V2-Sign相关推荐

  1. SO逆向入门实战教程九——blackbox

    文章目录 一.前言 二.准备 三.Unidbg模拟执行 四.Unidbg算法还原 五.尾声 一.前言 上篇中,我们借AB之口,讨论了这样一个问题--Unidbg是否适合做算法分析的主力工具,这个问题没 ...

  2. SO逆向入门实战教程八:文件读写

    文章目录 一.前言 二.demo1设计 三.Unidbg模拟执行demo1 四.demo2设计 五.Unidbg模拟执行demo2 六.尾声 一.前言 本篇分析的是自写demo,帮助大家熟悉Unidb ...

  3. SO逆向入门实战教程十:SimpleSign

    一.前言 这是系列的第十篇,通过该样本可以充分学习如何在Unidbg中补充环境.朋友zh3nu11和我共同完成了这篇内容,感谢. 二.准备 首先我们发现了init函数,它应该就是SO的初始化函数,其余 ...

  4. SO逆向入门实战教程一:OASIS

    文章目录 一.前言 二.准备 三.Unidbg模拟执行 四.ExAndroidNativeEmu 模拟执行 五.算法分析 六.尾声 一.前言 这是SO逆向入门实战教程的第一篇,总共会有十三篇,十三个实 ...

  5. Python之Numpy入门实战教程(1):基础篇

    Numpy.Pandas.Matplotlib是Python的三个重要科学计算库,今天整理了Numpy的入门实战教程.NumPy是使用Python进行科学计算的基础库. NumPy以强大的N维数组对象 ...

  6. Python之Numpy入门实战教程(2):进阶篇之线性代数

    Numpy.Pandas.Matplotlib是Python的三个重要科学计算库,今天整理了Numpy的入门实战教程.NumPy是使用Python进行科学计算的基础库. NumPy以强大的N维数组对象 ...

  7. Spring Boot 入门实战教程

    Spring Boot 2.0 入门实战教程 开发环境:JDK1.8或以上 源码下载:https://pan.baidu.com/s/1Z771VDiuabDBJJV445xLeA 欢迎访问我的个人博 ...

  8. 【kratos入门实战教程】1-kratos项目搭建和开发环境配置

    1.系列目录 [kratos入门实战教程]0-商城项目介绍 [kratos入门实战教程]1-kratos项目搭建和开发环境配置 [kratos入门实战教程]2-实现注册登陆业务 2.概览 经过上一篇的 ...

  9. 【kratos入门实战教程】2-实现注册登陆业务

    1.系列目录 [kratos入门实战教程]0-商城项目介绍 [kratos入门实战教程]1-kratos项目搭建和开发环境配置 [kratos入门实战教程]2-实现注册登陆业务 2.概览 通过本篇文章 ...

  10. 视频教程-深度学习与PyTorch入门实战教程-深度学习

    深度学习与PyTorch入门实战教程 新加坡国立大学研究员 龙良曲 ¥399.00 立即订阅 扫码下载「CSDN程序员学院APP」,1000+技术好课免费看 APP订阅课程,领取优惠,最少立减5元 ↓ ...

最新文章

  1. Vue组件中的data和props属性
  2. 效率提升多倍, 推荐值得收藏40 个命令总结
  3. R包dplyr进行数据清洗和整理
  4. 洛阳中考实验计算机分数,2019洛阳中考总分是多少 录取分数线是多少
  5. 2021年值得关注的5个RPA趋势
  6. 关于mybatis的xml文件中使用 >= 或者 <= 号报错的解决方案
  7. 笔记本电脑按开机键没反应怎么办?(先记得长按开机键,大约10秒钟看看可以吗)
  8. 连续特征离散化方法介绍
  9. 6-1 求链式表的表长
  10. C#自定义控件,在项目工具箱中加入自定义控件,调用自定义控件
  11. 发生android.view.ViewRoot$CalledFromWrongThreadException异常的解决方案
  12. 抽屉之Tornado实战(5)--点赞与评论树
  13. java wsdl xfire_java调用wsdl xfire和cxf两种方式
  14. html怎么做出相框的效果,PS滤镜制作漂亮的实木相框效果
  15. 2020年最新全国彩礼地图出炉,你那儿娶媳妇儿需要多少彩礼钱呢?数据分析来告诉你...
  16. 浅谈移动前端的最佳实践
  17. Kubeadm初始化Kubernetes集群
  18. 2021Eclipse下载与安装教程
  19. 我在名牌大学毕业后的经历 (看完感动,涌动,后泪流)转
  20. 云服务器系统种类,云服务器系统种类

热门文章

  1. Vue 访问外链失败问题
  2. 最新影视小程序,可以打包成双端APP。可开通流量主,独立系统无加密。
  3. vue中实现复制链接功能
  4. java毕业设计——基于JSP+JavaBean+sqlserver的在线购物系统设计与实现(毕业论文+程序源码)——在线购物系统
  5. PyQt5 python 数据库 表格动态增删改
  6. jflash烧录教程_JLINK-V8固件及烧录教程
  7. 就业设想 计算机篇,自我评价及未来工作设想.doc
  8. NLP语种检测的基准对比测试
  9. git 合并代码后出现 Merge conflict marker encountered
  10. Yes, I Love You