抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

跟着 [Unidbg 的基本使用(三)](https://www.yuque.com/lilac-2hqvv/xdwlsg/bmf8lm68ffvhrfu0# 《Unidbg 的基本使用(三)》) 学习 unidbg 第三天

目标 APK:right573.apk

目标方法实现:libnet_crypto.so

任务描述

JADX 中反编译 right573.apk,找到 com.izuiyou.network,我们关注于 NetCrypto 类里的 sign 方法,它是一个静态方法。

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
44
45
46
47
48
package com.izuiyou.network;

import android.app.ContextProvider;
import com.meituan.robust.ChangeQuickRedirect;
import com.meituan.robust.PatchProxy;
import com.meituan.robust.PatchProxyResult;
import defpackage.we5;

/* loaded from: classes4.dex */
public class NetCrypto {
public static ChangeQuickRedirect changeQuickRedirect;

static {
we5.a(ContextProvider.get(), "net_crypto");
native_init();
}

public static String a(String str, byte[] bArr) {
String str2;
PatchProxyResult proxy = PatchProxy.proxy(new Object[]{str, bArr}, null, changeQuickRedirect, true, 47387, new Class[]{String.class, byte[].class}, String.class);
if (proxy.isSupported) {
return (String) proxy.result;
}
String sign = sign(str, bArr);
if (str.contains("?")) {
str2 = "&sign=" + sign;
} else {
str2 = "?sign=" + sign;
}
return str + str2;
}

public static native byte[] decodeAES(byte[] bArr, boolean z);

public static native byte[] encodeAES(byte[] bArr);

public static native String generateSign(byte[] bArr);

public static native String getProtocolKey();

public static native void native_init();

public static native boolean registerDID(byte[] bArr);

public static native void setProtocolKey(String str);

public static native String sign(String str, byte[] bArr);
}

这里使用 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 NetCrypto = Java.use("com.izuiyou.network.NetCrypto");
let arg1 = "hello world";
let arg2 = "V I 50";
let ret = NetCrypto.sign(arg1, stringToBytes(arg2));
console.log("ret:"+ret);
})

ret: v2-b94195d5f3c2ad3a876f13346fa283a0

本篇的目标是使用 Unidbg 复现对 sign 的调用。

初始化

在 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
package com.izuiyou;

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 NetWork extends AbstractJni {
private final AndroidEmulator emulator;
private final DvmClass NetCrypto;
private final VM vm;

public NetWork() {
emulator = AndroidEmulatorBuilder
.for32Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName("cn.xiaochuankeji.tieba")
.build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/zuiyou/right573.apk"));
vm.setJni(this);
vm.setVerbose(true);
DalvikModule dm = vm.loadLibrary("net_crypto", true);
NetCrypto = vm.resolveClass("com.izuiyou.network.NetCrypto");
dm.callJNI_OnLoad(emulator);
}

public static void main(String[] args) {
NetWork nw = new NetWork();
}
}

样本 lib 目录下只包含 armeabi-v7a 的动态库文件,因此这里只能选择 32 位,运行代码。

1
2
3
4
5
6
7
8
9
10
11
 INFO [com.github.unidbg.linux.AndroidElfLoader] (AndroidElfLoader:481) - libnet_crypto.so load dependency libandroid.so failed
JNIEnv->FindClass(com/izuiyou/network/NetCrypto) was called from RX@0x12049fc5[libnet_crypto.so]0x49fc5
JNIEnv->RegisterNatives(com/izuiyou/network/NetCrypto, RW@0x121c9010[libnet_crypto.so]0x1c9010, 8) was called from RX@0x12049fd7[libnet_crypto.so]0x49fd7
RegisterNative(com/izuiyou/network/NetCrypto, native_init()V, RX@0x1204a069[libnet_crypto.so]0x4a069)
RegisterNative(com/izuiyou/network/NetCrypto, encodeAES([B)[B, RX@0x1204a0b9[libnet_crypto.so]0x4a0b9)
RegisterNative(com/izuiyou/network/NetCrypto, decodeAES([BZ)[B, RX@0x1204a14d[libnet_crypto.so]0x4a14d)
RegisterNative(com/izuiyou/network/NetCrypto, sign(Ljava/lang/String;[B)Ljava/lang/String;, RX@0x1204a28d[libnet_crypto.so]0x4a28d)
RegisterNative(com/izuiyou/network/NetCrypto, getProtocolKey()Ljava/lang/String;, RX@0x1204a419[libnet_crypto.so]0x4a419)
RegisterNative(com/izuiyou/network/NetCrypto, setProtocolKey(Ljava/lang/String;)V, RX@0x1204a479[libnet_crypto.so]0x4a479)
RegisterNative(com/izuiyou/network/NetCrypto, registerDID([B)Z, RX@0x1204a4f5[libnet_crypto.so]0x4a4f5)
RegisterNative(com/izuiyou/network/NetCrypto, generateSign([B)Ljava/lang/String;, RX@0x1204a587[libnet_crypto.so]0x4a587)

“libnet_crypto.so 试图加载 libandroid.so 这个依赖库,但没找到”,原因在于 Unidbg 并未完全实现所有 Android 系统库。libandroid.so 是一个依赖性较高的系统库,涉及众多 Android 系统功能(如 APK 资源访问、系统调用等),但 Unidbg 并未在 /src/main/resources/sdkxx/lib 目录下提供完整实现。Android 提供了大量的系统库,很多库依赖于系统硬件、软硬件接口和复杂的系统调用,Unidbg 作为模拟器难以完全模拟这些功能。

Unidbg 只加载了依赖较少、使用频率高的库函数,如 libandroid.solibart.solibjnigraphics.so 等无法完全加载的库被实现为虚拟模块。虚拟模块提供了对部分函数的模拟实现,从而解决了类似 libandroid.so 这类系统库的加载问题。

在使用 Unidbg 时,如果遇到缺少系统库(如 libandroid.so)的情况,可以通过引入虚拟模块来解决。虚拟模块机制允许 Unidbg 模拟这些库的一部分函数,从而确保程序能够正常运行。具体来说,虚拟模块在加载目标 SO 之前,模拟库的部分功能,避免了无法加载的依赖问题。

在 31 行之后插入

1
new AndroidModule(emulator, vm).register(memory);;

虚拟模块的原理

虚拟模块本质上是通过 Hook 和模拟某些函数来解决依赖问题。它们的设计较为优雅,能有效地在模拟环境中填补缺失的功能。以 libandroid.so 为例,虚拟模块只实现了其中的一小部分函数:

  • AAssetManager_fromJava
  • AAsset_close
  • AAsset_getBuffer
  • AAsset_getLength
  • AAsset_read

这些仅为 libandroid.so 中数百个导出函数中的一部分,虽然无法提供完整的模拟,但在实际使用中,已经能够应对大多数场景。对于其他系统库,如 libjnigraphics.solibmediandk.so,也面临类似的问题。

发起调用

因为是静态函数,而且返回值是字符串,所以采用 callStaticJniMethodObject 发起调用。

除此之外,参数 1 和 2 是字符串和字节数组类型,都属于 Unidbg 会替我们处理类型转换的类型。

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
44
45
46
47
48
49
50
51
52
package com.izuiyou;

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 com.github.unidbg.virtualmodule.android.AndroidModule;

import java.io.File;
import java.nio.charset.StandardCharsets;

public class NetWork extends AbstractJni {
private final AndroidEmulator emulator;
private final DvmClass NetCrypto;
private final VM vm;

public NetWork() {
emulator = AndroidEmulatorBuilder
.for32Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName("cn.xiaochuankeji.tieba")
.build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/zuiyou/right573.apk"));
vm.setJni(this);
vm.setVerbose(true);
// 使用 libandroid.so 虚拟模块
new AndroidModule(emulator, vm).register(memory);;
DalvikModule dm = vm.loadLibrary("net_crypto", true);
NetCrypto = vm.resolveClass("com.izuiyou.network.NetCrypto");
dm.callJNI_OnLoad(emulator);
}

public String callSign(){
String arg1 = "hello world";
byte[] arg2 = "V I 50".getBytes(StandardCharsets.UTF_8);
String ret = NetCrypto.callStaticJniMethodObject(emulator, "sign(Ljava/lang/String;[B)Ljava/lang/String;", arg1, arg2).getValue().toString();
return ret;
}

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

补 JNI 环境

直接运行报错: Unidbg 为了提醒我们补 JNI 环境,主动抛出异常。

1
2
3
4
5
6
7
8
9
10
JNIEnv->FindClass(com/izuiyou/common/base/BaseApplication) was called from RX@0x1204da21[libnet_crypto.so]0x4da21
JNIEnv->GetStaticMethodID(com/izuiyou/common/base/BaseApplication.getAppContext()Landroid/content/Context;) => 0x2157b33c was called from RX@0x1204da57[libnet_crypto.so]0x4da57
[10:23:42 715] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987556, svcNumber=0x170, PC=unidbg@0xfffe0794, LR=RX@0x1204db2f[libnet_crypto.so]0x4db2f, syscall=null
java.lang.UnsupportedOperationException: com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:504)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:438)
at com.github.unidbg.linux.android.dvm.DvmMethod.callStaticObjectMethodV(DvmMethod.java:59)
at com.github.unidbg.linux.android.dvm.DalvikVM$113.handle(DalvikVM.java:1815)
at com.github.unidbg.linux.ARM32SyscallHandler.hook(ARM32SyscallHandler.java:131)
at com.github.unidbg.arm.backend.Unicorn2Backend$11.hook(Unicorn2Backend.java:352)

异常分析

第一部分

1
[10:23:42 715]  WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987556, svcNumber=0x170, PC=unidbg@0xfffe0794, LR=RX@0x1204db2f[libnet_crypto.so]0x4db2f, syscall=null

Unidbg 中,JNI 调用和系统调用都通过软中断 SVC 发起,imm 值用来区分它们。svcNumber=0x170 是 SVC 软中断后跟随的值(imm)。Unicorn 等 CPU 模拟器会拦截这些中断,进而 Unidbg 可以接管这些中断,并对 JNI 调用做模拟。根据 imm 是否为 0,Unidbg 确认逻辑应该导向模拟的 JNI 函数还是模拟的系统调用。换句话说,在 Unidbg 里,JNI 函数被提升到了和系统调用相同的层级,因此 JNI 报错也发生在 syscallHandler 里面。

异常日志中的 NR 为负数和 syscall=null 表明系统调用未被正确处理。

PC (Program Counter): PC=unidbg@0xfffe0794 是当前指令的地址,指向了即将执行的下一条指令。

LR (Link Register): LR=RX@0x4004db2f[libnet_crypto.so]0x4db2f 是返回地址,也就是当调用完成后,程序应该返回的位置。这个地址通常指向调用 JNI 函数的地方。

SVC 跳板函数:SVC(Supervisor Call)是一种系统调用机制,Unidbg 利用它在模拟过程中进行底层操作,替代了真实环境中 JNI 函数的实现。

第二部分是其余部分

1
2
3
4
5
6
7
java.lang.UnsupportedOperationException: com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:504)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:438)
at com.github.unidbg.linux.android.dvm.DvmMethod.callStaticObjectMethodV(DvmMethod.java:59)
at com.github.unidbg.linux.android.dvm.DalvikVM$113.handle(DalvikVM.java:1815)
at com.github.unidbg.linux.ARM32SyscallHandler.hook(ARM32SyscallHandler.java:131)
at com.github.unidbg.arm.backend.Unicorn2Backend$11.hook(Unicorn2Backend.java:352)

首先是 Unidbg 无法处理的函数其签名 com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;,这个格式很清晰,即 com/izuiyou/common/base/BaseApplication 类的 getAppContext 方法。

再看它的调用栈,来自于 callStaticObjectMethodV 这个 JNI 方法。接下来讨论为什么要补 JNI 环境。

补环境的原因

Unidbg 的工作原理是通过构造一个模拟的 JNIEnvJavaVM 环境,并结合 SVC(系统调用)跳板函数来拦截和模拟 JNI 函数的调用。

但到底如何去模拟 JNI 函数,这是问题的核心。Unidbg 实现了一套 JNI 处理逻辑,当遇到 JNI 调用时,如果是 FindClassNewGlobalRefGetObjectClassGetMethodIDGetStringLengthGetStringCharsReleaseStringChars 等等函数,这些函数的处理相对简单,因为它们不涉及复杂的内存或数据操作,而是围绕类和对象的基本操作,Unidbg 可以在没有 Dex 或 Apk 的情况下,根据内部的数据结构或模拟的类来完成这些调用。

那么补 JNI 环境的需求在哪里?在于如果是 callStaticObjectMethodcallObjectMethodVgetObjectFieldgetStaticIntField 等 JNI 函数,**这些函数对样本的 Java 层数据做访问,Unidbg 既未运行 Dex,更没运行 Apk,Unidbg 需要模拟这些 JNI 函数的返回值和数据,而这就需要依赖于使用者提供额外的外部信息。

举个例子,下面是一个小 demo。通过 JNI 访问了样本的 com.example.jnidemo.Utils 类里的静态字段 country

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_jnidemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
jclass clz = env->FindClass("com/example/jnidemo/Utils");
jfieldID jfieldId = env->GetStaticFieldID(clz, "country", "Ljava/lang/String;");
jstring ret = static_cast<jstring>(env->GetStaticObjectField(clz, jfieldId));
const char* c_str = env->GetStringUTFChars(ret, nullptr);
return env->NewStringUTF(c_str);
}

Utils 类的代码如下

1
2
3
4
5
package com.example.jnidemo;

public class Utils {
public static String country = "China";
}

Unidbg 只模拟执行 Native 层,自然无法无法感知和获取到 Utils 类以及其中的 country 字段,所以必须交由使用者自行处理,比如在 JADX 中反编译样本,找到 Utils 类并静态分析代码,获悉 country 字段的值,比如直接 Frida Hook 查看等等。

因为担心用户意识不到补 JNI 的需求,所以通过抛出报错和打印堆栈予以提醒。

需要注意,并不是所有基于 JNI 发起的函数调用、字段访问都必须由用户处理,其中有部分可以被预处理。举一些例子

  • Android PackageManager 类里的 GET_SIGNATURES 静态字段 ,它的值固定是 64
  • String 类的 getBytes 方法
  • List 实现类的 size 方法

为了减少用户的补 JNI 负担,所以 Unidbg 预处理了数百个常见的 JNI 函数调用和字段访问,逻辑位于 src/main/java/com/github/unidbg/linux/android/dvm/AbstractJni.java 类里。

比如 callBooleanMethodV,调用返回布尔值的实例方法,预处理了如下方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public boolean callBooleanMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "java/util/Enumeration->hasMoreElements()Z":
return ((Enumeration) dvmObject).hasMoreElements();
case "java/util/ArrayList->isEmpty()Z":
return ((ArrayListObject) dvmObject).isEmpty();
case "java/util/Iterator->hasNext()Z":
Object iterator = dvmObject.getValue();
if (iterator instanceof Iterator) {
return ((Iterator<?>) iterator).hasNext();
}
case "java/lang/String->startsWith(Ljava/lang/String;)Z":{
String str = (String) dvmObject.getValue();
StringObject prefix = vaList.getObjectArg(0);
return str.startsWith(prefix.value);
}
}

throw new UnsupportedOperationException(signature);
}

比如 getObjectField,访问对象类型的实例字段,做了如下处理。

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
@Override
public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
if ("android/content/pm/PackageInfo->signatures:[Landroid/content/pm/Signature;".equals(signature) &&
dvmObject instanceof PackageInfo) {
PackageInfo packageInfo = (PackageInfo) dvmObject;
if (packageInfo.getPackageName().equals(vm.getPackageName())) {
CertificateMeta[] metas = vm.getSignatures();
if (metas != null) {
Signature[] signatures = new Signature[metas.length];
for (int i = 0; i < metas.length; i++) {
signatures[i] = new Signature(vm, metas[i]);
}
return new ArrayObject(signatures);
}
}
}
if ("android/content/pm/PackageInfo->versionName:Ljava/lang/String;".equals(signature) &&
dvmObject instanceof PackageInfo) {
PackageInfo packageInfo = (PackageInfo) dvmObject;
if (packageInfo.getPackageName().equals(vm.getPackageName())) {
String versionName = vm.getVersionName();
if (versionName != null) {
return new StringObject(vm, versionName);
}
}
}

throw new UnsupportedOperationException(signature);
}

这两个调用是获取 Apk 签名信息以及版本信息,Unidbg 之所以能处理,依赖我们传入的 APK,解析出了这些信息。

在 AbstractJNI 中,下面四类处理为主

  • App 基本信息与签名
  • JDK 加密解密与数字签名
  • 字符串和容器类型
  • Android FrameWork 类库

第一类就比如上面的两个,但不止于此,加载和解析 APK 让 Unidbg 获取了大量的信息。

比如处理获取包名

1
2
3
4
5
6
7
8
9
10
@Override
public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
if ("android/app/ActivityThread->currentPackageName()Ljava/lang/String;".equals(signature)) {
String packageName = vm.getPackageName();
if (packageName != null) {
return new StringObject(vm, packageName);
}
}
throw new UnsupportedOperationException(signature);
}

第二类是 JAVA 加解密和数字签名相关的方法,它主要服务于 Apk 签名校验,出现频率很高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case "java/security/KeyFactory->getInstance(Ljava/lang/String;)Ljava/security/KeyFactory;":{
StringObject algorithm = vaList.getObjectArg(0);
assert algorithm != null;
try {
return dvmClass.newObject(KeyFactory.getInstance(algorithm.value));
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
case "javax/crypto/spec/SecretKeySpec-><init>([BLjava/lang/String;)V":{
byte[] key = (byte[]) vaList.getObjectArg(0).value;
StringObject algorithm = vaList.getObjectArg(1);
assert algorithm != null;
SecretKeySpec secretKeySpec = new SecretKeySpec(key, algorithm.value);
return dvmClass.newObject(secretKeySpec);
}

第三类是对基本类型的包装类、字符串、容器类型的处理。

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
@Override
public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature) {
case "java/lang/String-><init>([B)V":
ByteArray array = varArg.getObjectArg(0);
return new StringObject(vm, new String(array.getValue()));
case "java/lang/String-><init>([BLjava/lang/String;)V":
array = varArg.getObjectArg(0);
StringObject string = varArg.getObjectArg(1);
try {
return new StringObject(vm, new String(array.getValue(), string.getValue()));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}

throw new UnsupportedOperationException(signature);
}
case "java/lang/Integer->intValue()I": {
DvmInteger integer = (DvmInteger) dvmObject;
return integer.value;
}
case "java/util/List->size()I": {
List<?> list = (List<?>) dvmObject.getValue();
return list.size();
}
case "java/util/Map->size()I":{
Map<?, ?> map = (Map<?, ?>) dvmObject.getValue();
return map.size();
}

第四类是依赖于 Android FrameWork 的类库,它们没法在普通的 JAVA 环境里良好处理,因此需要做一些粗糙的模拟或占位。

比如 Android 系统服务

1
2
3
4
5
case "android/app/Application->getSystemService(Ljava/lang/String;)Ljava/lang/Object;": {
StringObject serviceName = vaList.getObjectArg(0);
assert serviceName != null;
return new SystemService(vm, serviceName.getValue());
}

SystemService 的实现如下,就是大量的简单占位,调用其中的方法实际获取数据时,还是要使用者来补。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.github.unidbg.linux.android.dvm.api;

import com.github.unidbg.arm.backend.BackendException;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.VM;

public class SystemService extends DvmObject<String> {

public static final String WIFI_SERVICE = "wifi";
public static final String CONNECTIVITY_SERVICE = "connectivity";
public static final String TELEPHONY_SERVICE = "phone";
public static final String ACCESSIBILITY_SERVICE = "accessibility";
public static final String KEYGUARD_SERVICE = "keyguard";
public static final String ACTIVITY_SERVICE = "activity";
public static final String SENSOR_SERVICE = "sensor";
public static final String INPUT_METHOD_SERVICE = "input_method";
public static final String LOCATION_SERVICE = "location";
public static final String WINDOW_SERVICE = "window";
public static final String UI_MODE_SERVICE = "uimode";
public static final String DISPLAY_SERVICE = "display";
public static final String AUDIO_SERVICE = "audio";

public SystemService(VM vm, String serviceName) {
super(getObjectType(vm, serviceName), serviceName);
}

private static DvmClass getObjectType(VM vm, String serviceName) {
switch (serviceName) {
case TELEPHONY_SERVICE:
return vm.resolveClass("android/telephony/TelephonyManager");
case WIFI_SERVICE:
return vm.resolveClass("android/net/wifi/WifiManager");
case CONNECTIVITY_SERVICE:
return vm.resolveClass("android/net/ConnectivityManager");
case ACCESSIBILITY_SERVICE:
return vm.resolveClass("android/view/accessibility/AccessibilityManager");
case KEYGUARD_SERVICE:
return vm.resolveClass("android/app/KeyguardManager");
case ACTIVITY_SERVICE:
return vm.resolveClass("android/os/BinderProxy"); // android/app/ActivityManager
case SENSOR_SERVICE:
return vm.resolveClass("android/hardware/SensorManager");
case INPUT_METHOD_SERVICE:
return vm.resolveClass("android/view/inputmethod/InputMethodManager");
case LOCATION_SERVICE:
return vm.resolveClass("android/location/LocationManager");
case WINDOW_SERVICE:
return vm.resolveClass("android/view/WindowManager");
case UI_MODE_SERVICE:
return vm.resolveClass("android/app/UiModeManager");
case DISPLAY_SERVICE:
return vm.resolveClass("android/hardware/display/DisplayManager");
case AUDIO_SERVICE:
return vm.resolveClass("android/media/AudioManager");
default:
throw new BackendException("service failed: " + serviceName);
}
}

}

在过去几年的更新里,Unidbg 一直在对 AbstractJNI 做扩充,让它预处理更多的逻辑,减少用户的使用负担。

关于 AbstractJNI 可以做两点总结。

  1. 确实能让我们免于一些补环境的苦恼
  2. 相较于所有可能的 JNI 访问,它只是杯水车薪,补 JNI 环境这个步骤不可避免。

基本规范

回到最初的报错上

1
2
3
4
5
6
7
8
9
10
JNIEnv->FindClass(com/izuiyou/common/base/BaseApplication) was called from RX@0x1204da21[libnet_crypto.so]0x4da21
JNIEnv->GetStaticMethodID(com/izuiyou/common/base/BaseApplication.getAppContext()Landroid/content/Context;) => 0x2157b33c was called from RX@0x1204da57[libnet_crypto.so]0x4da57
[10:23:42 715] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987556, svcNumber=0x170, PC=unidbg@0xfffe0794, LR=RX@0x1204db2f[libnet_crypto.so]0x4db2f, syscall=null
java.lang.UnsupportedOperationException: com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:504)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:438)
at com.github.unidbg.linux.android.dvm.DvmMethod.callStaticObjectMethodV(DvmMethod.java:59)
at com.github.unidbg.linux.android.dvm.DalvikVM$113.handle(DalvikVM.java:1815)
at com.github.unidbg.linux.ARM32SyscallHandler.hook(ARM32SyscallHandler.java:131)
at com.github.unidbg.arm.backend.Unicorn2Backend$11.hook(Unicorn2Backend.java:352)

函数签名是 com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context; 根据抛出的异常,它来自 callStaticObjectMethodV 调用。

如何构造和补 Context 是一个特殊问题,下面这个构造方式在绝大多数情况下可行。

1
DvmObject<?> context = vm.resolveClass("android/app/Application", vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(null);

但是这里应该使用如下方法构造 Context

1
DvmObject<?> context = vm.resolveClass("cn/xiaochuankeji/tieba/AppController").newObject(null);

可以在 AbstractJNI 的 callStaticObjectMethodV 里添加对这个方法的处理(不是好办法)。

更惯常的做法让本类继承 AbstractJNI,在创建虚拟机时 vm.setJni 设置为当前类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public NetWork() {
emulator = AndroidEmulatorBuilder
.for32Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName("cn.xiaochuankeji.tieba")
.build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/zuiyou/right573.apk"));
// 设置 JNI
vm.setJni(this);
vm.setVerbose(true);//输出调试
// 使用 libandroid.so 虚拟模块
new AndroidModule(emulator, vm).register(memory);;
DalvikModule dm = vm.loadLibrary("net_crypto", true);
NetCrypto = vm.resolveClass("com.izuiyou.network.NetCrypto");
dm.callJNI_OnLoad(emulator);
}

然后本类里重写 callStaticObjectMethodV 方法,处理目标方法。

1
2
3
4
5
6
7
8
9
10
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature){
case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;":{
DvmObject<?> context = vm.resolveClass("cn/xiaochuankeji/tieba/AppController").newObject(null);
return context;
}
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}

这里要警惕张冠李戴,callStaticObjectMethodV 和 callStaticObjectMethod 是两个不同的 JNI 方法,不要混淆,更不要补错地方哭错坟。

在有些情况下,样本过于复杂,要处理的 JNI 调用太多,也可以创建一个单独的 JNI 处理类

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

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.linux.android.dvm.*;

public class zuiYouJNI extends AbstractJni {
private final AndroidEmulator emulator;

public zuiYouJNI(AndroidEmulator emulator){
this.emulator = emulator;
}

}

在本类 setJNI 也对应改变

1
vm.setJni(new zuiYouJNI(emulator));

开始补环境

处理完这个 JNI 调用

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.izuiyou;

import com.github.unidbg.AbstractEmulator;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.ModuleListener;
import com.github.unidbg.arm.TraceFunctionCall;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.linux.ARM32SyscallHandler;
import com.github.unidbg.linux.ARM64SyscallHandler;
import com.github.unidbg.linux.AndroidElfLoader;
import com.github.unidbg.linux.AndroidSyscallHandler;
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.file.ByteArrayFileIO;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.TreeMap;

public class NetWork extends AbstractJni{
private final AndroidEmulator emulator;
private final DvmClass NetCrypto;
private final VM vm;

public NetWork() {
emulator = AndroidEmulatorBuilder
.for32Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName("cn.xiaochuankeji.tieba")
.build();

Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/zuiyou/right573.apk"));
vm.setJni(this);
vm.setVerbose(true);
// 使用 libandroid.so 的虚拟模块
new AndroidModule(emulator, vm).register(memory);
DalvikModule dm = vm.loadLibrary("net_crypto", true);
NetCrypto = vm.resolveClass("com.izuiyou.network.NetCrypto");
dm.callJNI_OnLoad(emulator);
}

public String callSign(){
String arg1 = "hello world";
byte[] arg2 = "V I 50".getBytes(StandardCharsets.UTF_8);
String ret = NetCrypto.callStaticJniMethodObject(emulator, "sign(Ljava/lang/String;[B)Ljava/lang/String;", arg1, arg2).getValue().toString();
return ret;
}

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


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

新的报错:

1
2
3
4
5
6
7
JNIEnv->GetMethodID(cn/xiaochuankeji/tieba/AppController.getPackageManager()Landroid/content/pm/PackageManager;) => 0xead4ad98 was called from RX@0x1204dbab[libnet_crypto.so]0x4dbab
[09:40:56 740] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987532, svcNumber=0x11f, PC=unidbg@0xfffe0284, LR=RX@0x1204d563[libnet_crypto.so]0x4d563, syscall=null
java.lang.UnsupportedOperationException: cn/xiaochuankeji/tieba/AppController->getPackageManager()Landroid/content/pm/PackageManager;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)
at com.github.unidbg.linux.android.dvm.DalvikVM$32.handle(DalvikVM.java:552)

获取包管理器如何返回?AbstractJNI 里有可供参考的代码。

重写 参考 AbstractJNI 去补 PackageManager。

1
2
3
4
5
6
7
8
9
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature){
case "cn/xiaochuankeji/tieba/AppController->getPackageManager()Landroid/content/pm/PackageManager;":{
return vm.resolveClass("android/content/pm/PackageManager").newObject(null);
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

继续报错:

1
2
3
4
5
6
7
JNIEnv->GetMethodID(cn/xiaochuankeji/tieba/AppController.getPackageName()Ljava/lang/String;) => 0x7a7c20f8 was called from RX@0x1204dc8f[libnet_crypto.so]0x4dc8f
[10:00:10 990] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987532, svcNumber=0x11f, PC=unidbg@0xfffe0284, LR=RX@0x1204d563[libnet_crypto.so]0x4d563, syscall=null
java.lang.UnsupportedOperationException: cn/xiaochuankeji/tieba/AppController->getPackageName()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
at com.izuiyou.NetWork.callObjectMethodV(NetWork.java:77)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

取包名,AbstractJNI 里同样有这个处理逻辑。

直接照抄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature){
case "cn/xiaochuankeji/tieba/AppController->getPackageManager()Landroid/content/pm/PackageManager;":{
return vm.resolveClass("android/content/pm/PackageManager").newObject(null);
}
case "cn/xiaochuankeji/tieba/AppController->getPackageName()Ljava/lang/String;":{
String packageName = vm.getPackageName();
if (packageName != null) {
return new StringObject(vm, packageName);
}
break;
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

继续报错:

1
2
3
4
5
6
JNIEnv->GetMethodID(cn/xiaochuankeji/tieba/AppController.getClass()Ljava/lang/Class;) => 0x98d62f6e was called from RX@0x1204d5eb[libnet_crypto.so]0x4d5eb
[10:04:58 104] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987588, svcNumber=0x11f, PC=unidbg@0xfffe0284, LR=RX@0x1204d563[libnet_crypto.so]0x4d563, syscall=null
java.lang.UnsupportedOperationException: cn/xiaochuankeji/tieba/AppController->getClass()Ljava/lang/Class;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
at com.izuiyou.NetWork.callObjectMethodV(NetWork.java:84)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)

同样在 AbstractJNI 里能找到 getClass 的处理逻辑

照抄

1
2
3
case "cn/xiaochuankeji/tieba/AppController->getClass()Ljava/lang/Class;":{
return dvmObject.getObjectType();
}

继续报错:

1
2
3
4
5
6
7
NIEnv->GetMethodID(java/lang/Class.getSimpleName()Ljava/lang/String;) => 0x76c2b89 was called from RX@0x1204d669[libnet_crypto.so]0x4d669
[10:07:42 055] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987588, svcNumber=0x11f, PC=unidbg@0xfffe0284, LR=RX@0x1204d563[libnet_crypto.so]0x4d563, syscall=null
java.lang.UnsupportedOperationException: java/lang/Class->getSimpleName()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
at com.izuiyou.NetWork.callObjectMethodV(NetWork.java:87)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

这在 AbstractJNI 里可没有,像下面这样处理,AbstractJNI 里找不到 getSimpleName 的处理,但是通过搜索发现,该函数是在获取类的简写名,比如 android.content.Context 简写名就是 ContextDvmClassgetClassName 可以获取类名,我们要截取末尾的名字。

1
2
3
4
5
case "java/lang/Class->getSimpleName()Ljava/lang/String;":{
String className = ((DvmClass) dvmObject).getClassName();
String[] name = className.split("/");
return new StringObject(vm, name[name.length - 1]);
}

继续报错:

1
2
3
4
5
6
JNIEnv->GetMethodID(cn/xiaochuankeji/tieba/AppController.getFilesDir()Ljava/io/File;) => 0xa07dc812 was called from RX@0x1205af55[libnet_crypto.so]0x5af55
[10:08:48 961] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452988524, svcNumber=0x11f, PC=unidbg@0xfffe0284, LR=RX@0x1204d563[libnet_crypto.so]0x4d563, syscall=null
java.lang.UnsupportedOperationException: cn/xiaochuankeji/tieba/AppController->getFilesDir()Ljava/io/File;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
at com.izuiyou.NetWork.callObjectMethodV(NetWork.java:92)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)

进行占位处理即可。

1
2
3
case "cn/xiaochuankeji/tieba/AppController->getFilesDir()Ljava/io/File;":{
return vm.resolveClass("java/io/File").newObject(null);
}

新的报错:

1
2
3
4
5
6
7
8
JNIEnv->GetMethodID(java/io/File.getAbsolutePath()Ljava/lang/String;) => 0xb4553f34 was called from RX@0x1205af99[libnet_crypto.so]0x5af99
[10:10:00 362] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452988524, svcNumber=0x11f, PC=unidbg@0xfffe0284, LR=RX@0x1204d563[libnet_crypto.so]0x4d563, syscall=null
java.lang.NullPointerException
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:307)
at com.izuiyou.NetWork.callObjectMethodV(NetWork.java:95)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)
at com.github.unidbg.linux.android.dvm.DalvikVM$32.handle(DalvikVM.java:552)

这样处理:

1
2
3
case "cn/xiaochuankeji/tieba/AppController->getFilesDir()Ljava/io/File;":{
return vm.resolveClass("java/io/File").newObject("/data/data/cn.xiaochuankeji.tieba/files");
}

报错:

1
2
3
4
5
6
JNIEnv->GetMethodID(java/io/File.getAbsolutePath()Ljava/lang/String;) => 0xb4553f34 was called from RX@0x1205af99[libnet_crypto.so]0x5af99
[10:11:55 595] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452988524, svcNumber=0x11f, PC=unidbg@0xfffe0284, LR=RX@0x1204d563[libnet_crypto.so]0x4d563, syscall=null
java.lang.ClassCastException: java.lang.String cannot be cast to java.io.File
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:306)
at com.izuiyou.NetWork.callObjectMethodV(NetWork.java:95)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)

提示 getAbsolutePath 方法处有问题,无法将 String 类 转成 File 类,需进行如下处理。

1
2
3
case "java/io/File->getAbsolutePath()Ljava/lang/String;":{
return new StringObject(vm, dvmObject.getValue().toString());
}

继续运行,走到了检测 debug 相关的逻辑

1
2
3
4
5
[10:13:21 766]  WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987468, svcNumber=0x173, PC=unidbg@0xfffe07c4, LR=RX@0x12050cf7[libnet_crypto.so]0x50cf7, syscall=null
java.lang.UnsupportedOperationException: android/os/Debug->isDebuggerConnected()Z
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticBooleanMethodV(AbstractJni.java:191)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticBooleanMethodV(AbstractJni.java:186)
at com.github.unidbg.linux.android.dvm.DvmMethod.callStaticBooleanMethodV(DvmMethod.java:184)

返回 false 防止被检测到。

1
2
3
4
5
6
7
8
9
@Override
public boolean callStaticBooleanMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature){
case "android/os/Debug->isDebuggerConnected()Z":{
return false;
}
}
return super.callStaticBooleanMethodV(vm, dvmClass, signature, vaList);
}

继续报错:

1
2
3
4
5
JNIEnv->GetStaticMethodID(android/os/Process.myPid()I) => 0xfb198f3e was called from RX@0x1205b3b5[libnet_crypto.so]0x5b3b5
[10:14:50 350] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987428, svcNumber=0x17e, PC=unidbg@0xfffe0874, LR=RX@0x1205b4a7[libnet_crypto.so]0x5b4a7, syscall=null
java.lang.UnsupportedOperationException: android/os/Process->myPid()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticIntMethodV(AbstractJni.java:211)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticIntMethodV(AbstractJni.java:206)

返回 Unidbg 所生成的 PID

1
2
3
4
5
6
7
8
9
@Override
public int callStaticIntMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature){
case "android/os/Process->myPid()I":{
return emulator.getPid();
}
}
return super.callStaticIntMethodV(vm, dvmClass, signature, vaList);
}

得到结果:

1
2
JNIEnv->NewStringUTF("v2-b94195d5f3c2ad3a876f13346fa283a0") was called from RX@0x1204a373[libnet_crypto.so]0x4a373
call s result:v2-b94195d5f3c2ad3a876f13346fa283a0

补充:

PID 的处理方式:Unidbg 通过从 JVM 获取 PID 并与 0x7fff 运算,确保 PID 不超过 Android 系统的最大值 32768(0x8000)。

平台差异:由于 Linux、MacOS 和 Windows 在 PID 上限上的差异,Unidbg 的这种处理方式确保了跨平台的一致性。

修改 PID 的不便:由于 PID 在 Unidbg 中是 final 变量,要修改它需要修改源码,这使得灵活性受限。

评论