基于 AndServer-RPC 模拟执行 so 文件

本节介绍利用 AndServe-PRPC 模拟执行 so 文件的过程。

AndServer 的简介

平时我们编写服务器脚本,代码都是运行在电脑上的。例如写一个简单的 Flask 服务器脚本,就需要在电脑上运行该脚本来启动对应的服务。那服务器能不能直接运行在手机上呢?答案是肯定的。

AndServer 是可以运行 Android 手机上的一个 HTTP 服务器,其实就是一个 Android 的第三方包,我们可以开发一个 Android App 后将其引入; 再将其提供的服务器功能设置为随之启用,并指定运行的端口,这样在 App 启动的时候就可以在 Android 手机上启动一个 HTTP 服务了。

AndServer 包是基于 Java 编写的,在 Java 生态中有一个非常流行的服务器框架叫作 SpringMVC,不过它是运行在电脑端的。AndServer 借鉴了 SpringMVC 的一些设计思路,具有和其相似的功能,例如利用注解(Annotations)来定义一些路由规则和处理规则,使用起来非常方便。

那 AndServer 和我们本节要讲的内容有什么关系呢?接下来我们详细看一下。

基本思路

由于 so 文件有其特定的指令架构,因此我们不能直接在电脑上调用和执行它,而 so 文件又隐含了我们想要的 token 结果,那么在 Android 端模拟执行 so 文件后,怎么能把结果方便地暴露出来呢?在 13.9 节,我们通过 Frida 成功在电脑上拿到了 so 文件的执行结果,那这里的 AndServer,其实就是换了一个暴露结果的思路,即通过 HTTP 接口把结果暴露出来。

通过以上介绍我们可以发现,AndServer 相当于在手机上启动了一个 HTTP 服务器,这个服务器内部可以直接调用 App 中的方法得到结果并返回。参数怎么传递呢?很简单,通过 HTTP 请求的参数传递即可,例如我们已经了解到 encrypt 方法接收 string 和 offset 参数,那我们就可以把这两个参数映射为 HTTP 中 URL 的参数或者请求体。执行结果怎么返回呢?很自然地,通过响应结果返回就好了。

我们现在相当于借助 AndServer 把 Android App 中的方法包装了一下,提供了 HTTP 服务。有了 HTTP 服务后,我们就可以通过 requests 等库传入对应的参数来获取 token 结果了,而这个 token 本质上是 App 调用 so 文件产生的,算是一个模拟执行的过程,我们可以将这整个过程称为 AndServer-RPC。

准备工作

本节的示例 App 和 13.9 节的一样。我们使用 jadx-gui 反编译其 apk 文件,之后可以看到对应的 so 文件,如图 13-135 所示。

图13-135 App9 反编译结果

将反编译后的项目导出,保存其中的 lib 目录,以备下面使用。然后准备一台 Android 手机,模拟器和真机均可,将其和电脑连接,并确保能在电脑上使用 adb 命令访问到该 Android 手机。

本节会开发一个 Android App,所以请确保正确配置好了 Android 开发环境,具体的配置方式可以参考 htps://setup.scrape.center/android 。

App的初始化

我们首先创建一个 Android App。打开 Android Studio,新建一个空白项目,包名可以随意取,这里我把 App 名称取为 AndServerTest,包名取为 com.germey.andservertest,如图 13-136 所示。

图13-136 创建一个 AndroidApp

做一些项目的初始化工作。接着就需要将刚才准备好的 so 文件放到本项目中了,这里我把整个 lib 目录放置在 app 目录下,代码结构如图 13-137 所示。

图13-137 所创建App的代码结构

另外我们需要修改一下 build.gradle 文件,在 android 声明的部分添加对这个 lib 目录的引用,定义一个 sourceSets 的声明,代码如下:

android {
    ...
    sourceSets {
        main {
            jniLibs.srcDirs = ['lib']
        }
    }
    ...
}

这里我们通过 jniLibs.srcDirs 指定了 so 文件的保存路径,这样 App 在加载 Native 库的时候就知道应该从哪里寻找 so 文件了。

so 文件已经准备就绪,那应该怎么调用它呢?具体的调用参数和方法名又怎么写呢?一个比较好的方法是从原来的 App 里找出对应的调用逻辑,然后把调用相关的定义复制到当前的 App 项目中。

使用 jadx-gui 反编译原来的 apk 文件,我们可以从结果中搜索关键字轻松找到对 libnative.so 文件的调用声明,如图 13-138 所示。

图13-138 源码中调用 so 文件的地方

在图 13-138 中,有一个 NativeUtils 类,这个类在 com.goldze.mvvmhabit.utils 包中。NativeUtils 类里定义了一个 encrypt 方法,接收参数 str 和 i。这个 encrypt 方法前面有一个 native 关键字,证明真正的方法定义在 Native 层,逻辑定义其实就在 so 文件中。我们使用 IDA Pro 对 so 文件进行逆向,可以找到 so 文件中定义的入口方法 Java_com_goldze_mvvmhabit_utils_NativeUtils_encrypt,其方法命名是有一定规律的,就是把 Java 代码中的包名、类名、方法名都用下划线连接起来,对应的其实是刚才所说的 encrypt 方法的真实定义。在 Java 层调用 encrypt 方法,就相当于调用了 so 文件中对应的 Native 层的 Java_com_goldze_mvvmhabit_utils_NativeUtils_encrypt 方法,后者的参数和前者的参数一一对应,后者的执行结果会被回传给前者,最后前者返回的结果就是后者的执行结果。

所以,如果我们想在刚才创建的 App 里完全模拟对 so 文件的调用,就需要遵循其调用规范,即包名、类名、方法名要和 so 文件中的 Java_com_goldze_mvvmhabit_utils_NativeUtils_encrypt 方法对应起来。于是我们创建一个 com.goldze.mvvmhabit.utils 包,然后定义一个 NativeUtils 类,再在 Nativeutils 类中定义一个 encrypt 方法,其实就是把原来 App 中的定义原封不动地复制到新的 App 项目中。最终新的 App 项目变成了如图 13-139 所示的这样。

图13-139 所创建App的最终内容

至此,我们已经成功引人了 so 文件,调用方法也声明好了。下面我们就引入 AndServer 来调用 so 文件,并将结果通过 HTTP 服务器暴露出来。

引入 AndServer

截至编写本节内容时,AndServer 的最新版本是 2.1.9,所以在 App 项目的 build.gradle 文件中的 dependencies 部分添加如下引用 AndServer 的内容:

implementation 'com.yanzhenjie.andserver:api:2.1.9'
annotationProcessor 'com.yanzhenjie.andserver:processor:2.1.9'

添加之后,Android Studio 会提示我们要不要下载 AndServer 包,点击确认即可,这样 AndServer 包就成功被下载到项目中了。

由于 AndServer 一直在更新,所以最新版本以官方发布为准,见 https://github.com/yanzhenjie/AndServer

接下来,我们先定义一个基本的页面入口,修改 src/main/res/layout/activity_main.xml 文件,添加一个按钮和一个文本控件,代码如下:

这个按钮就是用来控制 AndServer 启动和停止的,文本控件是用来显示 AndServer 的状态信息的。

然后修改一些文本值的定义,打开 src/main/res/values/strings.xml 文件,把内容修改成如下这样:

对于按钮,我们给它绑定了一个叫作 toggleServer 的方法,其含义是关闭或者打开 AndServer 我们需要在 MainActivity 类里定义一下这个方法,并实现启动和停止 AndServer 的相关逻辑,因此 MainActivity 类的内容被修改成了这样:

在onCreate方法里,我们初始化了AndServer对象,指定其运行端口为8080,同时调用1istener 方法添加了ServerListener对象。在初始化ServerListener对象的时候,定义了onStarted、onStopped、 onException三个方法,它们分别对应在AndServer启动后、停止后、出现异常后的处理逻辑,我们在三个方法中改变了刚才声明的按钮和文本控件的内容。例如在AndServer启动后,文本控件会显示The serverisstarted,证明服务器启动成功。

对于和按钮绑定的toggleServer方法,这里的逻辑是判断AndServer是不是在运行,如果没有运行,就调用startup方法启动它,如果已经在运行,则调用shutdown方法停止运行。

这样我们就定义好了AndServer的声明和控制逻辑,同时将其启动和停止行为与按钮绑定在了一起。接下来我们还需要声明对应的接口定义,新建一个叫作AppController的类:

这里我们引人了@RestController、@QueryParam和@GetMapping三个注解,其用法类似Python中的装饰器,我们将@RestController注解作用在AppController类上,同时声明一个login方法,并将@GetMapping注解作用在login方法上,绑定对应的路由。

login方法接收两个参数,一个是string,另一个是offset,方法中会直接调用我们刚才声明的 Nativeutils类中的encrypt方法,得到sign的内容,最后以JsoNObject形式返回sign的值。

经过这样的定义,我们就利用AndServer创建了可以接收GET请求的服务,URL路径也是 encrypt,查询字符串参数是string和offset,返回结果是一个JsoN字符串。

整个AndServer就实现完毕了,我们在手机上运行一下整个App,打开的页面如图13-140所示。可以看到页面中间有一个“STARTSERVER”按钮,同时下方显示“Theserverisstopped”的字样。

我们点击 “START SERVER” 按钮,即可看到页面变成图 13-141 所示的这样。

图13-140 AndServerTestApp的运行页面 图13-141 点击 “STARTSERVER” 按钮的结果

可以看到按钮下方的文字变成了 “The server is started”,就这证明 AndServer 启动成功了。接下来打开手机上的浏览器,试着访问一下 8080 端口的服务,输入 http://localhost:8080/encrypt?string=test&offset=0 ,显示的页面如图13-142所示。

图13-142在手机上访问8080端口的结果

可以看到,AndServer通过HTTP响应的方式返回了sign的值。

爬取数据

其实图 13-142 中返回的 sign 值就是我们一直说的加密参数 token,关于它的含义和生成过程,这里就不再赞述了。现在我们可以使用 Python 实现一下数据爬取了,在电脑上新建一个 spider.py 脚本,内容如下:

这里我们定义了一个 get_token 方法,接收 string 和 offset 两个参数,内部逻辑就是构造刚才 AndServer 提供的请求 URL,然后使用 requests 库请求这个 URL,并将响应结果转为 JSON 字符串,最后提取出 sign 值,即 token。

利用 get_token 方法的到 token 之后,我们就可以构造用来请求列表页的 URL,继而爬取列表页的数据了。现在试着运行一下 spider.py 脚本,运行结果如下:

可以发现发生了错误,请求被拒绝了,这是因为 AndServer 是运行在 Android 手机上的,只有在手机上才能访问到 localhost:8080,而脚本是在电脑上运行的。解决办法其实很简单,我们只需要使用 adb 命令配置一下端口转发就好了:

adb forward tcp:8080 tcp:8080

执行这个命令之后,电脑上 8080 端口收到的请求,就会被转发到手机上的 8080 端口,这样在电脑上访问 8080 端口就相当于访问手机上的 8080 端口了。重新运行 spider.py 脚本,运行结果如下:

这次我们成功爬取了数据。

总结

本节中我们利用 AndServer 成功在 Android 手机上搭建了 HTTP 服务器,并模拟执行了 so 文件,使执行结果可以通过 HTTP 服务器暴露出来。最后我们通过 Python 脚本调用了该 HTTP 服务器,拿到了关键的 token 值,成功爬取了数据。

对于模拟执行 so 文件的场景,AndServer-RPC 不失为一个不错的解决方案,大家在实际生产环境中也可以尝试应用它。