Unidbg使用指南 - aeskkkey/reverse GitHub Wiki
个人笔记,转载请注明出处。
unidbg的github地址,基于unicorn来实现指令集的运算。
当前版本0.9.5-SNAPSHOT,其他版本一些[行数]可能对不上,自己查找。
导入工程,下载maven依赖。使用IDEA编辑器的可以参考逆向工具之unidbg,这里不展开讲。
通用步骤如下:
//1. 构建一个模拟器实例
AndroidEmulator emulator = AndroidEmulatorBuilder
.for32Bit() // 使用32位,相对来说,32位指令比64位更便于阅读,ida分析也一般分析32位
.setProcessName("你的app进程名") // 设置进程名称
// .addBackendFactory(new DynarmicFactory(true)) //添加后端,推荐使用Dynarmic,运行速度快,但并不支持某些新特性
.setRootDir(new File("unidbg-android/src/test/java/tmp/root")) // 设置模拟器根目录,文件IO会在该目录下进行,否则在系统临时目录下进行
.build(); // 生成模拟器
//2. 获取操作内存的接口
Memory memory = emulator.getMemory();
//3.设置Android SDK 版本,目前只支持19,23两个版本
memory.setLibraryResolver(new AndroidResolver(23));
//4.创建虚拟机
VM vm = emulator.createDalvikVM();
// 也可以用传入一个apk文件,来生成虚拟机;传入apk,unidbg会帮处理apk的签名、包名、版本号等Manifest信息
// VM vm = emulator.createDalvikVM(new Fil("apk文件路径名")));
//5.选择设置日志输出,vm的Verbose会输出JniEnv函数表的调用信息;其他类的信息通过apache的log4j日志模块查看
vm.setVerbose(true);
Logger.getLogger(ARM32SyscallHandler.class).setLevel(Level.DEBUG);
//6.添加IO处理
emulator.getSyscallHandler().addIOResolver(new MyIOResolver());
//7.添加对Java方法调用的处理
vm.setJni(new MyJni());
//8.加载ELF文件
DalvikModule dm = vm.loadLibrary(new File("so文件路径名"), false);
//9.调用JNI_OnLoad
dm.callJNI_OnLoad(emulator);
//10.获取so模块
Module module = dm.getModule();
System.out.println("module base: 0x" + Long.toHexString(module.base) + ",size 0x" + Long.toHexString(module.size));
//11.调用native方法
DvmClass jniClazz = vm.resolveClass("com.example.temp.JNIUtils".replace(".", "/"));
DvmObject context = vm.resolveClass("android/content/Context").newObject(null);
Object ret = jniClazz.callStaticJniMethodObject(emulator, "getSign(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;", context, "data_str");
第6步中添加了一个对IO的处理,当系统调用open, read, write, seek, mkdir, fstat等和文件相关指令时,都会调用到我们的IO处理里
//6.添加IO处理
emulator.getSyscallHandler().addIOResolver(new MyIOResolver());
unidbg是开源的,有使用不懂的地方,尽量多看源码。例如这里可以看一下open的实现,了解是如何调用到IOResolver的:
- 32位的cpu指令调用基本都在
ARM32SyscallHandler
这个类里,[62行]hook是对系统指令的hook; - [137行]当NR=5时,为open调用;
- [1912行]从模拟器emulator里面读出文件路径pathname和操作码oflags
- 跳到
UnixSyscallHandler
[307行],先调用resolve
找我们实现的Resolver,如果没有自己实现IO,再调用createDriverFileIO
模版如下:
public class MyIOResolver implements IOResolver {
private static final Log log = LogFactory.getLog(MyIOResolver.class);
private static final String ROOT = "unidbg-android/src/test/java/tmp/root";
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
if (log.isDebugEnabled()) {
log.debug("openFile: " + pathname + ", " + Integer.toHexString(oflags));
}
if (pathname == null || pathname.isEmpty()) {
return null;
}
if (("/proc/" + emulator.getPid() + "/status").equals(pathname)) {
return FileResult.success(new ByteArrayFileIO(oflags, pathname, (
"Name:\tcom.example.temp\nState:\tr\nTgid:\t" + emulator.getPid() + "\nPid:\t" + emulator.getPid() + "\nTracerPid:\t0\n").getBytes()));
}
if ("/system/usr/share/zoneinfo/tzdata".equals(pathname)) {
return FileResult.success(new SimpleFileIO(oflags, new File(ROOT, pathname), pathname));
// return FileResult.success(emulator.getFileSystem().createSimpleFileIO(new File(ROOT, pathname), oflags, pathname));
// 两个方法等效
}
if (("/data/user/0/com.example.temp/files").equals(pathname)){
return FileResult.success(new DirectoryFileIO(oflags, pathname, new File(ROOT, pathname)));
// return FileResult.success(emulator.getFileSystem().createDirectoryFileIO(new File(pathname), oflags, pathname));
// 两个方法等效
}
return null;
}
}
可供选择的文件类型大致分为几种:
-
ByteArrayFileIO
,只读文件:
-
构造时传入文件内容bytes,供so读取;可以查看该类定义,当要进行write时,会抛出异常;
-
通常像读/proc里面的信息,基本都可以用这个来构造,返回so想要的内容;
public class ByteArrayFileIO extends BaseAndroidFileIO {
@Override
public int write(byte[] data) {
throw new UnsupportedOperationException();
}
}
-
SimpleFileIO
,可读可写文件
- 会在工程的实际路径里面构造一个文件,然后进行真实的读写IO;
- 通常so会频繁对某个文件进行读写操作时,可以使用该类;
public class SimpleFileIO extends BaseAndroidFileIO implements NewFileIO {
private synchronized RandomAccessFile checkOpenFile() { // 打开文件并进行读写
try {
if (_randomAccessFile == null) {
FileUtils.forceMkdir(file.getParentFile());
if (!file.exists() && !file.createNewFile()) {
throw new IOException("createNewFile failed: " + file);
}
_randomAccessFile = new RandomAccessFile(file, "rws");
onFileOpened(_randomAccessFile);
}
return _randomAccessFile;
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
-
DirectoryFileIO
,不可读不可写文件
- 主要是文件夹、link等文件,so通常通过fstat查看这些文件信息,但是无法进行读写
public class DirectoryFileIO extends BaseAndroidFileIO {
public enum DirentType {
DT_FIFO(1), /* FIFO */
DT_CHR(2), /* character device */
DT_DIR(4), /* directory */
DT_BLK(6), /* block device */
DT_REG(8), /* regular file */
DT_LNK(10), /* symbolic link */
DT_SOCK(12), /* socket */
DT_WHT(14); /* whiteout */
private final byte type;
DirentType(int type) {
this.type = (byte) type;
}
}
}
- 其他文件类型,unidbg在
package com.github.unidbg.linux.file
下定义了很多文件类型模板,可以按需使用。
- 例如如果so查看了
/proc/${pid}/maps
文件,可以使用MapsFileIO
,里面定义了maps格式可以方便生成maps数据; - 例如如果so查看了系统文件,可以用
DriverFileIO
,里面有一些定义好的/dev
下的文件;
- 自定义文件类型
通常可能会遇到要定制自定义文件处理类的情况。大部分文件处理都继承自BaseAndroidFileIO
public abstract class BaseAndroidFileIO extends BaseFileIO implements AndroidFileIO {}
再往上看,BaseFileIO
继承自AbstractFileIO
,实现了FileIO
接口,接口里面抽象了几乎所有的文件IO操作,只需要按需实现其功能即可;
public interface FileIO {
int SEEK_SET = 0;
int SEEK_CUR = 1;
int SEEK_END = 2;
void close();
int write(byte[] data);
int read(Backend backend, Pointer buffer, int count);
int pread(Backend backend, Pointer buffer, int count, long offset);
int fcntl(Emulator<?> emulator, int cmd, long arg);
int ioctl(Emulator<?> emulator, long request, long argp);
FileIO dup2();
int connect(Pointer addr, int addrlen);
int bind(Pointer addr, int addrlen);
int listen(int backlog);
int setsockopt(int level, int optname, Pointer optval, int optlen);
int sendto(byte[] data, int flags, Pointer dest_addr, int addrlen);
int lseek(int offset, int whence);
int ftruncate(int length);
int getpeername(Pointer addr, Pointer addrlen);
int shutdown(int how);
int getsockopt(int level, int optname, Pointer optval, Pointer optlen);
int getsockname(Pointer addr, Pointer addrlen);
long mmap2(Emulator<?> emulator, long addr, int aligned, int prot, int offset, int length) throws IOException;
int llseek(long offset, Pointer result, int whence);
int recvfrom(Backend backend, Pointer buf, int len, int flags, Pointer src_addr, Pointer addrlen);
String getPath();
}
最便捷的方式,就是继承已有文件模板,继承覆盖部分功能。
例如:如果希望实现对一个可读可写文件的处理,而且不希望在本地生成文件,所有IO都在内存里模拟处理,那么可以继承ByteArrayFileIO
,把抛异常的write方法覆盖实现即可。
public class ReadWriteFile extends ByteArrayFileIO {
@Override
public int read(Backend backend, Pointer buffer, int count) {
synchronized (lock) {
return super.read(backend, buffer, count);
}
}
@Override
public int write(byte[] data) {
if (data == null) {
return 0;
}
if (log.isDebugEnabled()) {
log.debug(path + " write: length " + data.length);
}
byte[] temp = new byte[bytes.length + data.length];
synchronized (lock) {
System.arraycopy(bytes, 0, temp, 0, bytes.length);
System.arraycopy(data, 0, temp, bytes.length, data.length);
bytes = temp;
pos += data.length;
}
lastModified = System.currentTimeMillis();
return data.length;
}
}
第7步中添加了一个对JNI的处理,当调用到java方法时候,会到该类里面找实现。
//7.添加对Java方法调用的处理
vm.setJni(new MyJni());
vm.setVerbose(true);
之后,可以看到JNIEnv的调用日志,直接工程里面全局搜索日志前缀字符串,可以发现是在com.github.unidbg.linux.android.dvm.DalvikVM
里面处理了对JNIEnv通用函数的调用。
JNI函数table可以查阅oracle JNI Functions,对应的DalvikVM
在[2430行]注册了这些函数。
final UnidbgPointer impl = svcMemory.allocate(0x3a4 + emulator.getPointerSize(), "JNIEnv.impl");
for (int i = 0; i < 0x3a4; i += 4) {
impl.setInt(i, i);
}
impl.setPointer(0x10, _GetVersion);
impl.setPointer(0x18, _FindClass);
impl.setPointer(0x24, _ToReflectedMethod);
impl.setPointer(0x34, _Throw);
impl.setPointer(0x3c, _ExceptionOccurred);
impl.setPointer(0x44, _ExceptionClear);
impl.setPointer(0x4c, _PushLocalFrame);
impl.setPointer(0x50, _PopLocalFrame);
impl.setPointer(0x54, _NewGlobalRef);
...
例如CallStaticObjectMethod
:
Pointer _CallStaticObjectMethod = svcMemory.registerSvc(new ArmSvc() {
@Override
public long handle(Emulator<?> emulator) {
RegisterContext context = emulator.getContext();
UnidbgPointer clazz = context.getPointerArg(1);
UnidbgPointer jmethodID = context.getPointerArg(2);
if (log.isDebugEnabled()) {
log.debug("CallStaticObjectMethod clazz=" + clazz + ", jmethodID=" + jmethodID);
}
DvmClass dvmClass = classMap.get(clazz.toIntPeer());
DvmMethod dvmMethod = dvmClass == null ? null : dvmClass.getStaticMethod(jmethodID.toIntPeer());
if (dvmMethod == null) {
throw new BackendException();
} else {
VarArg varArg = ArmVarArg.create(emulator, DalvikVM.this, dvmMethod);
DvmObject<?> obj = dvmMethod.callStaticObjectMethod(varArg); // 调用该方法
if (verbose) {
System.out.printf("JNIEnv->CallStaticObjectMethod(%s, %s(%s) => %s) was called from %s%n", dvmClass, dvmMethod.methodName, varArg.formatArgs(), obj, context.getLRPointer());
}
return addLocalObject(obj);
}
}
});
DvmMethod在调用方法前,会获取jni对象,最终返回的就是vm.setJni(new MyJni());
里面传入的MyJni()对象。
protected final Jni checkJni(BaseVM vm, DvmClass dvmClass) {
Jni classJni = dvmClass.getJni();
if (vm.jni == null && classJni == null) {
throw new IllegalStateException("Please vm.setJni(jni)");
}
return classJni != null ? classJni : vm.jni;
}
unidbg的com.github.unidbg.linux.android.dvm.AbstractJni
里处理了一部分常见的jni方法调用,我们只需继承这个类,按提示补充即可。
import com.github.unidbg.linux.android.dvm.AbstractJni;
public class MyJni extends AbstractJni {
}
可以直接运行工程,按照提示一步步来加上即可。例如如果报错
[13:13:09 900] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:467) - handleInterrupt intno=2, NR=-1073744353, svcNumber=0x129, PC=unidbg@0xfffe0324, LR=RX@0x40002d8d[libxx.so]0x2d8d, syscall=null
java.lang.UnsupportedOperationException: android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.getObjectField(AbstractJni.java:144)
那么就在MyJni里面补充
@Override
public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
switch (signature) {
case "android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;":
return new StringObject(vm, "/data/app/"+this.packageName+"-1/base.apk");
}
// 调用父方法,父方法里如果没有就会抛异常
return super.getObjectField(vm, dvmObject, signature);
}
可以在AbstractJni
类里面看到一些预定义的getPackageName
,getApplicationInfo
等方法,如果构造vm时候,是使用apk来构造的,那么unidbg会帮你解析这些;
VM vm = emulator.createDalvikVM(new Fil("apk文件路径名")));
Dvm是DalvikVM的简写,对app里面出现的java类和对象,unidbg不可能会有其代码和实例,因此用了DvmClass
和DvmObject
来包装指代,同时定义了StringObject, DvmInterge等基本包装类型。
使用JNI方法传入的对象,都必须是用Dvm来包装一次。例如如果app里面有函数
public class JNIUtils{
public static native String getSign(Context context, String str);
}
那么调用时候,需要:
//1.声明一个DvmClass指代JNIUtils类
DvmClass jniClazz = vm.resolveClass("com.example.temp.JNIUtils".replace(".", "/"));
//2.声明一个DvmObject指代Context,StringObject指代String,DvmInteger指代int
// newObject给的参数是dvmObject里的value属性值,因为根本用不到value,所以直接给null就行
DvmObject context = vm.resolveClass("android/content/Context").newObject(null);
//3.调用函数
Object ret = jniClazz.callStaticJniMethodObject(emulator, "getSign(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;", context, "data_msg");
基本类型在调用时,会自动包装成Dvm的类型,因此上面例子字符串可以直接给。DvmObject
类代码第[100行]定义了jni调用如下,可以看到,基本的String类型,会先自动转成StringObject,并且添加到vm的一个local引用里。调用结束之后会释放该引用。
protected static Number callJniMethod(Emulator<?> emulator, VM vm, DvmClass objectType, DvmObject<?> thisObj, String method, Object...args) {
UnidbgPointer fnPtr = objectType.findNativeFunction(emulator, method);
vm.addLocalObject(thisObj);
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(thisObj.hashCode());
if (args != null) {
for (Object arg : args) {
if (arg instanceof Boolean) {
list.add((Boolean) arg ? VM.JNI_TRUE : VM.JNI_FALSE);
continue;
} else if(arg instanceof Hashable) {
list.add(arg.hashCode()); // dvm object
if(arg instanceof DvmObject) {
vm.addLocalObject((DvmObject<?>) arg);
}
continue;
} else if (arg instanceof String) {
StringObject str = new StringObject(vm, (String) arg);
list.add(str.hashCode());
vm.addLocalObject(str);
continue;
} else if(arg instanceof byte[]) {
ByteArray array = new ByteArray(vm, (byte[]) arg);
list.add(array.hashCode());
vm.addLocalObject(array);
continue;
}
list.add(arg);
}
}
return Module.emulateFunction(emulator, fnPtr.peer, list.toArray())[0];
}
如果函数参数是一个对象数组,例如如下,有两种方式。
public static native String getSign(Object... args);
第一种方式是用ArrayObject
对象包装,但是每个元素都必须是DvmObject的子类:
// getSign("str_1", 1, context);
ArrayObject args = new ArrayObject(new StringObject(vm, "str_1"),
DvmInteger.valueOf(vm, 1),
vm.resolveClass("android/content/Context").newObject(new Object()));
第二种方式是使用ProxyDvmObject
,两种方式等效。
ArrayObject args = ProxyDvmObject.createObject(
vm, "str_1", 1, vm.resolveClass("android/content/Context").newObject(new Object()));
打开vm的日志之后,会发现so获取了很多Class和MethodID,但是这些都不需要在JniResolver里面定义,也不需要处理。
JNIEnv->FindClass(android/content/pm/PackageManager) was called from RX@0x40002b55[libxx.so]0x2b55
JNIEnv->FindClass(android/content/pm/PackageInfo) was called from RX@0x40002b65[libxx.so]0x2b65
JNIEnv->FindClass(android/content/pm/Signature) was called from RX@0x40002b75[libxx.so]0x2b75
JNIEnv->GetMethodID(android/app/Activity.getPackageManager()Landroid/content/pm/PackageManager;) => 0x6ef28d94 was called from RX@0x40002b8f[libxx.so]0x2b8f
JNIEnv->CallObjectMethod(android.app.Activity@4f47d241, getPackageManager() => android.content.pm.PackageManager@4c3e4790) was called from RX@0x40002b9f[libxx.so]0x2b9f
JNIEnv->GetMethodID(android/app/Activity.getPackageName()Ljava/lang/String;) => 0x1e74d6f4 was called from RX@0x40002bb9[libxx.so]0x2bb9
JNIEnv->CallObjectMethod(android.app.Activity@4f47d241, getPackageName() => com.jingdong.app.mall) was called from RX@0x40002bc9[libxx.so]0x2bc9
unidbg会在调用_FindClass
(DalvikVM
[49行])里自动创建该class的封装类DvmClass
RegisterContext context = emulator.getContext();
Pointer env = context.getPointerArg(0);
Pointer className = context.getPointerArg(1);
String name = className.getString(0);
DvmClass dvmClass = resolveClass(name);//创建封装,添加到vm索引里
long hash = dvmClass.hashCode() & 0xffffffffL;
return hash;
DalvikVM
的父类BaseVM
里定义了classMap,用hash值索引。debug时候可以看到各个DvmClass的hash值
final Map<Integer, DvmClass> classMap = new HashMap<>();
// debug查看 classMap = {HashMap@1677} size = 19
// {Integer@1748} 2918 -> {DvmClass@1749} "class [B"
// {Integer@1750} 1187028615 -> {DvmClass@1751} "class java/security/cert/X509Certificate"
// {Integer@1752} -238452991 -> {DvmClass@1753} "class android/content/pm/ApplicationInfo"
// {Integer@1754} -578271390 -> {DvmClass@1755} "class android/content/pm/PackageManager"
// {Integer@1756} 608747430 -> {DvmClass@1649} "class com/jingdong/common/utils/BitmapkitUtils"
// {Integer@1757} 821642487 -> {DvmClass@1758} "class android/content/pm/PackageInfo"
// {Integer@1759} 495024495 -> {DvmClass@1760} "class java/security/cert/X509Extension"
// {Integer@1761} 1239130571 -> {DvmClass@1762} "class sun/security/util/DerEncoder"
// {Integer@1763} -615889765 -> {DvmClass@1764} "class android/content/pm/Signature"
同理methodId也是,通过方法名加变量名的signature字符串的hashcode来做索引值,生成一个DvmMethod;
可以在debug下,点开每个DvmClass可以看到methodMap的属性。
private final Map<Integer, DvmMethod> methodMap = new HashMap<>();
private final Map<Integer, DvmMethod> staticMethodMap = new HashMap<>();
int getMethodID(String methodName, String args) {
String signature = getClassName() + "->" + methodName + args;
int hash = signature.hashCode();
if (log.isDebugEnabled()) {
log.debug("getMethodID signature=" + signature + ", hash=0x" + Long.toHexString(hash));
}
if (vm.jni == null || vm.jni.acceptMethod(this, signature, false)) {
if (!methodMap.containsKey(hash)) {
methodMap.put(hash, new DvmMethod(this, methodName, args, false));
}
return hash;
} else {
return 0;
}
}
// debug查看 value = {DvmClass@1766} "class android/app/Activity"
// vm = {DalvikVM@1654}
// superClass = null
// interfaceClasses = {DvmClass[0]@1841}
// className = "android/app/Activity"
// staticMethodMap = {HashMap@1843} size = 0
// methodMap = {HashMap@1844} size = 3
// -{Integer@1854} 510973684 -> {DvmMethod@1855} "android/app/Activity->getPackageName()Ljava/lang/String;"
// -{Integer@1856} -2099175516 -> {DvmMethod@1857} "android/app/Activity->getApplicationInfo()Landroid/content/pm/ApplicationInfo;"
// -{Integer@1858} 1861389716 -> {DvmMethod@1859} "android/app/Activity->getPackageManager()Landroid/content/pm/PackageManager;"
// fieldMap = {HashMap@1845} size = 0
// staticFieldMap = {HashMap@1846} size = 0
// nativesMap = {HashMap@1847} size = 0
// jni = null
// objectType = {DvmClass@1783} "class java/lang/Class"
// value = null
// DvmObject.vm = {DalvikVM@1654}
// memoryBlock = null
总结一下上面so调用java函数的流程,例如调用类成员函数CallObjectMethod
:
-
DalvikVM
处理_FindClass
,如果是没有定义的Class,那么生成一个DvmClass对象并添加到vm的classMap
里面; -
DalvikVM
处理_GetMethodID
:- 根据classname的hashcode从vm的
classMap
里面拿到对应的dvmClass对象,如果没有则抛异常; - 根据methodName和args,调用dvmClass对象的
getMethodID(String methodName, String args)
方法,生成一个方法hashcode和dvmMethod对象,并且存到dvmClass对象的methodMap
里面;
- 根据classname的hashcode从vm的
-
DalvikVM
处理_CallObjectMethod
:- 根据所给的class的hashCode,先从vm的
classMap
里面拿到对应的dvmClass对象,如果没有则抛异常; - 根据所给的methodId,从dvmClass对象的
methodMap
里面去找对应的DvmMethod,如果没有则抛异常; - 执行DvmMethod的
callObjectMethod
,先调用chekJni来获取对应的jni接口; - 如果DvmMethod所对应的DvmClass有定义的Jni接口,那么使用该Jni接口来处理函数执行;
- 如果DvmClass没有实现Jni接口,那么使用vm的Jni接口来处理函数执行;
- 如果我们实现了vm的Jni接口,例如
MyJni
,那么就会调用相应函数执行。
- 根据所给的class的hashCode,先从vm的
上面流程3.5提到DvmClass自己的Jni接口,那什么时候会用到该接口呢?
对于一个app内的类,我们上面的做法是:用DvmClass来封装这个类,MethodID自动创建,我们只需要在JniResolver里面处理MethodID对应的方法即可。
另一种方法是,使用ProxyClassFactory
,加载我们工程定义的同名类,并在该类里面定义app类的类方法。使用方法就是在通用步骤里,加上下面这行。
vm.setDvmClassFactory(new ProxyClassFactory());
ProxyClassFactory
的部分代码如下,显然我们在无参构造这个类的时候,使用了工程的ClassLoader(传递性)
public class ProxyClassFactory implements DvmClassFactory {
protected final ProxyClassLoader classLoader;
public ProxyClassFactory() {
this(ProxyClassFactory.class.getClassLoader());
}
public ProxyClassFactory(ClassLoader classLoader) {
this.classLoader = new ProxyClassLoader(classLoader);
}
@Override
public DvmClass createClass(BaseVM vm, String className, DvmClass superClass, DvmClass[] interfaceClasses) {
return new ProxyDvmClass(vm, className, superClass, interfaceClasses, classLoader, visitor);
}
}
创建DvmClass时候,创建的是ProxyDvmClass
,主要完成两件事:
- DvmClass的value,由
classLoader.loadClass(name)
来完成,即加载了工程里的一个同名类class; - 定义了DvmClass的jni,也就是上上面调用流程3.5提到DvmClass自己的Jni接口,用了ProxyJni来实现;ProxyJni通过反射调用的方式,来调用工程里面同名类的方法。
public class ProxyDvmClass extends DvmClass {
protected ProxyDvmClass(BaseVM vm, String className, DvmClass superClass, DvmClass[] interfaceClasses, ProxyClassLoader classLoader, ProxyDvmObjectVisitor visitor) {
super(vm, className, superClass, interfaceClasses, null);
setJni(createJni(classLoader, visitor));
try {
this.value = classLoader.loadClass(getName()); // 使用classLoader来加载工程的同名类
} catch (ClassNotFoundException ignored) {
}
}
protected JniFunction createJni(ProxyClassLoader classLoader, ProxyDvmObjectVisitor visitor) {
return new ProxyJni(classLoader, visitor); // 使用ProxyJni来处理jni函数调用
}
}
定义了DvmClass的jni,调用该类的函数,就不会再走vm绑定的JniResolver了,而是会反射调用我们工程的同名类来实现。
简单来说,
- 使用默认的
DvmClassFactory
,unidbg会自动构建so里面FindClass
和GetMethodID
使用到的DvmClass
和DvmMethod
;当调用函数或者获取类属性时,调用vm绑定的JniResolver来实现。 - 使用
ProxyClassFactory
,unidbg会通过反射调用,来构造和调用工程里同名类的对象和方法。
没有详细去了解,这里列几个见到的用法:
Module libc = null;
for (Module each : emulator.getMemory().getLoadedModules()) {
if ("libc.so".equals(each.getPath())) {
libc = each;
break;
}
}
HookZz zz = HookZz.getInstance(emulator);
zz.replace(libc.findSymbolByName("lrand48"), new ReplaceCallback() {
@Override
public HookStatus onCall(Emulator<?> emulator, long originFunction) {
return HookStatus.LR(emulator, 0);
}
});
zz.instrument(module.base + 0x18B8 | 1, new InstrumentCallback<Arm32RegisterContext>() {
@Override
public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) {
int length = ctx.getR2Int();
System.out.println("stack " + Arrays.toString(ctx.getStackPointer().getByteArray(0, 64)));
System.out.println("hook: 0x18B8 length: " + new String(ctx.getR1Pointer().getByteArray(0, length)));
}
});
zz.wrap(module.base + 0x10EA4 | 1, new WrapCallback<RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
System.out.println("hook 10EA4 before key: " + Arrays.toString(ctx.getPointerArg(1).getByteArray(0, 40)));
System.out.println("hook 10EA4 before type: " + ctx.getIntArg(2));
}
@Override
public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
System.out.println("hook 10EA4 after result: " + Arrays.toString(ret) + "\n");
}
});
IxHook xHook = XHookImpl.getInstance(emulator); // 加载xHook,支持Import hook,文档看https://github.com/iqiyi/xHook
xHook.register("libxx.so", "memcpy", new ReplaceCallback() {
@Override
public HookStatus onCall(Emulator<?> emulator, long originFunction) {
RegisterContext ctx = emulator.getContext();
System.out.println("stack " + getVector(ctx.getStackPointer().getByteArray(0, 64)));
System.out.println("memcpy: " + Arrays.toString(ctx.getPointerArg(1).getByteArray(0, ctx.getIntArg(2))));
return HookStatus.RET(emulator, originFunction);
}
});
xHook.refresh(); // 使Import hook生效
没有实际例子,这里简单介绍用法:
Emulator提供了获取debug的接口,默认attach是用DebuggerType.CONSOLE
,开启调试方式如下:
Debugger MyDbg = emulator.attach();
MyDbg.addBreakPoint(module.base + 0xc6c9);
当运行到断点时,会在命令行窗口显示提示,此时可以输入指令来进行调试;help提供的命令如下:
c: continue
n: step over
bt: back trace
st hex: search stack
shw hex: search writable heap
shr hex: search readable heap
shx hex: search executable heap
nb: break at next block
s|si: step into
s[decimal]: execute specified amount instruction
s(blx): execute util BLX mnemonic, low performance
m(op) [size]: show memory, default size is 0x70, size may hex or decimal
mr0-mr7, mfp, mip, msp [size]: show memory of specified register
m(address) [size]: show memory of specified address, address must start with 0x
wr0-wr7, wfp, wip, wsp <value>: write specified register
wb(address), ws(address), wi(address) <value>: write (byte, short, integer) memory of specified address, address must start with 0x
wx(address) <hex>: write bytes to memory at specified address, address must start with 0x
b(address): add temporarily breakpoint, address must start with 0x, can be module offset
b: add breakpoint of register PC
r: remove breakpoint of register PC
blr: add temporarily breakpoint of register LR
p (assembly): patch assembly at PC address
where: show java stack trace
trace [begin end]: Set trace instructions
traceRead [begin end]: Set trace memory read
traceWrite [begin end]: Set trace memory write
vm: view loaded modules
vbs: view breakpoints
d|dis: show disassemble
d(0x): show disassemble at specify address
stop: stop emulation
run [arg]: run test
cc size: convert asm from 0x400018b8 - 0x400018b8 + size bytes to c function
unidbg源码里StringObject.toString()返回值会多一个双引号。
@Override
public String toString() {
if (value == null) {
return null;
} else {
return '"' + value + '"';
}
}
需要注意不要直接用toString就好
StringObject dvmStr = new StringObject(vm, "msg");
String value = "prefix," + dvmStr.getValue() + ",suffix";
当然为了方便,我直接把clone下来的工程源码里的双引号去掉了。
报错:
Find symbol open failed: handle=0xffffffff...
错误发生在AndroidElfLoader
类的dlsym函数[284行]
@Override
public Symbol dlsym(long handle, String symbolName) {
for (LinuxModule module : modules.values()) {
if (module.base == handle) { // virtual module may have same base address
Symbol symbol = module.findSymbolByName(symbolName, false);
if (symbol != null) {
return symbol;
}
}
}
if ("environ".equals(symbolName)) {
return new VirtualSymbol(symbolName, null, environ.toUIntPeer());
}
return null;
}
主要是传入的handle值为-1,导致无法找到对应的模块基址。至于为什么传入值为-1,目前没找到原因。解决办法是去掉module.base == handle的判断,直接默认不会有同名symbol导出;如果有同名symbol,自行判断处理。
[21:26:44 798] WARN [com.github.unidbg.AbstractEmulator] (AbstractEmulator:389) - emulate RX@0x4000374d[libxx.so]0x374d exception sp=unidbg@0xbffff5d8, msg=null, offset=40537ms
Exception in thread "main" java.lang.IllegalStateException: Illegal JNI version: 0xffffffff
在BaseVM里面抛出的异常,主要是callJNI_OnLoad
失败了,如果没有什么问题,不影响后续调用(看app是否校验这个init成功),可以忽略。
final void checkVersion(int version) {
if (version != JNI_VERSION_1_1 &&
version != JNI_VERSION_1_2 &&
version != JNI_VERSION_1_4 &&
version != JNI_VERSION_1_6 &&
version != JNI_VERSION_1_8) {
System.err.println("Illegal JNI version: 0x" + Integer.toHexString(version));
// throw new IllegalStateException("Illegal JNI version: 0x" + Integer.toHexString(version));
}
}
JNIEnv->NewGlobalRef(android.app.Activity@4f47d241) was called from RX@0x400037b7[libxx.so]0x37b7
JNIEnv->GetMethodID(android/content/Context.getPackageManager()Landroid/content/pm/PackageManager;) => 0x6ef28d94 was called from RX@0x40002b8f[libxx.so]0x2b8f
[17:00:22 852] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:467) - handleInterrupt intno=2, NR=-578271390, svcNumber=0x114, PC=unidbg@0xfffe01d4, LR=RX@0x40002b9f[libxx.so]0x2b9f, syscall=null
com.github.unidbg.arm.backend.BackendException
at com.github.unidbg.linux.android.dvm.DalvikVM$21.handle(DalvikVM.java:405)
这个直接去看报错的DalvikVM.java:405,发现是获取DvmMethod为空导致的。
- so获取了一个Activity对象,解析其类名生成
DvmClass activityClazz
,并构造了一个DvmObject activity
- so获取了一个Context的
getPackageManager()
方法,methodID存在了DvmClass contextClazz
的methodMap里面; - so对
DvmObject activity
尝试调用这个methodID,但是找不到methodID。
在Android中,Activity继承Context,能调用Context函数很正常;但是在DvmClass
封装这两个class时候,没有处理他们的继承关系。
解决办法一:不要传Activity dvm对象,直接给一个Context dvm对象
DvmObject activity = vm.resolveClass("android/content/Context").newObject(null);
方法二:处理他们的继承关系
DvmClass contextClz = vm.resolveClass("android/content/Context");
DvmObject activity = vm.resolveClass("android/app/Activity", contextClz).newObject(null);
方法三:使用ProxyClassFactory
,定义Context和Activity并保持他们的继承关系。
参考issue-jmethodID投毒导致CallObjectMethod异常问题
long ->两个int