购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.2 unidbg的符号调用与地址调用

这一节,我们来学习unidbg提供的两种模拟调用方式:符号调用和地址调用。

2.2.1 unidbg主动调用前置准备

首先重新编译一下APK,使它生成全架构的so文件,如图2—11所示。

将Android Studio左侧项目结构图切换为Project视图,然后找到项目中编译出来的app-debug.apk,如图2—12所示。

图2—11 编译APK操作

图2—12 app-debug.apk目录

接着使用终端进入相应目录,使用以下命令解压APK文件。

unzip app-debug.apk

使用IDEA打开之前的unidbg项目,在unidbg-android/src/test/java下依照引用so文件的全类名来编写相同的类。然后将上面解压的APK中的lib/arm64-v8a中的so及APK文件都复制到该目录下,最终效果如图2—13所示。

图2—13 代码编写准备界面

2.2.2 unidbg主动调用so函数

首先编写MainActivity构造方法来模拟so执行所需的环境。

由代码可知,前4行将常用的变量存储为成员变量,以便在不同的成员函数间使用。

初始化环境的代码与1.3.3节的代码大致相同。只是在创建vm虚拟机时,传入了原本的APK文件,使unidbg可以依据APK为模拟环境做一些初始化工作。同时通过加载so文件得到的dalvikModule对象的getModule()方法,得到操作so的对象并将其保存到成员变量中备用。

unidbg支持两种调用so中函数的方式:符号调用和地址调用。

符号调用方式的代码示例如下:

public void callMd5(){

DvmObject obj=ProxyDvmObject.createObject(vm,this);

String data="dta";

DvmObject dvmObject=obj.callJniMethodObject(emulator,"md5(Ljava/lang/

String;)Ljava/lang/String;",data);

String result=(String)dvmObject.getValue();

System.out.println("[symble]Call the so md5 function result is==>"+result);

}

相关代码与1.3.3节的示例代码区别不大。先根据传入的this参数生成一个调用so的代理对象obj,该处传入的this参数即com.dta.lesson2.MainActivity对象,如果全类名无法与原APK中调用so的java类对应,则unidbg将无法补全相应的函数名。

由于md5()方法的返回值为String类型,所以应当使用callJniMethodObject()方法来调用md5()方法,同时使用DvmObject对象来接收该返回值。除了基本类型之外,其余的类型都要返回Object对象,相关的API如图2—14所示。

图2—14 unidbg支持的callJniMethod相关的API

对于得到的md5()方法的结果DvmObject对象,我们可以通过调用它的getValue()方法来获得相应的值。由于我们已知函数的返回值为String类型,因此可以将其强制转换为String类型,之后将获得的值输出。

unidbg中另一种常用的函数调用方式是地址调用。一般情况下,我们所需的函数可能没有导出,所以无法进行符号调用。

地址调用方式的代码示例如下:

在使用符号调用方式时,unidbg帮助我们完成了很多操作,例如拼接函数名、填充参数等。但在使用地址调用方式时,这些操作需要我们自己来完成。

根据函数的原型md5(_JNIEnv * env,jobject thiz,jstring str),我们需要手动构造参数列表,补全相应参数,才能对相关函数进行调用。

首先通过getJNIEnv()方法获取JNIEnv,并使用指针类型来存储。然后使用与符号调用相同的方式创建一个调用so函数的代理对象,并将其作为所需的第二个参数jobject。对于第三个参数需要注意的是,在unidbg中,当传入参数为非指针和Number类型时,都需要先将其定义为DvmObject对象并添加到VM中,才能够使用该参数。而对于常用的String类,unidbg对其做了相应的封装,这里新建了一个StringObject对象来作为第三个参数。

之后定义一个List<Object>参数列表,并将构造的三个参数添加到其中。然后使用module.callFunction()方法来对函数进行调用,该方法需要三个参数,分别为模拟器对象、函数的相对偏移地址和参数数组。

对于函数偏移的获取,我们需要使用IDA载入该so函数以找到相应的获取函数,如图2—15所示。

可以看到md5()方法的偏移为0x8E80,但由于该汇编指令的格式为thumb,所以需要对函数地址进行加1操作,最终此处填入的参数的值为0x8E81。对于arm指令与thumb指令,可以通过指令长度来区分:thumb的指令长度为2字节,而arm指令长度为4字节。此外,arm指令中是没有PUSH指令的,这也可以作为区分的依据。

调用函数后获得的返回值为Number类型,这与函数的实际返回值String类型是不相符的。不过不用着急,我们可以用两种方案来处理:对于基本数值类型或者布尔类型,可以直接通过Number获取相应的值;而对于Object对象来说,Number中实际存储的是Object对象的hash值,因此需要先通过vm.getObject()方法来获取DvmObject对象(传入的参数为number.intValue(),即Object对象的hash值),之后即可通过getValue()方法来获取对象中存储的值,并将其强制转换为String类型后输出。

图2—15 使用IDA获取函数偏移

main()方法就比较简单了,仅仅是实例化MainActivity对象后使用了符号调用和地址调用这两个方式来完成so中函数的调用。

public static void main(String[]args){

long start=System.currentTimeMillis();

MainActivity mainActivity=new MainActivity();

System.out.println("load the vm"+(System.currentTimeMillis()-start)+"ms");

mainActivity.callMd5();

mainActivity.call_address();

}

输出如下,可以发现函数的输出是正确计算出来的。

Picked up_JAVA_OPTIONS:-Dawt.useSystemAAFontSettings=on-Dswing.aatext=true

load the vm 16020ms

[symble]Call the so md5 function result is==>36072180305f072a2e2c7ea96eedf034

[addr]Call the so md5 function result is==>36072180305f072a2e2c7ea96eedf034

Process finished with exit code 0

2.2.3 unidbg部分API简单讲解

在上述代码中,我们接触到几个常用的接口,如AndroidEmulator、Memory、VM等。

AndroidEmulator是一个抽象的Android模拟器接口。它作为一个枢纽,协调unidbg中大多数模块的工作,至关重要。我们的大多数操作需要借助它完成。

而AndroidEmulatorBuilder可以帮助我们快速创建AndroidEmulator实例。例如:

AndroidEmulator emulator=AndroidEmulatorBuilder

//指定32位处理器

.for32Bit()

//指定64位处理器

//.for64bit()

//添加后端工厂

.addBackendFactory(new DynarmicFactory(true))

//指定进程名,推荐以Android包名作为进程名

.setProcessName("com.github.unidbg")

//设置根路径,此路径相当于Android中的根目录

.setRootDir(new File("target/rootfs/default"))

//生成AndroidEmulator实例

.build();

我们也可以通过AndroidEmulator实例来获取内存操作接口Memory实例:

Memory memory=emulator.getMemory();

创建DalvikVM虚拟机:

//创建虚拟机

VM dalvikVM=emulator.createDalvikVM();

//创建虚拟机并指定APK文件

VM dalvikVM=emulator.createDalvikVM(new File("apk file path"));

Memory内存接口主要提供内存管理和ELF文件加载两个功能。

对于ELF文件的加载,我们一般用加载APK的形式实现,因为unidbg会帮助我们进行一些解析处理,可以节省很多时间。

Module module=emulator.loadLibrary(new File("file path"),true);

而VM对象的主要作用就是代理一套JNI,帮助用户借助JNI来操作Java层。

VM的实际功能是调用JNI_OnLoad函数。某些JNI方法在JNI_OnLoad函数进行动态绑定时需调用此函数才能够调用JNI方法,所以这里推荐在加载模块后再执行此方法。

//参数一:模拟器实例

//参数二:要执行JNI_OnLoad函数的模块

dalvikVM.callJNI_OnLoad(emulator,module);

或者控制JNI交互的详细日志输出:

//设置是否输出JNI运行日志

vm.setVerbose(true);

对于CallMethod系列的方法,第二个参数是方法签名,我们只需传入部分名称即可,这是因为unidbg在底层逻辑中进行了相应的处理。

对于上面的项目,按住Ctrl后单击callJniMethodObject()方法,可以跟进相应的实现,之后跟进其内部的callJniMethod()方法,再跟进findNativeFunction()方法,可以查看unidbg找到相应函数的过程,如图2—16所示。

图2—16 unidbg findNativeFunction()实现

可以看到,在第239行unidbg根据对传入对象的全类名与函数名进行拼接来得到最终的函数名。

而使用callFunction()方法时需要传入参数,除了基本数据类型之外,其余数据类型都要封装为DvmObject并传入VM虚拟机后才能够使用,具体后文会有详细的案例。 Zs6J5smRYuOEuJdDiz5b29cH05omP2BFg/u30iPHiqqIf93KDZqlQv67x8np/EWI

点击中间区域
呼出菜单
上一章
目录
下一章
×