这篇文章上次修改于 1187 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
原文 -> https://blog.csdn.net/qq_38851536/article/details/117533396
前言
这是SO逆向入门实战教程的第三篇,总共会有十三篇,十三个实战。有以下几个注意点:
- 主打入门级的实战,适合有一定基础但缺少实战的朋友(了解JNI,也上过一些Native层逆向的课,但感觉实战匮乏,想要壮壮胆,入入门)。
- 侧重新工具、新思路、新方法的使用,算法分析的常见路子是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就"不乱码",这是为什么呢?甚至有人在论坛发帖求助类似问题:
其实其中的道理并不复杂,甚至可以说很简单——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(){
// 0x4a069
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.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方法
@Override
public 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。
@Override
public 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(){
// 0x4a069
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。
module.callFunction(emulator, 0x4a069, list.toArray());
};
@Override
public 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()); // 第一个参数是env
list.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(){
// 0x4a069
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。
module.callFunction(emulator, 0x4a069, list.toArray());
};
private String callSign(){
// 准备入参
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.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();
};
@Override
public 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,返回一个空的类,看贵公子下一步干什么,我们只需要最后补正确就行。
@Override
public 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(){
// 0x4a069
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。
module.callFunction(emulator, 0x4a069, list.toArray());
};
private String callSign(){
// 准备入参
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.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();
};
@Override
public 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);
}
@Override
public 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(){
// 0x4a069
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。
module.callFunction(emulator, 0x4a069, list.toArray());
};
private String callSign(){
// 准备入参
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.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();
};
@Override
public 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);
}
@Override
public 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情况。
@Override
public 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);
};
继续运行
检测是否有调试,如法炮制
@Override
public 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
@Override
public 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(){
// 加载HookZz
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.base + 0x65540 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
@Override
// 类似于 frida onEnter
public 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 onLeave
public 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(){
// 加载HookZz
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.base + 0x65540 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
@Override
// 类似于 frida onEnter
public 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 onLeave
public 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(){
// 加载HookZz
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.base + 0x65540 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
@Override
// 类似于 frida onEnter
public 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 onLeave
public 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 binascii
SV = [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" + binval
return binval
def 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 % 32
k = k % (2 ** 32)
upper = (k << bits) % (2 ** 32)
result = upper | (k >> (32 - (bits)))
return (result)
# 分块
def blockDivide(block, chunks):
result = []
size = len(block) // chunks
for 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) * 8
def md5sum(msg):
# 计算比特长度,如果内容过长,64个比特放不下。就取低64bit。
msgLen = bitlen(msg) % (2 ** 64)
# 先填充一个0x80,其实是先填充一个1,后面跟对应个数的0,因为一个明文的编码至少需要8比特,所以直接填充 0b10000000即0x80
msg = msg + b'\x80' # 0x80 = 1000 0000
# 似乎各种编码,即使是一个字母,都至少得1个字节,即8bit才能表示,所以不会出现原文55bit,pad1就满足的情况?可是不对呀,要是二进制文件呢?
# 填充0到满足要求为止。
zeroPad = (448 - (msgLen + 8) % 512) % 512
zeroPad //= 8
msg = 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
# 魔改IV
A = 0x67552301
B = 0xEDCDAB89
C = 0x98BADEFE
D = 0x16325476
# MD5的主体就是对abcd进行n次的迭代,所以得有个初始值,可以随便选,也可以用默认的魔数,这个改起来毫无风险,所以大家爱魔改它,甚至改这个都不算魔改。
# main loop
for i in range(0, iterations):
a = A
b = B
c = C
d = D
block = msg[i * 64:(i + 1) * 64]
# 明文的处理,顺便调整了一下端序
M = blockDivide(block, 16)
# Rounds
a = 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 result
if __name__ == "__main__":
data = str("r0ysue").encode("UTF-8")
print("plainText: ", data)
print("result: ", md5sum(data))
结果与样本结果一致,因此可以断定,此处就是魔改且只魔改了IV的MD5算法。但我并不打算在此处结束这篇文章,我们还可以讨论更多的话题。
- 如何主动调用一个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,存放结果的buffer
var 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);
// arg1
String input = "r0ysue";
// malloc memory
MemoryBlock memoryBlock1 = emulator.getMemory().malloc(16, false);
// get memory pointer
UnidbgPointer input_ptr=memoryBlock1.getPointer();
// write plainText on it
input_ptr.write(input.getBytes(StandardCharsets.UTF_8));
// arg2
int input_length = input.length();
// arg3 -- buffer
MemoryBlock memoryBlock2 = emulator.getMemory().malloc(16, false);
UnidbgPointer output_buffer=memoryBlock2.getPointer();
// 填入参入
list.add(input_ptr);
list.add(input_length);
list.add(output_buffer);
// run
module.callFunction(emulator, 0x65540 + 1, list.toArray());
// print arg3
Inspector.inspect(output_buffer.getByteArray(0, 0x10), "output");
};
需要注意,在Unidbg中,同样的功能有至少两种实现和写法——Unicorn的原生方法以及Unidbg封装后的方法,在阅读别人代码时需要灵活变通。就好比 getR0long 和emulator.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
没有评论