基于 unidbg 模拟执行 so 文件

13.9 节和 13.10 节介绍的两种方式都是在 Android 手机上执行的 so 文件,那有没有办法可以在电脑上直接执行 so 文件呢?当然也是有方法的,Python 的 AndroidNativeEmu 和 Java 的 unidbg 等都支持在电脑上直接执行 so 文件。

目前,unidbg 的功能相对来说更为强大,使用也更为广泛,所以本节我们介绍利用 unidbg 模拟执行 so 文件的方法。

unidbg的简介

unidbg 是一个基于 unicorn 的逆向工具(unicorn 是一个 CPU 模拟框架),在 unicorn 的基础上,unidbg 可以模拟 JNI 调用 Native API,支持模拟调用系统指令,支持 JavaVM、JNIEnv 和模拟 ARM32、ARM64指令。于是unidbg 就可以支持执行基于 ARM 指令的 so 文件,也就是可以模拟执行 Android 手机上的 so 文件。另外除了模拟执行,unidbg 还支持 Native 层的 Hook 操作,我们可以通过 Hook 的方式拦截和修改 Native 层的一些逻辑。

unidbg 的 GitHub 地址是 https://github.com/zhkl0228/unidbg ,里面包含更多详情介绍。

准备工作

unidbg 是基于 Java 编写的,这里我们建议使用 IntelliJ IDEA 编写代码,所以需要安装一下 IntelliJ IDEA,具体的安装方式可以参考 https://setup.scrape.center/intelliJ。

我们还需要复制 unidbg 的源码,命令如下:

git clone https://github.com/zhklo228/unidbg·git

本节使用的示例 App 还是 App9,和 13.10 节一样,先下载 apk 文件,然后用 jadx-gui 提取出 so 文件,如图 13-143 所示。

图13-143 App9 源码中的 so 文件

本节我们使用 armeabi-v7a 文件中的 libnative.so 文件。

模拟执行

使用 IntelliJ IDEA 打开复制好的 unidbg 文件夹,打开后的项目结构如图 13-144 所示。

我们将得到的 libnative.so 文件放到 unidbg-android/src/test/resources/app9 目录下,如图 13-145 所示。

图13-144 unidbg 文件夹的项目结构 图13-145 放置 libnative.so 文件

放好后,我们来编写一个Java类实现对so文件的模拟执行。在unidbg-android/src/test/java目录下,已经有一些写好的测试文件,都是以包名形式出现。我们同样可以根据App9的包名新建对应的文件夹,这里我们新建一个名为com.goldze.mvvmhabit.utils的包,如图13-146所示。

图13-146 新建一个包

我们再新建一个 NativeUtils 类,代码文件保存为 NativeUtils.java,内容如下:

这里的写法可能看起来比较陌生,不用着急,我会一点点讲其中的原理。首先可以看到,在 NativeUtils 类中调用了一些 unidbg 提供的类,有 AndroidEmulator、DvmClass、VM、DalvikModule、 Memory 等。

  • AndroidEmulator:顾名思义,这代表Android进程模拟器,emulator就是一个Android进程模拟器对象。

  • Memory:代表内存,利用它我们可以定义一个模拟器的内存操作接口,例如调用它的malloc方法可以分配内存空间,调用getStackSize方法可以获取内存栈的大小。

  • VM:代表虚拟机(VirtualMachine),我们可以调用AndroidEmulator对象的createDalvikVM方法创建一个Dalvik虚拟机对象,有了这个虚拟机后,我们就可以模拟加载so文件了。

  • DalvikModule:代表Dalvik模块,vM对象可以调用loadLibrary方法把so文件加载到虚拟内存中,其返回结果就是一个Dalvik模块对象,我们可以模拟调用该对象的.JNIOnload方法执行一些so文件的加载和初始化工作。

  • DvmClass:可以把它视为Java层的Class对象。调用vM对象的resolveClass方法并传入我们定义好的Java类的路径,该方法便会返回一个Java类的操作对象,即DvmClass对象。通过 DvmClass对象的一些方法(如callStaticJniMethodobject),我们就可以调用Native方法了。

直接看上述内容,可能比较难理解。如果想深入了解,可以多看 unidbg、unicorn 的源码,或者学习 Android 虚拟机的一些基础知识。

所以,NativeUtils 类的构造方法的实现流程基本分如下几步。

  1. 利用 AndroidEmulatorBuilder 类创建一个模拟器对象emulator,这里使用setProcessName方法指定了App9的包名。

  2. 声明一个Memory对象,这里使用setLibraryResolver指定了其适配哪个版本的AndroidSDK,这里指定的版本是23。unidbg目前提供对19和23这两个AndroidSDK的支持,这里使用23,对应 Android 6.0。

  3. 调用emulator变量的createDalvikVM方法创建一个Dalvik虚拟机对象,赋值为vm。

  4. 利用vm变量的loadLibrary方法加载so文件,这里我们直接指定了一个File对象,并指定了so文件的路径,createDalvikVM方法的返回结果是一个DalvikModule对象,将其赋值为dm变量。

  5. 调用dm变量的JNI_Onload方法执行一些so文件的加载和初始化工作。

  6. 调用vm变量的resolveClass方法返回一个Java类的操作对象,即DvmClass对象,赋值为cls 变量。

以上流程完成后,我们就可以利用cls变量调用so文件中的方法了。我们再在NativeUtils类中增加一个调用方法,代码如下:

这里我们定义了一个encrypt方法,接收string和offset参数,因为其在so文件中对应的 Java_com_goldze_mvvmhabit_utilsNativeutils_encrypt方法就是接收string和offset参数。方法中我们调用cls的callStaticJniMethodobject方法实现了对Native方法的调用,第一个参数是模拟器对象,第二个参数是要调用的Native方法的名称,即encrypt,之后的参数就是这个encrypt方法的参数。对于String类型的参数,这里我们使用vm变量的addLocalobject方法创建了一个代表字符串类型的参数。对于int类型的参数,则可以直接传入。

callStaticJniMethodobject方法返回的是一个DvmObject对象,通过调用这个对象的getValue方法我们就能得到Native方法encrypt最终的返回结果了,这里我们加了一个强制类型转换,把返回结果转换成了学符串类型。

最后我们来测试一下,在Nativeutils类中添加一个main方法:

运行NativeUtils类,操作过程如图13-147所示。

图13-147 运行Nativeutils类

最终的运行结果如下:

可以看到结果中输出了最终的 token 值,我们成功模拟执行了 so 文件。

暴露结果

我们已经成功拿到token结果了,接下来如何爬取数据呢?现在模拟执行so文件的逻辑是用Java 语言编写的,难道爬虫也要用Java语言编写吗?虽然可以,但这不是唯一选择。

我们可以借鉴13.10节的思路,也通过HTTP服务器将unidbg的运行结果暴露出来,这个可以用 Java中的SpringBoot实现。

首先需要在unidbg-android/pom.xml文件里面添加对SpringBoot的引用,添加两个dependency即可,代码如下:

添加完后,IntelliJIDEA会把SpringBoot对应的包下载到本地。,接着我们定义一个AppController 类,这个类和NativeUtils类同级,其基本写法和13.10节非常相似,类内容如下:

这里其实也是定义了一个GET请求,接收的查询字符串参数也是string和offset,再加上调用 NativeUtils类的encrypt方法获取的token结果,最后返回一个Map对象。

下面在AppController类同级的地方定义一个SpringBoot入口类AppServer,其内容如下:

这个类也是可以直接运行的,运行之后就会开启一个SpringBoot服务。那这个服务运行在哪个端口呢?这个似乎还没指定?对此可以创建一个 unidbg-android/src/test/resources/application.properties 文件来声明SpringBoot服务运行的地址和端口,文件内容如下:

server.address=0.0.0.0
server.port=9999

最后,运行AppServer即可启动SpringBoot服务,该服务会运行在9999端口,操作如图13-148 所示。

图13-148运行AppServer

在浏览器中访问测试 URL http://localhost:9999/encrypt?string-test&offset=0,返回结果如图 13-149 所示。

图13-149浏览器返回的token结果

可以看到返回了 token 结果,这样我们就成功把利用 unidbg 模拟执行 so 文件的结果也通过 HTTP 服务器暴露出来了,从而就可以实现调用。

爬取数据

我们使用Python脚本来调用上面定义的HTTP接口和爬取数据:

爬取结果和 13.10 节是一样的,这里不再赞述。

总结

本节中我们学习了利用 unidbg 模拟执行 so 文件的方法,同时为了实现数据爬取,我们通过 SpringBoot 暴露了模拟执行的结果,最后顺利通过 Python 脚本对接接口的方式爬取了数据。

本节内容其实仅用到了 unidbg 所有功能的冰山一角,要想深入了解更多内容,可以研究 unidbg 的源码。