X麦APP逆向

听说演唱会一票难求,有人问我能不能写个抢票程序,仔细研究了半个月,解决处理一个一个难题,尤其是人机验证环节验证码处理一度让我有退却的想法,不过好在最后还是解决了,就是不知道离当黄牛还有多远距离^_^。

先睹为快

先来看看完成之后的效果

主要演示了购买流程和过验证码的环节,下面就来看看逆向学习的过程,这里借鉴了文章,但不会介绍具体实现过程,仅介绍学习当思路和流程。

工具准备

工欲善其事,必先利其器,在整个的分析流程需要用到如下工具

  • jadx-gui,APK 静态代码解析
  • frida,动态代码分析执行
  • 已 root 的 Android 机,安装运行 frida-server
  • adb,电脑 与 手机通讯
  • wireshark,抓包分析

Roo Android 机

关于 root 手机的准备可以参考去年我折腾旧手机的文章

frida

frida 作为动态工具类似 x64dbg,可以帮我们动态分析运行过程中的调用关系和变量数值,由于分析的是 App,所以需要在电脑端和手机端分别安装 frida

  • 电脑端:frida-tools,作为分析端,发送操作指令分析返回信息
  • 手机端:frida-server 作为电脑端指令接收方,对目标应用进行操作并反馈

电脑端安装比较容易,安装 Python 后直接 pip install frida-tools 即可,官方可能比较慢,可以借助国内源下载。

关于 frida-server 的安装,可以参考 frida android 篇教程,首先查看架构

1
2
$ adb shell getprop ro.product.cpu.abi
arm64-v8a

发布页面,找到对应架构的 frida-server ,由于本例中是 arm64 所以选择 https://github.com/frida/frida/releases/download/16.1.9/frida-server-16.1.9-linux-arm64.xz 下载,解压得到 frida-server,然后丢到手机上按如下命令运行即可

1
2
3
4
$ adb root # might be required
$ adb push frida-server /data/local/tmp/
$ adb shell "su -c chmod 755 /data/local/tmp/frida-server"
$ adb shell "su -c /data/local/tmp/frida-server &"

其他

其他软件的安装相对简单,此处仅提供官方地址,安装过程就不再缀述。

手机抓包

根据本人粗浅的经验,逆向分析一般从两个方向进行

  • 动态分析,通过界面操作触发观察记录数据
  • 静态分析,根据可观测数据定位相关静态代码

分析静态代码模拟相关操作,达到相同效果。一般来说通过抓包可以获得一个不错的切入口,但是由于接口通讯基本都使用了 HTTPS,所以抓包需要一些小技巧才能分析。

这里沿用原文的方法,使用 tcpdump + frida + wireshark 的方式来进行数据包解密。主要原理可以类似之前博客 应用程序抓Https包 ,核心是通过记录会话密钥来达到解密功能。

tcpdump 安装

首先准备 tcpdump 软件,下载架构匹配 tcpdump,由于本例中是 arm64,所以前往 64位下载地址下载,然后执行如下命令推送至手机里

1
2
$ adb push tcpdump /data/local/tmp/
$ adb shell "su -c chmod 755 /data/local/tmp/tcpdump"

会话密钥监听

以上准备工作只是做了一半,还需要准备 frida 脚本来监听记录会话密钥,大佬已经写好了直接拿来用即可,脚本内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// sslkeyfilelog.js
function startTLSKeyLogger(SSL_CTX_new, SSL_CTX_set_keylog_callback) {
console.log("start----")
function keyLogger(ssl, line) {
console.log(new NativePointer(line).readCString());
}
const keyLogCallback = new NativeCallback(keyLogger, 'void', ['pointer', 'pointer']);

Interceptor.attach(SSL_CTX_new, {
onLeave: function(retval) {
const ssl = new NativePointer(retval);
const SSL_CTX_set_keylog_callbackFn = new NativeFunction(SSL_CTX_set_keylog_callback, 'void', ['pointer', 'pointer']);
SSL_CTX_set_keylog_callbackFn(ssl, keyLogCallback);
}
});
}
startTLSKeyLogger(
Module.findExportByName('libssl.so', 'SSL_CTX_new'),
Module.findExportByName('libssl.so', 'SSL_CTX_set_keylog_callback')
)

将上述内容保存为 sslkeyfilelog.js之后,用 frida 命令 frida -U -l .sslkeyfilelog.js -f cn.damai 加载运行。运行后表明会话密钥记录开始。接着执行 ./tcpdump -i any -w tmp.pcap,接着在App界面操作选票下单,跟随界面操作 frida 界面会滚动出现一些数据,这些数据就是会话密钥,操作结束后关闭 tcpdump 以及 frida,将 frida 界面里的数据保存为 sslkey.txt,其类似如下内容

1
2
CLIENT_RANDOM 557e6dc49faec93dddd41d8c55d3a0084c44031f14d66f68e3b7fb53d3f9586d 886de4677511305bfeaee5ffb072652cbfba626af1465d09dc1f29103fd947c997f6f28962189ee809944887413d8a20
CLIENT_RANDOM e66fb5d6735f0b803426fa88c3692e8b9a1f4dca37956187b22de11f1797e875 65a07797c144ecc86026a44bbc85b5c57873218ce5684dc22d4d4ee9b754eb1961a0789e2086601f5b0441c35d76c448

通过命令 adb pull /data/local/tmp/tmp.pacp . 将抓包数据下载到本地,接着在 wireshark 首选项 -> protocols -> TLS中,设置 (Pre)-Master-Secret log filename为上述sslkey.txt,然后用 wireshark 打开 tmp.pacp,分析操作请求。

接口解析

在 wireshark 窗口中显示一些请求,其中比较重要的是如下请求

  • /gw/mtop.trade.order.build,订单构建
  • /gw/mtop.trade.order.create,订单提交

关于请求内容解析可以参考原文,这里对原文的推理做一些其他方式的解析,从静态分析的角度来进行签名解析。

静态分析

需要说明的是静态分析更多的是需要耐心和合理的猜测。用 jadx-gui 打开 x 麦 apk 文件,加载完毕后,使用快捷键 ctrl + shift + f 搜索文本 mtop.trade.order.build,可以定位到类 UltronBuildOrder ,里面的方法和环境配置有关,有getApiNameOnline getApiNameTest 等方法,根据方法名推测是用于不同环境下,Online作为在线的意思概率很高,对着 getApiNameOnline 右键查找用例,可以追踪到 DMQueryKey 的方法 getBuildApiName,接着右键查找用例,可以定位到 UltronDataManager,可以看到其方法名有 createBuildRequest createSubmitRequest 基本对应 订单的构建 以及 提交,基本可以确定 UltronDataManager 类为订单基础类。

定位过程中可以借助 frida-trace 追踪函数的调用链,命令为 frida-trace -U -j '类名!方法名' 进程名,这里就不做展示了。

frida 脚本编写

在实际编写功能离不开对 frida 脚本的熟悉,建议系统学习一遍官方文档,这里记录下编写过程中我遇到的问题

主线程

Andriod 系统中有些函数不能运行在工作线程中,这时我们就需要运行在主线程中,例如我们获取应用当上下文可以通过如下方式

1
2
3
4
Java.scheduleOnMainThread(function () {
let application = Java.use("android.app.ActivityThread").currentApplication();
let context = application.getApplicationContext()
});

同步通知

在编写中容易涉及多线程通知的问题,这里我用的是 CountDownLatch 方式可以有效解决

1
2
let CountDownLatch = Java.use("java.util.concurrent.CountDownLatch");
let countDownLatch = CountDownLatch.$new(1)

写在最后

关于x 麦的逆向其实省略了很多没有写,包括订单构建中的场次信息如何获取,验证码如何处理等,都是花费了很多精力去处理的,精力有限,只做些简单的记录吧。对了,在这趟逆向之旅中,我深刻感受到知识融汇贯通的魅力,没有 Root 、 adb 和一些编程学习的能力,也许早就中途放弃了,希望各位在逆向的路上不要轻易放弃,坚持探索终会有成果的。