Web,安全研究

破解CSU校园跑步

本来是去年6月份的文,拖到现在

preface

技术栈:小程序反编译,JS逆向,蓝牙伪造,抓包,移动端

正常来说,跑步定位有两种方式,一种是GPS定位,一种是基站定位,大部分软件都会采用GPS定位,目前最精确的也是GPS定位,GPS定义可以获取具体的经纬度,当在地图上移动的时候,也能获取到具体的移动路线,而基站定位只能获取大致范围,比如某某公司附近,某某广场旁,无法显示具体移动路线,所以大部分传统跑步定位只需要模拟GPS信号即可

如上,传统校园跑只需模拟GPS信号,内卷的话可以再检测陀螺仪运动,但题主这次碰到的情况是只检测了GPS和蓝牙

背景

由于题主吹牛波一,觉得绕过跑步这种太过简单,早早夸下海口,结果在用fakelocation的时候怎么也绕不过去。

对方用的是跑步小程序,绑定手机号连接到学校后台,然后开始跑步,跑步前要开启小程序蓝牙权限(伏笔)。

开始Bypass

题主用fakelocation模拟步数后,在各个地图app下都可以看到明显的跑步轨迹,围绕校园操场做不是很规则的椭圆运动,但小程序的开始跑步就是点不动(没反应),对方跑到操场去后发现即可点动,怀疑疑似蓝牙相关,聪明的我开始搜索相关关键字:找到下文

中南大学-阳光课外锻炼系统-竞价公告-机电设备采购平台

Untitled

一般这种软件都是通过招标进入的学校,找招标文书看就行了,既然确认了小程序与蓝牙定位相关。联想到我大四的时候学校有人破解了共享单车,原理也是通过小程序的蓝牙包伪造,联系到同学。

同学跟我说这个是蓝牙BLE相关,让我去学一下概念和小程序API

蓝牙低功耗 (Bluetooth Low Energy, BLE) | 微信开放文档

Untitled

如上介绍,蓝牙BLE简单理解也是由一个服务端Server 中心设备和多个客户端client 外围设备组成。

在大部分情况下,一般由一个蓝牙桩在某个地点充当服务端,用户的手机等移动设备充当客户端,像共享单车,本文的跑步定位都是这种。当然服务端和客户端也可以反过来。

现在知道了蓝牙定位的大概原理,我们就需要进行绕过,作为多年安全搬砖工程师,第一反应肯定是伪造绕过,于是我们就需要知道如何伪造蓝牙信号,因为此次跑步端采用小程序实现,小程序本质是本地运行Javascript的,我们可以通过一些反编译手段,获取小程序的源代码,通过对源代码的审计来进行伪造。

小程序在安卓机上的存储地址是

/data/data/com.tencent.mm/MicroMsg/{一串16进制字符}/appbrand/pkg/

由于我的物理机有太多小程序,小程序的文件夹是随机字符串,于是我在安卓模拟器上只运行一次小程序,再通过ADB安卓调试工具将上面的文件夹脱了下来,但即使这样小程序文件夹中也存在多个文件夹,由于不好分辨具体哪个是我的目标小程序我只能全部下载,分别反编译后在判断那个是我的目标。

微信小程序反编译可以用下面这个工具

https://github.com/xuedingmiaojun/wxappUnpacker

使用wxappUnpacker进行反编译,开始进行代码审计

Untitled

Untitled

然后就是正常的Js审计,会小程序开发的会熟悉一些,里面的代码就是正常的小程序目录结构。但由于正常小程序打包会进行一些pack操作导致丢失一部分逻辑链和符号信息。类似webpack,所以审计起来会有一点麻烦。但好在此系统主题逻辑代码并没有丢失太多。所以审计起来不太费力。

小程序在使用的时候要点击 蓝牙功能,我们根据微信小程序api找到对应函数

Untitled

所以全局搜索startBluetoothDevicesDiscovery,定位到函数调用点

Untitled

根据实际情况,我们无法开始跑步,所以我们的目的是找到开始跑步的函数,即入口点。

根据api,小程序开始扫描蓝牙设备后需要通过onBluetoothDeviceFound 监听函数进行事件处理,此函数的返回值包含了扫描到的蓝牙设备。继续搜索

Untitled

onBluetoothDeviceFound: function() {
        var a = this;
        wx.getBluetoothDevices({
            success: function(e) {
                for (var n = 0; n < e.devices.length; n++) {
                    var o = 0;
                    if ("xBeacon" == e.devices[n].localName || "xBeacon" == e.devices[n].name) {
                        if (null != e.devices[n].serviceData && void 0 != e.devices[n].serviceData && e.devices[n].serviceData.hasOwnProperty("00001529-0000-1000-8000-00805F9B34FB")) for (var i = 0; i < a.data.deviceList.length; i++) if (a.data.deviceList[i].deviceAddress == t(e.devices[n].serviceData["00001529-0000-1000-8000-00805F9B34FB"])) {
                            a.venueCardRegister(a.data.deviceList[i].deviceId);
                            var r = new Date();
                            a.setData({
                                lastdata: r,
                                gspstatus: 0,
                                yxvalidCount: a.data.yxvalidCount + 1,
                                quyuoff: !0
                            }), wx.setStorageSync("lastdata", a.data.lastdata), wx.setStorageSync("yxvalidCount", a.data.yxvalidCount), 
                            o = 1, wx.stopBluetoothDevicesDiscovery({
                                success: function(a) {
                                    wx.closeBluetoothAdapter({
                                        success: function(a) {
                                            wx.openBluetoothAdapter({
                                                success: function(a) {
                                                    wx.startBluetoothDevicesDiscovery({
                                                        powerLevel: "high",
                                                        success: function(a) {},
                                                        fail: function(a) {}
                                                    });
                                                }
                                            });
                                        }
                                    });
                                }
                            });
                            break;
                        }
                        if (1 == o) break;
                    }
                }
            },

可以看到,这个函数直接对所有扫描的设备进行遍历,判断扫描的设备名字是否含有 xBeacon

如果有,则判断其serviceData是否属性名00001529-0000-1000-8000-00805F9B34FB

现在我们知道了程序的逻辑,但现在有个小问题,我们是通过反向审计找到的功能点,程序内不止一个判断蓝牙设备的函数,我们不能判定这个函数就是开始跑步所判断的函数,我们还需要找到对应的函数入口。

好在进行搜索,程序内所有的蓝牙判断函数都是一样的,我们可以放心的按照上面的模式进行伪造。但如果要找到程序入口,我们可以从前端下手。

小程序里面,需要点击长按开始 开始跑步,全局搜索可以搜到对应小程序前端和点击事件

Untitled

Untitled

根据正常的审计经验,基本可以判断这个函数就是真正的入口判断函数,可以看到,如果没有找到蓝牙设备,会弹出右下获取线路,请前往指定区域 这也符合小程序的逻辑。

Solve

现在我们需要Bypass进行绕过,我当时想到了两个思路

  1. 将小程序的判断逻辑删除进行重打包
  2. 使用frida动态hook绕过判断
  3. 模拟一个符合程序规范的蓝牙BLE设备

第一个思路,微信会对小程序开发者进行签名,重打包绕不过签名校验,只能使用自己的签名重新打包,但是反编译再编译并不是无损的,会有一堆Bug,过程太繁琐,放弃。

第二个思路,当时Frida Hook小程序资料太少,未找到,放弃

于是使用第三个思路,我一开始想用计算机Python来模拟BLE设备,但只能更改电脑的蓝牙名称,无法发送蓝牙广播,更别谈模拟BLE信号。

然后我准备使用魔法打败魔法,自己开发一个测试小程序发送蓝牙BLE,但是用了所有api,只能使得onBluetoothDeviceFound函数获取到advertisData。无法获取到serviceData。

于是请教了下清华的iot大哥xuanxuan

Untitled

Untitled

Untitled

发现一个神器NRF CONNECT,这个软件可以直接模拟蓝牙BLE,甚至可以直接从周围环境copy蓝牙信号,因为我一直在家里闭门造车,如果可以去到学校操场的话,可以直接用这个软件复制模拟操场的蓝牙插桩。

Untitled

Untitled

😠要是早知道这个东西 我还那么努力审什么。

result

拿一个手机开启NRF CONNECT,使用另一个手机点击小程序开始跑步,成功绕过。

放一个结果视频吧
https://v.douyin.com/yFByBFh/

评论

  1. evoA
    Chrome 119

    12213

  2. evoA
    Chrome 119

    12213

This is just a placeholder img.