某10

懒得写,咕了

小米运动APP的坎坷抓包之旅

0x00

今天在研究小米手环3 NFC版的模拟Mifare卡功能,发现模拟出的卡0扇区是只读的,遂在网上搜索操作0扇区方法。
经过一番搜索 发现了这位老哥的一篇文章《小米手环3 NFC 自定义 门禁卡数据》。仔细阅读了一下,发现这位老哥的思路可行。当我架好了Fiddler,安卓端设置好代理,安装好Fiddler的根证书,开始抓包的时候却发现Fiddler并不能解密,而且小米运动app也无法正常通信。尝试用浏览器开了个网页检查了下证书,发现Fiddler证书配置正确,并且可以正常抓包。猜测是App设置了只信任系统中的证书。
无奈,只好对APK方面进行动手。

提示:图片看不清可以选择右键在新标签页中查看

0x01
首先准备APK反编常用工具一套:apktool, baksmali, dex2jar, JD-GUI
另外需要准备APK重打包签名工具jarsigner(JDK自带)
准备一个小米运动APP:com.xiaomi.hm.health.apk

0x02
首先用apktool将APK的classes*.dex逆向为smali文件

apktool -r d ./android-async-http-1.4.8.jar

由于我们不需要解开资源文件所以这里加了-r开关,不解开资源文件可以避免打包时的一些错误,并且可以加快打包速度。

然后使用d2j-dex2jar将classes*.dex转换成jar,这样可以方便我们使用JD-GUI工具查看源码。
jd-gui1.png
将转换后的jar拖进JD-GUI,顿时头皮一麻,几乎所有业务类、方法、变量名全部被混淆成了oO0的组合。
思考了一下,想到刚刚用Fiddler抓包时app无法正常通信,是否可能会吐出异常日志呢?或许我可以从logcat中的报错找到对应方法。
0x03
想到这里,将手机连到电脑,启用开发者模式,使用adb shell ps查找到包名为com.xiaomi.hm.health程序的PID。用adb logcat --pid=查找到的PID来监视app进程的日志输出。
logcat.png
随便在软件上操作一下,果然出现报错堆栈信息,根据报错信息我从上从下查下去,抛除内置库,发现这个app使用了一个名为com.loopj.android.http的库。
0x04
根据以往开发经验,这种库中应该有可以关闭证书校验的开关。在JD-GUI中可以看到这个库并没有被混淆。但由于编译后代码没有注释通常难以理解。经过一番搜索,找到了该库的源码:android-async-http。在AsyncHttpClient类第175行中,发现这个类的构造方法中fixNoHttpResponseException参数可以省略ssl验证。
0x05
看到这里,我就准备着手修改代码了。定位到使用Apktool逆向的文件中,使用编辑器打开AsyncHttpClient.smali,由于我以前从未接触过smali,打开文件的一瞬间还是有点头大。但还好,这东西虽然类似汇编,但可读性不知道比汇编高到哪里去了,通过简单的阅读,找到了构造函数的位置。
edit.png
结构层次一目了然,其中可以观察到省略ssl参数默认值为false。在smali文件中表现为const/4 v0, 0x0。可以推测出最后的0x0代表false,按照经验0x1应该就是代表true了。逐个修改完后保存文件,使用Apktool将smali文件打包回apk。

apktool b --use-aapt2 ./com.xiaomi.hm.health -o ./miapp.apk

打包出apk,然后使用对这个apk进行签名,由于之前搞过两天安卓开发,所以我已经有准备好的密钥文件了。没有可以到网上搜一搜生成方法。

jarsigner -verbose -keystore <密钥存储文件.jks> -signedjar <签名后的apk输出路径> <待签名的apk路径>  <对应密钥名>

将签名后的apk安装到手机前,记得卸载原来的apk,因为我没有修改apk包名,原签名与修改后重打包的签名不一致会导致无法安装。启动app正常。当我充满希望再次抓包的时候,现实再次给了我重重一击。Fiddler还是无法解密,App也没能绕过ssl检查进行正常通信。
0x06
我再次仔细的对比代码,结合Loopj项目历史Issues关于SSL的问题,发现小米运动app中的loopj库虽然有开关可以控制ssl检查,但实际上缺少了绕过ssl检查的相关流程的代码,再次查阅原库。在第306行中找到了消失的绕过ssl检查代码。这里调用了一个MySSLSocketFactory类,也是app中缺少的。由于经验缺乏,我选择了比较绕的方法来解决问题。先下载了一个老发行版。下载下是jar格式,里面类也都是class文件,并不是smali文件,那就开始转吧。
用d2j-jar2dex工具将jar转换为dex

d2j-jar2dex.sh --output=loopj.dex android-async-http-1.4.8.jar

然后使用baksmali工具再将dex拆成smali

java -jar ./baksmali-2.3.4.jar d ./loopj.dex

将其中的MySSLSocketFactory$1.smaliMySSLSocketFactory.smali文件复制到对应位置。
这里我还踩了个坑,用了d2j-dex2smali来拆loopj.dex,结果是复制过去,修改完重打包时apktool不能正常识别d2j-dex2smali吐出的smali,会报错。

0x07
QQ截图20200113030150.png
再次返回JD-GUI,对照smali代码直接将SSLSocketFactory部分改成MySSLSocketFactory。

0x08
再次保存、打包、签名安装。打开app,使用Fiddler抓包。
QQ截图20200113030417.png
抓包解密成功。但是门卡模拟还是进不去,该部分请求仍然无法解密。查看日志,并没有直接输出什么错误,未完待续吧。
LogCat记录一下

01-13 03:17:30.340 27289 27588 I Logcat  : api --> getCardList()  >>> (ApiWrapper.kt:34) -> invoke() | Thread:RxCachedThreadScheduler-4
01-13 03:17:30.349 27289 27588 I Logcat  : api --> O00000Oo(374cf6c517d5)  >>> (ApiWrapper.kt:34) -> invoke() | Thread:RxCachedThreadScheduler-4
01-13 03:17:30.354 27289 27588 I Logcat  : api <-- O00000Oo()【执行成功】, 耗时: 1 ms >>> (ApiWrapper.kt:43) -> invoke() | Thread:RxCachedThreadScheduler-4

2020/01/14
后来发现小米运动app门卡请求部分调用的是okhttp3库,研究了半天,又搓了一个MyX509TrustManager

package okhttp3.internal;

import java.security.cert.CertificateException;
import javax.net.ssl.X509TrustManager;

public class MyX509TrustManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return new java.security.cert.X509Certificate[]{};
        }
}

编译成smali,放在okhttp3\internal下面 与Util.smali同级,然后修改Util.smali,将原TrustManaver修改成我们自己的MyX509TrustManager

.line 667
invoke-virtual {v0}, Ljavax/net/ssl/TrustManagerFactory;->getTrustManagers()[Ljavax/net/ssl/TrustManager;

move-result-object v0
//修改为
.line 667
const/4 v1, 0x1
new-array v0, v1, [Ljavax/net/ssl/TrustManager;
const/4 v1, 0x0
new-instance v2, Lokhttp3/internal/MyX509TrustManager;
invoke-direct {v2}, Lokhttp3/internal/MyX509TrustManager;-><init>()V
aput-object v2, v0, v1

这样就完成了对okhttp ssl检查的绕过,再使用fiddler进行抓包,这次可以成功截获门卡部分请求的api。
QQ图片20200114211022.png
0xFF
遗憾的是 新版本的api已经变更为服务器下发command执行,其中似乎指令数据还没有研究明白。

评论卡

已有 4 条评论

  1. snowtank
    snowtank
    2020年01月17日

    小撸厉害啊!

  2. 123
    123
    2020年09月11日

    返回的指令是ADPU指令,可以通过某些网站解析它的含义,但是还是会有一部分解析不出来。

  3. zou
    zou
    2021年06月06日

    请问登录的时候也会进行加密,这部分应该怎么解决呢

    1. 小撸
      小撸
      2021年06月07日

      时间有点久远了,如果正确干掉所有证书强验证,应该是可以正常登录的