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); ...; MainActivity = vm.resolveClass("com/DemoJohn/ndkdemo/MainActivity"); } public static void main(String[] args) throws Exception { CallNDKDemo test = new CallNDKDemo(true); test.callFunc(); test.destroy(); }
void callFunc() { StringObject strResult = MainActivity.callStaticJniMethodObject(emulator, "testJniFunc()Ljava/lang/String;"); 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); } 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) { ...; emulator.getSyscallHandler().addIOResolver(this); }
@Override public FileResult resolve(Emulator emulator, String pathname, int oflags) { if (("/proc/self/maps").equals(pathname)) { return FileResult.success(new SimpleFileIO(oflags, new File("./maps"), pathname)); } 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();
|
运行之后,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); 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
| const char* command = "..."; FILE* file = popen(command, "r");
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
| 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/ARM32SyscallHandler
和ARM64SyscallHandler
补环境的话新建一个类,继承上面的类,至少实现构造函数和handleUnknownSyscall()
方法,也就是如果ARM32SyscallHandler
和ARM64SyscallHandler
类里都没匹配到调用号,就会走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();
|