从案例中学习unidbg的使用(一)

一、引言

目标 APK:lvzhou.apk

目标方法声明所处的 Dex:target.dex(见参考链接)。样本在 JAVA 层做了加壳。

目标方法:com.weibo.xvideo.NativeApi.s

目标方法实现:liboasiscore.so

二、任务描述

使用 JADX 反编译 target.dex,找到 NativeApi 类,其中的s方法就是本篇的目标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.weibo.xvideo;

import org.jetbrains.annotations.NotNull;

public final class NativeApi {
public NativeApi() {
System.loadLibrary("oasiscore");
}

...

@NotNull
public final native String s(@NotNull byte[] bArr, boolean z);
}

首先使用 Frida 进行动态分析,动调目标进程获得入参和返回值。

1
2
3
4
5
6
7
8
9
Java.perform(function() {
let NativeApi = Java.use("com.weibo.xvideo.NativeApi");
NativeApi["s"].implementation = function (bArr, z) {
console.log('s is called' + ', ' + 'bArr: ' + bArr + ', ' + 'z: ' + z);
let ret = this.s(bArr, z);
console.log('s ret value is ' + ret);
return ret;
};
})

接下来主动调用s方法,它是一个实例方法,可供参考的 Frida 代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
Java.perform(function() {
function stringToBytes(str) {
var javaString = Java.use('java.lang.String');
return javaString.$new(str).getBytes();
}

let NativeApi = Java.use("com.weibo.xvideo.NativeApi");
let arg1 = "aid=01A-khBWIm48A079Pz_DMW6PyZR8uyTumcCNm4e8awxyC2ANU.&cfrom=28B5295010&cuid=5999578300&noncestr=46274W9279Hr1X49A5X058z7ZVz024&platform=ANDROID&timestamp=1621437643609&ua=Xiaomi-MIX2S__oasis__3.5.8__Android__Android10&version=3.5.8&vid=1019013594003&wm=20004_90024";
let arg2 = false;
let ret = NativeApi.$new().s(stringToBytes(arg1), arg2);
console.log("ret:"+ret);
})

输出为3882b522d0c62171d51094914032d5ea,结果无误。接下来我们用unidbg来调用该函数。

三、初始化

我们的代码写在 unidbg-android/src/test/java 路径下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.bilibili;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class xvideo extends AbstractJni {
private final AndroidEmulator emulator;
private final DvmClass NativeApi;
private final VM vm;

public xvideo() {
//初始化模拟器
emulator = AndroidEmulatorBuilder
.for64Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName("com.sina.oasis")
.build();
//初始化内存
Memory memory = emulator.getMemory();
//设置sdk版本
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/bilibili/lvzhou.apk"));
vm.setJni(this);
vm.setVerbose(true);
DalvikModule dm = vm.loadLibrary("oasiscore", true);
NativeApi = vm.resolveClass("com/weibo/xvideo/NativeApi");
//执行 JNI_OnLoad 函数
dm.callJNI_OnLoad(emulator);
}

public static void main(String[] args) {
xvideo xv = new xvideo();
}
}

看起来有些复杂,包含初始化模拟器、初始化内存、设置依赖库路径、创建虚拟机处理器、加载目标 SO、执行其 JNI_OnLoad 函数这一系列操作。

本篇讨论初始化模拟器这一个部分,就是下面这部分,其余代码逻辑放在后面文章里缓缓展开。

1
2
3
4
5
emulator = AndroidEmulatorBuilder
.for64Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName("com.sina.oasis")
.build();

接下来讨论建造过程中涉及的参数。

3.1 位数与架构

依据处理的是32位还是64位的SO,模拟器选择for32Bit或者for64Bit。Unidbg 只支持ARM架构。因此for64Bit对应ARM64,for32Bit同理对应ARM32。对于 ARM 架构,armeabi、armeabi-v7a 对应于 32 位,arm64-v8a 对应于 64 位。

3.2 进程名

setProcessName用于设置进程名,最好按照样本的真实进程名做设置,否则会带来隐患。

目标程序可以通过getprogname库函数获取进程名,如果不加以设置,在 Unidbg 环境里会返回unidbg作为进程名返回,对应源代码逻辑如下。

1
this.processName = processName == null ? "unidbg" : processName;

3.3 后端

addBackendFactory用于设置指令执行引擎。

先回顾一下处理器、操作系统、应用程序的基本关系,大体上是下面这样。

处理器的基本任务是执行汇编指令,操作系统的基本任务是管理、调度资源并提供服务,应用程序依赖于 CPU 执行指令,基于操作系统提供的 API 实现功能。

而在 Unidbg 里,为了让程序感觉自己在操作系统里,做了如下设计。

Unidbg 扮演操作系统,提供微型、有限的操作系统服务,它实现了许多系统调用、代理和模拟了大量的 JNI 调用。在 SO 的感知里,就好似在真实 Android 系统环境里运行。

Backend 即后端,它扮演的角色是处理器,负责执行机器指令,它最常见的方案是虚拟化和指令模拟器,我们这里主要讨论指令模拟器。Unicorn 是近年来最亮眼的指令模拟器,它也是 Unidbg 早先唯一的后端。但后来,Unidbg 支持了数个新的后端,目前共五个 Backend,分别是 Unicorn、Unicorn2、Dynarmic、Hypervisor、KVM。如果不添加 BackendFactory,默认使用 Unicorn Backend,代码逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static Backend createBackend(Emulator<?> emulator, boolean is64Bit, Collection<BackendFactory> backendFactories) {
if (backendFactories != null) {
for (BackendFactory factory : backendFactories) {
Backend backend = factory.newBackend(emulator, is64Bit);
if (backend != null) {
return backend;
}
}
}
// 默认使用 Unicorn后端
return new UnicornBackend(emulator, is64Bit);
}

各种 BackendFactory 的构造方法都要传入fallbackUnicorn,这是一个布尔型参数,它表示如果这个后端创建失败时如何处理——报错还是回退到 Unicorn Backend。

相比较 Unicorn,Unidbg 基于 Unicorn2 设计和实现了多线程的相关逻辑,因此在 Unicorn 和 Unicorn2 之间,应该选择 Unicorn2。

1
addBackendFactory(new Unicorn2Factory(true))

3.4 根目录

setRootDir用于设置虚拟文件系统的根目录,在语义上它对应于 Andorid 的根目录。

1
2
3
4
5
6
emulator = AndroidEmulatorBuilder.for32Bit()
.setProcessName(executable.getName())
// 设置根目录
.setRootDir(new File("target/rootfs"))
.addBackendFactory(new DynarmicFactory(true))
.build();

当我们认为目标 SO 可能会做文件访问与读写操作时,就应该设置根目录,程序对文件的读写会落在这个目录里。

如果不加以设置,Unidbg 会默认在本机临时目录下创建根目录,这会在将项目迁移到其他电脑上时带来不便。所以我们一般会主动设置根目录,并设置为target/rootfs这个相对路径,使得潜在的文件依赖位于在当前 Unidbg 项目里,方便打包处理和迁移。

3.5 多线程

当 Unidbg 飘红报错Out of Memory时,你需要打开 Unidbg 的多线程模式。

Unidbg 在 Unicorn2 后端上实现了相对完善的多线程处理逻辑,如果读者希望开启多线程,除了要将 Backend 设置为 Unicorn2 外,还需要在模拟器初始化后通过setEnableThreadDispatcher开启多线程调度,以及设置线程切换条件registerEmuCountHook

1
2
3
4
5
6
7
8
emulator = AndroidEmulatorBuilder.for32Bit()
.addBackendFactory(new Unicorn2Factory(false))
.setProcessName("test")
.build();
// 设置执行多少条指令切换一次线程
emulator.getBackend().registerEmuCountHook(10000);
// 开启线程调度器
emulator.getSyscallHandler().setEnableThreadDispatcher(true);

由于 Unidbg 所实现的多线程逻辑不完备,并不是所有场景里都适用,因此是默认不开启多线程逻辑。建议在模拟执行相对复杂的样本时,就打开多线程;如果样本难度一般,就不必打开。

四、发起调用

接下来发起对s的调用,它是一个实例方法,读者可以将calls和 Frida Call 的代码逻辑做对照。

1
2
3
4
5
6
7
8
9
10
11
12
public String calls(){
String arg1 = "aid=01A-khBWIm48A079Pz_DMW6PyZR8uyTumcCNm4e8awxyC2ANU.&cfrom=28B5295010&cuid=5999578300&noncestr=46274W9279Hr1X49A5X058z7ZVz024&platform=ANDROID&timestamp=1621437643609&ua=Xiaomi-MIX2S__oasis__3.5.8__Android__Android10&version=3.5.8&vid=1019013594003&wm=20004_90024";
Boolean arg2 = false;
String ret = NativeApi.newObject(null).callJniMethodObject(emulator, "s([BZ)Ljava/lang/String;", arg1.getBytes(StandardCharsets.UTF_8), arg2).getValue().toString();
return ret;
}

public static void main(String[] args) {
xvideo xv = new xvideo();
String result = xv.calls();
System.out.println("call s result:"+result);
}

运行后得出我们的预期结果

1
call s result:3882b522d0c62171d51094914032d5ea

4.1 创建类和实例化

回顾 DEX 的反编译结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.weibo.xvideo;

import org.jetbrains.annotations.NotNull;

public final class NativeApi {
public NativeApi() {
System.loadLibrary("oasiscore");
}

@NotNull
public final native String d(@NotNull String str);

@NotNull
public final native String dg(@NotNull String str, boolean z);

@NotNull
public final native String e(@NotNull String str);

@NotNull
public final native String s(@NotNull byte[] bArr, boolean z);
}

s方法是NativeApi类里的实例方法,因此在调用它时,需要先获取NativeApi类,再创建它的实例,最后才是发起调用,比如 Frida Call 代码就遵循这样的原则。Java.use获取类对象,$new()实例化,然后调用s方法。

1
2
3
4
5
6
7
8
9
10
11
12
Java.perform(function() {
function stringToBytes(str) {
var javaString = Java.use('java.lang.String');
return javaString.$new(str).getBytes();
}

let NativeApi = Java.use("com.weibo.xvideo.NativeApi");
let arg1 = "aid=01A-khBWIm48A079Pz_DMW6PyZR8uyTumcCNm4e8awxyC2ANU.&cfrom=28B5295010&cuid=5999578300&noncestr=46274W9279Hr1X49A5X058z7ZVz024&platform=ANDROID&timestamp=1621437643609&ua=Xiaomi-MIX2S__oasis__3.5.8__Android__Android10&version=3.5.8&vid=1019013594003&wm=20004_90024";
let arg2 = false;
let ret = NativeApi.$new().s(stringToBytes(arg1), arg2);
console.log("ret:"+ret);
})

Unidbg 作为 SO 模拟器,它没有加载 DEX,更没有运行 DEX,自然没法获取其中对应的类信息,那么它如何处理NativeApi以及更多的用户自定义的类库访问需求?这是 Unidbg 的核心问题之一。

首先,它构建了一套描述类库,或者说映射类库的机制。在 JAVA 层,一个类是Class,在 JNI 层,一个类是Jclass,而在 Unidbg 的 JNI 层,一个类是DvmClass

在 Unidbg 中,通过vm.resolveClass(className)可以声明一个类名是classNameDvmClass

1
DvmClass NativeApi = vm.resolveClass("com/weibo/xvideo/NativeApi");

在类名的表述上,必须是全类名,旧版 Unidbg 上必须用斜杠/作为分隔符,而 Unidbg 0.9.6 及以上的当前版本中,用斜杠/或点号.分割都可行,因为resolveClass会替我们做这种替换。

1
2
3
4
5
@Override
public final DvmClass resolveClass(String className, DvmClass... interfaceClasses) {
className = className.replace('.', '/');
// 省略逻辑
}

resolveClass是一个可变参数方法,它的第二个参数是可变参数,用于进一步描述我们所构造的这个DvmClass的父类和接口。

1
2
3
4
/**
* @param interfaceClasses 如果不为空的话,第一个为superClass,其它的为interfaces
*/
DvmClass resolveClass(String className, DvmClass... interfaceClasses);

举个例子,Android 的 Application 类。它继承自 ContextWrapper,ContextWrapper 继承自 Context,具体如下图所示:

如果想用DvmClass表明 Application 的父类情况,可以像下面这样嵌套表示。

1
2
3
DvmClass Context = vm.resolveClass("android/content/Context");
DvmClass ContextWrapper = vm.resolveClass("android/content/ContextWrapper", Context);
DvmClass Application = vm.resolveClass("android/app/Application", ContextWrapper);

需要注意,这并不意味着我们必须非要这样才能表示 Application 类,类是数据和方法的集合,**DvmClass并不是要完美模拟对应的类,更多时候只是简单的“占位”,使之符合形式上的要求。**

接下来讨论类的实例化,对类调用newObject(Object value)即可完成实例化,得到一个形式上的对象。

1
2
DvmClass NativeApi = vm.resolveClass("com/weibo/xvideo/NativeApi");
DvmObject<?> nativeApi = NativeApi.newObject(null);

在 Java 中它是 Object,JNI 中叫做 Jobject,Unidbg 里叫 DvmObject。

newObject 方法的参数和真实对象的构造函数的参数不是一回事,它只是一种“形式上”的对象,不需要和真实的对象保持一致。

resolveClass以及newObject只是对真实类和对象的描述、占位、投影,然后根据程序逻辑上对这些类、对象的使用情况,再逐步完善这样的一种描述、占位、投影,使之能完成基本需求。

这么做是有好处的,我们常说“吃多少,拿多少”,Unidbg 所设计的这种逻辑,就是程序用多少,就补多少。一个对象可能有一百个方法,两百个成员属性,但程序中如果只使用到了其中的一个方法,两个属性,那么只需要处理它们,而不用管其他的部分。

通过NativeApi.newObject(null)创建实例后,接下来就是发起函数调用。

1
2
3
4
5
6
public String calls(){
String arg1 = "aid=01A-khBWIm48A079Pz_DMW6PyZR8uyTumcCNm4e8awxyC2ANU.&cfrom=28B5295010&cuid=5999578300&noncestr=46274W9279Hr1X49A5X058z7ZVz024&platform=ANDROID&timestamp=1621437643609&ua=Xiaomi-MIX2S__oasis__3.5.8__Android__Android10&version=3.5.8&vid=1019013594003&wm=20004_90024";
Boolean arg2 = false;
String ret = NativeApi.newObject(null).callJniMethodObject(emulator, "s([BZ)Ljava/lang/String;", arg1.getBytes(StandardCharsets.UTF_8), arg2).getValue().toString();
return ret;
}

4.2 发起调用

我们想对s发起调用,因为它的返回值是字符串,是对象,所以用callJniMethodObject,如果返回值是 int 就用callJniMethodInt,其他返回值类型的调用方法同理类似。

如果s是静态方法,那么自然不需要 newObject,调用则通过 callStaticJNIMethodXXX 系列函数。

接下来看看方法的具体参数,参数 1 是模拟器实例,参数 2 是方法签名,接着是可变参数,用于传入调用方法的参数。

1
2
3
String arg1 = "aid=01A-khBWIm48A079Pz_DMW6PyZR8uyTumcCNm4e8awxyC2ANU.&cfrom=28B5295010&cuid=5999578300&noncestr=46274W9279Hr1X49A5X058z7ZVz024&platform=ANDROID&timestamp=1621437643609&ua=Xiaomi-MIX2S__oasis__3.5.8__Android__Android10&version=3.5.8&vid=1019013594003&wm=20004_90024";
Boolean arg2 = false;
String ret = NativeApi.newObject(null).callJniMethodObject(emulator, "s([BZ)Ljava/lang/String;", arg1.getBytes(StandardCharsets.UTF_8), arg2).getValue().toString();

如果目标函数是动态加载的,则参数 2 必须是方法签名;如果目标函数是静态加载的,那么直接用传入函数名即可,不需要写方法签名。

4.3 参数传递

回到调用代码,在 Unidbg 中如何传递函数的入参?

1
2
3
4
5
6
public String calls(){
String arg1 = "aid=01A-khBWIm48A079Pz_DMW6PyZR8uyTumcCNm4e8awxyC2ANU.&cfrom=28B5295010&cuid=5999578300&noncestr=46274W9279Hr1X49A5X058z7ZVz024&platform=ANDROID&timestamp=1621437643609&ua=Xiaomi-MIX2S__oasis__3.5.8__Android__Android10&version=3.5.8&vid=1019013594003&wm=20004_90024";
Boolean arg2 = false;
String ret = NativeApi.newObject(null).callJniMethodObject(emulator, "s([BZ)Ljava/lang/String;", arg1.getBytes(StandardCharsets.UTF_8), arg2).getValue().toString();
return ret;
}

参数 1 是字符串,参数 2 是布尔值,直接传递即可,不需要做其他处理,两大类参数都可以直接传递。

  • 基本类型直接传递,int、long、boolean、double 等。

  • 下面几种对象类型也可以直接传递

    • String

    • byte 数组

    • short 数组

    • int 数组

    • float 数组

    • double 数组

    • Enum 枚举类型

除此之外还有许多种可能的参数,比如字符串数组、二维数组、Android Context/Application、HashMap 等等,在大体上遵循两类处理办法。

  1. 如果是 JDK 中包含的类库和方法,比如二维数组、字符串数组、HashMap 等等,直接构造然后使用ProxyDvmObject.createObject(vm, obj);构造出对象。除此之外比如 Okhttp3 之类的第三方类库,导入到本地环境里,也可以使用这个办法。
  2. 如果是 JDK 中无法包含的类库,比如 Android FrameWork 以及样本自定义的类库,通过resolveClass(className).newObject处理,就像本节的NativeApi那样处理。

五、参考

[Unidbg 的基本使用(一)](https://www.yuque.com/lilac-2hqvv/xdwlsg/gmbn45b5n6g59kks?# 《Unidbg 的基本使用(一)》)


从案例中学习unidbg的使用(一)
http://example.com/2024/12/05/Unidbg模拟执行/从案例中学习unidbg的使用(一)/
作者
gla2xy
发布于
2024年12月5日
许可协议