MENU

对某高体育特供版 Android 客户端的亿点点研究

• 2020 年 10 月 07 日 • 默认分类

首先呢其实该 app 在网上也是有一些人po出分析来的,在这里就先引用一下。当然,由于一贯的背运气,每次看到他人分析的文章,不是刚好自己已经绕了无数个弯路之后分析出来了,就是已经过期了。

chuanggao-checkIn ChuangGaoFucker ChuangGaoFuckerApp

其中对我启示最重要的是最后一个项目,虽然我在看到它的时候已经完成了分析,但是它让我明白最快方法将模拟打卡给整出来,必须要有手机端的参与。

对网络请求的初步分析

测试的手机平台是 Android 10,Magisk root 过,安装了 Edxposed 并加载了强制信任证书的模块。正常安装后,使用抓包软件来记录分析网络请求。

分析发现,首先,请求的域名可分为学校子域名的统一登录域名(由于是学校特供版,所以是使用学校的统一登陆网页来作为登录)、某高体育的一个域名、umeng 一类的统计分析域名、taobao 一类的广告域名,以及一个在学校内搭建的某高体育服务器域名。

整体逻辑是在登录前基本是明文请求明文应答,也不会有奇怪的鉴权头,访问一下某高体育官方的服务器拿一下登录地址然后跳转到学校的统一登陆网页来登录,登录成功后进入主界面。

在登录成功后,网络请求除了用于定位用的 amap 域名外,基本都是与一个在学校内搭建的某高体育服务器域名进行通讯。而后者的这个通讯,基本上符合 RESTFUL API,返回数据基本均为 json 格式。其问题在于,这些通讯会有统一字段的鉴权头,Post 的 body 会加密,部分回传的数据也会加密。

POST /cgapp-server/api/l/v6.1/prejudgment HTTP/1.1
imei: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx
v1: 37ed23acecac42d4b6ded55db88e34e6
timestamp: 8223956929011209673
sign: 7af73079e38c1d5ec756c058d7589d0316749es7e0e856d9c97a3faf54tc13e160998
cgAuthorization: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyMDE5MzAyODA4Iiwic2Nob29sX2lkIjoiMzgiLCJleHAiOjE2MDEzMDE0NTAsImlhdCI6MTYwMDQzNzQ1MCwianRpIjoiOGE5MjUzZmYtMzlkYS00YmIxLTk0Y2ItNTIwOGFkYmY4NjJjIn0.ChXia9y-AsQ2OpNDfi79perEPGGRYflXjaiRoA53CTE
User-Agent: ****app/1.0.1 (Linux; Android 10; *****)
client: qR4RD89HyxN+QJQnZdZeo5Myeq7n482Ni+MAEQlHKAowAol9yw1OLn+l8nPZZWpO
Content-Type: application/x-www-form-urlencoded

可以先搜索一下鉴权头,发现只有一个键为cgAuthorization的鉴权头的值在之前的网络请求的回应中出现过,具体来说,是在登录成功后服务器会下发一个字符串,字符串以.作为分割,其中一个就是此值,这个很重要,暂时按下不提。

此外,在打开的时候 Magisk 会提示应用要获取 Root 权限,拒绝后正常运行。

初步脱壳

直接将 apk 用 dex-tools 一类的软件解出 dex 后用 jadx-gui 反编译发现不太对劲, 发现是有腾讯加固。因此整一个 Android 7 以下的手机或者直接虚拟机(VMOS),安装 Xposed、反射大师以及本文主角,发现应用直接强制退出并 Toast 提示检测到了 Xposed。这个时候推荐一个模块叫“对话框取消2.3.5”,安装后对主角应用开启增强模式,启用“反禁用Xposed“”反检测“后重启,开启反射大师,成功过检测。

进去登录后进入主界面,可以长按反射大师浮窗的提取dex,可以dump出全部的dex文件5个,然后用 jadx-gui 反编译发现基本脱壳成功。

初步反编译+分析

虽然脱壳出来了dex,但是用 jadx-gui 反编译后发现还是会有一定的变量名混淆之类的。

通过搜索可能与网络请求有关的关键字找到了具体的网络请求的代码,发现其会调用 BackAES 类, 再在 dex 文件中的一个找到了它的具体实现,发现是一个 AES ECB 的 base64 编码的加密方式。之后再一波分析,发现呢密钥就是上文提到的”下发一个字符串“中的一个部分的前 16 个字符。此加密类用于加密具体体育跑步数据并发 Post 包给服务器,我们直接将之前拦截到的网络包用这个方法来解密,可以拿到原始明文。

{
    "activeTime": "00:14:30",
    "alreadyPassPoint": "",
    "alreadyPassPointResult": "",
    "avgPace": "04\u002726\u0027\u0027",
    "avgSpeed": "13.5",
    "beganPoint": "10.000000|100.00000",
    "beginTime": "2020-01-11 19:00:00",
    "calorie": 186.4,
    "coordinate": [
        {
            "a": 10.000000,
            "ac": 0.0,
            "d": 0.0,
            "da": 0.0,
            "o": 100.000000,
            "s": 0.0,
            "st": 0,
            "sta": 0,
            "t": 0,
            "v": 0
        },
        {
            "a": 10.000020,
            "ac": 3.7900924682617188,
            "d": 0.0,
            "da": 0.0,
            "o": 100.000020,
            "s": 10.44,
            "st": 0,
            "sta": 0,
            "t": 1600167711044,
            "v": 1
        }
    ],
    "endPoint": "10.000020|100.000020",
    "endTime": "2020-01-11 19:14:30",
    "indoor": 0,
    "isValid": 1,
    "isValidReason": "",
    "lastOdometerTime": "00:01:09",
    "maxSpeedPerHour": "22.21",
    "minSpeedPerHour": "2.92",
    "minuteSpeed": [
        {
            "min": "1",
            "v": "10.4"
        },
        {
            "min": "2",
            "v": "10.98"
        }
    ],
    "modementMode": "2",
    "name": "你的名字",
    "needPassPointCount": "0",
    "odometer": "3.26",
    "pace": [
        {
            "km": "1",
            "t": "05\u002713\u0027\u0027"
        },
        {
            "km": "2",
            "t": "04\u002704\u0027\u0027"
        }
    ],
    "phoneVersion": "手机型号,29,10|app版本号",
    "planRouteName": "跑步地点",
    "routeId": "47",
    "sportId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx",
    "stepCount": 1910,
    "stepMinute": "136.43",
    "xh": "学号"
}

如上就是一个最简结构,感谢开发的社畜,键的名字都很易读懂,这里提醒一下注意火星坐标系。在具体分析的时候会发现,coordinate里面是每两秒采样一次坐标点以及当前的速度,并计算出此两秒内的位移、开始跑步后的位移,以及当前时间戳。而整个json数据上也会有开始跑步和结束跑步的时间,所以针对两秒采样一次的位置左边点,以及时间戳的处理是整个数据制造的核心。

数据的制造

这边长话短说,注意点是

  1. 火星坐标和经纬度之间的转换,这个网络上到处都是(所以也很好奇目前这个”仍“存在的意义
  2. 地球上两坐标之间的距离,这个可以直接用流行算法即可
  3. 时间戳时间戳!这个是重点
  4. 在打coordinate的时候,要记得存储每分钟最低最高速度、配速等奇奇怪怪的东西来吐给下面的字段

我的具体流程是,使用 奥维互动地图 在地图上打出一系列的点(关键点),然后导出 Google Kml 格式的文件(此时是标准的经纬度),使用我的 python 脚本读入这些关键点,自动根据设定的速度、步频和起点来进行升采样与随机噪声。目前我的实际操作中,针对一个标准体育跑道,打一圈关键点的数目可以在20-70之间,之后实际测试过时速 5-35 km/h 的速度来升采样与随机噪声,理论上可以绕无数圈的操场,打出来的路径均符合实际。之后的可以对速度进行函数的拟合,来做到越跑越慢以更符合实际。

分析鉴权头

我原本以为鉴权头不难,结果发现这个才是核心(自己还是too young)

继续长话短说,鉴权头的加密实现方式是在libAMapSDK_Location_v6_6_0.so库内的。不过说实话这个名字就很忽悠人,因为高德定位的so库是libAMapSDK_MAP_v7_4_0.so,这可能就是一种“混淆吧”。

这个 apk 里面的调用,我们具体是要用 cgapiEnrypt 这个方法。

package net.crigh.api.encrypt;

public class ChingoEncrypt {
    public static native String cgapiAESEncrypt(String str);

    public static native String cgapiEnrypt(String str, String str2);

    public static native int cgapiVerify(String str, String str2, String str3, String str4);

    static {
        try {
            System.loadLibrary("AMapSDK_Location_v6_6_0");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

那就简单高效的方法就是自己整一个 apk 来调这个 so 库,结果呢一打开就直接闪退,也没有报错。

作为菜鸡一波查找后发现这个 so 库应该是有签名验证一类的,检测到 apk 的签名和 so 库内部存储的不一样就直接退出。那么就到了接下来的一步了。

对 so 库进行反编译

这个就真的的反编译、反汇编了,因为 so 库出来的就是二进制的可执行文件,直接反编译出来就是汇编码,修改也是要对着 arm HEX 机器码来修改十六进制。

使用 IPA Pro7.0 来反编译,这个软件因为我买不起,所以用起来有一点问题,这里总结一下:

  1. 有一些 Java 的变量类型可能反编译出C伪代码来类型不对,百度搜一下 jni.h 一类的关键字,来导入一下,然后手动按一下。
  2. 好像没有办法根据 C 伪代码来切换到对应的汇编语句,所以我在用的时候只能开汇编图形导图,一直按 Tab 来看到了对应伪代码没有,之后再修改。

首先根据网上教程的经验,来搜索 signature 可以发现有两个非导出函数,然后再在这两个非导出上面按 X 来看,发被 JNI_Onload 和 cgapiEnrypt 引用。

首先这个 so 库的 cgapiEnrypt 是导出函数, 我当初是在想是不是在这个具体的函数上会有签名鉴权的玩意儿呢。

image-20201007224258673

然后捏发现了这个,但是把这个if给取反之后,发现它还是闪退,这就很奇怪了。然后我又在这里兜兜转转,后面发现,干嘛不去看一下 JNI_Onload 呢?

然后就发现了有一个 exit 的代码,把它给 NOP 后就不闪退了,就会报错说某一个 Activity 不存在,这个简单,建一个同名的空白 Activity 即可。

image-20201007225531891

之后就很简单啦~

frida hook 来二次确认参数

frida 整体使用不难,这里就不赘述了。

几个我使用中的问题:

  1. 由于 so 库并不是在 app 一启动的时候就启动起来,所以要等 app 启动起来之后再 hook。
  2. 不知道为什么,我直接终端执行那个 js 根本木有输出,后面曲线救国 js 写好之后再扔到 python 里面来注入。
返回文章列表 文章二维码
本页链接的二维码
打赏二维码