so层调用Java自写类

处理so调用自写的Java类(补环境)

主要解决so层代码调用了自写的Java层类

将自写的Java类,放到unidbg工程中

包名最好与原包名一致

类中有用到Android相关类,需要用Java去实现,比如自写的类里有Log.d,就不能import,需要自行实现Log方法(只要不报错能跑起来就行)

1
2
3
4
5
class Log {
public static void d(String a, String b){
System.out.println(a + "\t" + b);
}
}

代码不需要完全一致,只需函数处理结果符合预期即可

访问修饰符的问题

1
2
3
jni调用Java函数不用管访问修饰符
unidbg用Java开发,调用函数时需要注意访问修饰符
解决方法可以用反射,或者直接把方法改成public

demo

比如,那么首先在代码里就需要加载这个类,这是so文件能运行的基础,而且也要加载触发so层该函数的Java类(一般这个类是定义 native 方法的类)

首先,so层的native函数testJniFunc()调用了Java层的com.DemoJohn.ndkdemo,那么要想触发testJniFunc()函数,就需要加载定义了native testJniFunc()函数的类,并且执行加载好的类里的testJniFunc()函数

这里是com/DemoJohn/ndkdemo/MainActivity类定义了public native String testJniFunc();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CallNDKDemo extends AbstractJni {
CallNDKDemo(boolean logging) {
...;
DalvikModule dm = vm.loadLibrary(new File("xxx.so"), false);//加载so文件
...;
MainActivity = vm.resolveClass("com/DemoJohn/ndkdemo/MainActivity");//加载目标类
}
public static void main(String[] args) throws Exception {
// main函数里实例化当前类,然后调用目标类的native方法
CallNDKDemo test = new CallNDKDemo(true);
test.callFunc();
test.destroy();
}

void callFunc() {
StringObject strResult = MainActivity.callStaticJniMethodObject(emulator, "testJniFunc()Ljava/lang/String;"); // 执行Jni方法
System.out.println(strResult.getValue());
}

然后运行,会提示jni方法报错,因为这个时候还没有接管对应的JNI函数,其实补so的过程很多时候就是在做jni函数的处理,报错哪个jni函数就接管哪个jni函数就行了,比如这里报错了so层的callObjectMethod函数,就把当前类继承AbstractJni类(上面的demo已经继承了),然后在构造函数里添加vm.setJni(this);,之后就只管加接管的jni函数就行,如果接管的jni函数不是那么重要,不影响加密结果,可以直接返回随意数据都行,这里是调用vm.resolveClass("java/lang/ClassLoader").newObject(null);直接返回一个默认对象

1
2
3
4
5
6
7
8
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
if ("java/lang/Class->getClassLoader()Ljava/lang/ClassLoader;".equals(signature)) {
return vm.resolveClass("java/lang/ClassLoader").newObject(null);
}
// 记得调用父类AbstractJni的callObjectMethodV方法
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

然后会报错so层的allocObject()方法找不到自写的Java类,这个时候当然找不到,因为目录下就没有这个类,就需要把自写的Java类给放到unidbg的目录下,包名不要改,属性和方法全给改成public,然后接管allocObject()方法,同样的,后面的newObject()方法也报错,一样去接管该方法,需要注意的是newObject()方法是需要实例化出来一个ndkDemo对象的,并且还要把对象转换成DvmObject类型,这个就不能再随便返回值了

1
2
3
4
5
6
7
8
9
10
@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
if("com/DemoJohn/ndkdemo/NDKDemo-><init>(Ljava/lang/String;I)V".equals(signature)){
StringObject str = vaList.getObjectArg(0);
int nums = vaList.getIntArg(1);
NDKDemo ndkDemo = new NDKDemo(str.getValue(), nums);
return vm.resolveClass("com/DemoJohn/ndkdemo/NDKDemo").newObject(ndkDemo);
}
return super.newObjectV(vm, dvmClass, signature, vaList);
}

然后接管getStaticObjectField()setObjectField()方法,这里的signature就是so层和Java层对应函数的FieldID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
if ("com/DemoJohn/ndkdemo/NDKDemo->privateStaticStringField:Ljava/lang/String;".equals(signature)){
return new StringObject(vm, NDKDemo.privateStaticStringField);
}
return super.getStaticObjectField(vm, dvmClass, signature);
}

@Override
public void setObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature, DvmObject<?> value) {
if("com/DemoJohn/ndkdemo/NDKDemo->privateStringField:Ljava/lang/String;".equals(signature)) {
NDKDemo ndkDemo = (NDKDemo)dvmObject.getValue();
ndkDemo.privateStringField = (String) value.getValue();
return;
}
super.setObjectField(vm, dvmObject, signature, value);
}

有的接管的函数存在重载,怎么确定要接管哪个函数?看报错信息里的函数签名,然后根据函数签名来确定接管哪个函数

处理so与系统的交互

前面补的so环境是针对Java层的交互,so层用JNI去调用了Java层的东西,模拟的时候只有so但是没有Java代码,就需要在unidbg里给so层返回其想要的数据

文件访问就是针对so层和系统交互,so访问了Android系统的某个文件,unidbg肯定也需要模拟这个文件,这里解决的就是怎么模拟文件

文件访问 IO重定向

比如so层通过fopen(“/proc/self/maps”,”r”)访问了系统的文件,有时候可能可以跑起来,比如针对这个文件名,unidbg自带有一个虚拟的maps文件,但是这个虚拟的maps文件很容易检测,所以还是需要来模拟出一个我们自己的maps文件

首先把Android系统下的maps文件给pull到PC上去

然后把主类继承IOResolver接口,实现里面的resolve方法,这样当so有访问文件的操作时就会触发这个方法

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
public class NativeHelper extends AbstractJni implements IOResolver {

private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass NativeHelper;
private final boolean logging;

NativeHelper(boolean logging) {
...;
// 注册resolver
emulator.getSyscallHandler().addIOResolver(this);
}

@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
if (("/proc/self/maps").equals(pathname)) {
// 通过读文件的方法返回文件内容,new File()里的参数是maps文件在PC上的路径
return FileResult.success(new SimpleFileIO(oflags, new File("./maps"), pathname));
// 也可以直接返回字符串
// return FileResult.success(new ByteArrayFileIO(oflags, pathname, "DemoTestmaps".getBytes()));
}
//emulator.attach().debug();
System.out.println("DemoTest: " + pathname);
return null;
}
}

建议补so环境的时候,都去做一下文件访问的hook,这样可以知道so访问了哪些文件

rootfs虚拟文件系统

除了用上面的方法通过代码去hook,也可以通过rootfs虚拟文件系统

创建模拟器的时候,设置RootDir,然后将对应文件按照Android系统路径存放即可,通过这样设置,unidbg-android/src/test/java/com/xxxxx/ndk/rootfs路径会被unidbg认为是Android上的/路径

1
2
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.xxxxx.app")
.setRootDir(new File("unidbg-android/src/test/java/com/xxxxx/ndk/rootfs")).build();

获取当前RootDir的位置

1
2
emulator.getFileSystem().getRootDir();
// 上面的方法返回的路径是Windows上的路径,这个路径会被unidbg认为是Android系统的根路径,也就是Android上的/目录

运行之后,unidbg-android/src/test/java/com/xxxxx/ndk/rootfs下会生成很多文件,这些就是虚拟文件系统,然后在rootfs目录下放上proc/self/maps文件即可

如果上面的方法和虚拟文件系统方法同时存在,IO重定向优先级更高。而且注意补maps文件必须要使用IO重定向方式,因为unidbg中有默认的maps,即使使用了虚拟文件系统也不管用

环境变量

全局变量

全局变量的设置与获取

1
2
emulator.get(...)
emulator.set(...)

unidbg无法模拟子线程,也就是unidbg运行到pthread_create的时候不会报错,也不会返回任何东西,相当于不运行,这种时候就需要hook创建子线程的函数,然后手动返回相应的值

环境变量

可以通过getenv获取app相应的环境变量

unidbg提供了对环境变量的初始化,定义在/src/main/java/com/github/unidbg/linux/AndroidElfLoader.java中,也就是unidbg自带了一些环境变量,比如$PATH变量,但是自带的和原生Android系统的肯定不太一样,有时候还得去hook

1
2
3
4
5
this.environ = initializeTLS(new String[] {
"ANDROID_DATA=/data",
"ANDROID_ROOT=/system",
"PATH=/sbin.....";
});

通过libc里的setenv设置环境变量,但是这个会被上面的unidbg源码里自带的覆盖,如果想用的话得把上面代码里的path行给删掉,或者就改上面的源码也行

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws Exception {
NativeHelper test = new NativeHelper(true);
test.callFunc();
test.destroy();
}

void callFunc() {
Symbol setenv = module.findSymbolByName("setenv", true);
setenv.call(emulator, "PATH", "/sbin:/system/sbin:/product/bin:/apex/com.android.runtime/bin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin11111", 0);
...;
}

hook getenv来设置环境变量,getenv是libc里的函数,hook这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void callFunc() {
emulator.attach().addBreakPoint(module.findSymbolByName("getenv").getAddress(), new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext context = emulator.getContext();
String key = context.getPointerArg(0).getString(0);
// 打印so文件获取了哪些环境变量
System.out.println("DemoTest getenv: " + key);
return true;
}
});

NativeHelper.callStaticJniMethod(emulator, "readSomething()");
}

建议与系统交互的函数都先hook一下,防止出现错误而unidbg没有打印错误信息而导致无法确认错误所在

Hook Listener

仿照SystemPropertyHook来写,该类在源码里面

其实也是个hook,这里以hook getenv函数为例

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
package com.DemoTest.ndk;

import com.github.unidbg.Emulator;
import com.github.unidbg.arm.Arm64Hook;
import com.github.unidbg.arm.ArmHook;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.hook.HookListener;
import com.github.unidbg.memory.SvcMemory;
import com.sun.jna.Pointer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


public class GetEnvHook implements HookListener {

private final Emulator<?> emulator;

public GetEnvHook(Emulator<?> emulator) {
this.emulator = emulator;
}

@Override
public long hook(SvcMemory svcMemory, String libraryName, String symbolName, final long old) {
if ("libc.so".equals(libraryName)) {
if ("getenv".equals(symbolName)) {
if (emulator.is64Bit()) {
return svcMemory.registerSvc(new Arm64Hook() {
@Override
protected HookStatus hook(Emulator<?> emulator) {
RegisterContext context = emulator.getContext();
int index = 0;
Pointer pointer = context.getPointerArg(index);
String key = pointer.getString(0);
System.out.println("Hook: " + key);
// 调用原函数
return HookStatus.RET(emulator, old);
}
}).peer;
} else {
return svcMemory.registerSvc(new ArmHook() {
@Override
protected HookStatus hook(Emulator<?> emulator) {
RegisterContext context = emulator.getContext();
int index = 0;
Pointer pointer = context.getPointerArg(index);
String key = pointer.getString(0);
System.out.println("Hook: " + key);
return HookStatus.RET(emulator, old);
}
}).peer;
}
}

}
return 0;
}
}

把上面的类和运行so的类放在同一路径下,然后在运行so的类中注册

1
2
GetEnvHook getEnvHook = new GetEnvHook(emulator);
memory.addHookListener(getEnvHook);

针对某些版本的Android模拟,可以使用如下代码hook,import一下包

1
2
3
4
5
6
7
8
9
10
11
12
SystemPropertyHook systemPropertyHook = new SystemPropertyHook(emulator);
systemPropertyHook.setPropertyProvider(new SystemPropertyProvider() {
@Override
public String getProperty(String key) {
System.out.println("DemoTest __system_property_get: " + key);
switch (key) {

}
return "";
};
});
memory.addHookListener(systemPropertyHook);

系统属性获取的前两个获取不需要管,是libc的初始化,与so样本无关

其他和系统交互的函数

hook popen 监控so是否使用该函数,popen会执行系统命令,然后把执行命令的结果给写入到文件里,unidbg里默认的对popen的处理不是很好,有时候无法把执行成功,不会报错但是执行流程会跳过popen那里,这里一般建议是如果popen那里不是很重要的逻辑的话,直接跳过去不执行

1
2
3
4
5
6
7
// popen执行命令
const char* command = "...";
FILE* file = popen(command, "r");
// 有时候不会走这里的if流程
if (file) {
...
}

判断popen是否被调用

1
2
3
4
5
6
7
8
9
emulator.attach().addBreakPoint(module.findSymbolByName("popen").getAddress(), new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext context = emulator.getContext();
String command = context.getPointerArg(0).getString(0);
System.out.println("popen command: " + command);
return true;
}
});

跳过该行汇编代码,popen在汇编里的

1
2
3
4
5
6
7
8
// BL .popen
emulator.attach().addBreakPoint(module.base + 0x26E4, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_PC, address + 4);
return true;
}
});

Linux内核的syscall

unidbg里实现了一些常用的系统调用,有些还需要自己处理

系统调用号与系统函数对应

1
https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

libc的函数实际上就是对系统函数的封装

unidbg对syscall函数的封装类在/src/main/java/com/github/unidbg/linux/ARM32SyscallHandlerARM64SyscallHandler

补环境的话新建一个类,继承上面的类,至少实现构造函数和handleUnknownSyscall()方法,也就是如果ARM32SyscallHandlerARM64SyscallHandler类里都没匹配到调用号,就会走handleUnknownSyscall()流程

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
package com.DemoTest.ndk;

import com.github.unidbg.Emulator;
import com.github.unidbg.arm.context.EditableArm32RegisterContext;
import com.github.unidbg.linux.ARM64SyscallHandler;
import com.github.unidbg.memory.SvcMemory;
import com.sun.jna.Pointer;

public class MySyscallHandler extends ARM64SyscallHandler {

public MySyscallHandler(SvcMemory svcMemory) {
super(svcMemory);
}

@Override
protected boolean handleUnknownSyscall(Emulator<?> emulator, int NR) {
EditableArm32RegisterContext context = emulator.getContext();
if (NR == 114) {
int pid = context.getR0Int();
Pointer wstatus = context.getR1Pointer();
int options = context.getR2Int();
Pointer rusage = context.getR3Pointer();
System.out.println("wait4 pid=" + pid + ", wstatus=" + wstatus + ", options=0x" + Integer.toHexString(options) + ", rusage=" + rusage);
return true;
}
return super.handleUnknownSyscall(emulator, NR);
}
}

在hook类中注册,实际上就是要重写build()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
AndroidEmulatorBuilder builder = new AndroidEmulatorBuilder(true) {
public AndroidEmulator build() {
return new AndroidARM64Emulator(processName, rootDir, backendFactories) {
@Override
protected UnixSyscallHandler<AndroidFileIO> createSyscallHandler(SvcMemory svcMemory) {
return new MySyscallHandler(svcMemory);
}
};
}
};
emulator = builder.setProcessName("com.DemoTest.app")
.setRootDir(new File("unidbg-android/src/test/java/com/DemoTest/ndk/rootfs"))
.build();