首页
社区
课程
招聘
[原创]某品牌电动牙刷小程序"越权控制"逆向
发表于: 1天前 1020

[原创]某品牌电动牙刷小程序"越权控制"逆向

1天前
1020

说是"越权",其实产品设计时可能根本没考虑鉴权。

wux1an\wxapkg 微信小程序解包工具 + 3.9 版本 Windows 端微信。

GitHub Copilot 代码辅助分析 AI + VS Code 编辑器。

Wireshark 流量分析工具。

Sniffle 抓包环境

nRF Connect

使用 3.9 版本 Windows 端微信打开目标小程序。

使用 wxapkg 工具对微信小程序进行解包。

image.png

image.png

发现该小程序解包后的项目结构还是较为清晰的,主要关注 PagepackageHairdrierpackageBrush 这几个文件夹以及主目录下的这些 .js 文件。

通过文件名大致可以猜测 packageHairdrier 为该品牌吹风机控制的相关代码,packageBrush 为该品牌电动牙刷控制的相关代码。

而在 Page 文件夹中,有如下页面,应该是用户登录、设备连接等通用功能代码。

image.png

image.png

.js 的代码量不算少,为了能快速定位小程序控制电动牙刷的关键代码,需要结合前端显示的文字定位关键代码。

先逆向追踪 开关 这个按钮的功能。

直接在 VS Code 编辑器中搜索 开关 关键字。

image.png

因为 开关 按钮的开关文字是单独出现的,因此可以大致确定相关代码位于 .\packageBrush\chunk_7.appservice.js.\packageBrush\app-service.js 两个文件中。

根据以往微信小程序解包的经验,app-service.js 一般包含所有分片代码,上述两个文件中所出现的 开关 相关代码实际是同一份代码。

因此,接下来只需要对 .\packageBrush\chunk_7.appservice.js 相关代码进行逆向分析。

image.png

image.png

image.png

image.png

此时可以联想到,进入小程序之后,如果是未登录的状态,依旧可以显示控制电动牙刷的界面,但是按下任意按钮后,都会跳转到登录页面。

.\packageBrush\chunk_7.appservice.js 代码的最上方,可以看到我们上文分析函数所处的页面,即 displayControl,看来我们现在所分析的页面,并非电动牙刷的真实控制页面,而是未登录状态下的展示页面。

image.png

image.png

尝试搜索 brushControlbrushControlPlus 关键词,试图找出它们在哪个 chunk_*.appservice.js 文件中出现。

最终确认 brushControl 对应 chunk_1.appservice.js,而 brushControlPlus 对应 chunk_2.appservice.js

但是进入 chunk_2.appservice.js 却发现其代码仅有 51 行,应该是一个未启用的页面。

而在 chunk_1.appservice.js 中发现代码引用了 ble 蓝牙相关的库函数,看来这个代码文件应该就是我们想要的了。

image.png

在该页面中继续寻找开关按钮,定位到了 开启 关键字。

这一点比较意外,在真正的控制界面中,开启电动牙刷按钮下写的是 开启,而非展示页面下显示的 开关,在这个地方浪费了点时间,刚开始就应该进入真实的页面去锁定 UI。

至此,基本定位到了真正的开启按钮。

image.png

开启 关键词是在 j 函数中(第 57 行),其功能看起来是控制该按钮的显示。

接下来需要查看该按钮所绑定的函数,于是尝试搜索 j 函数在哪里调用。

j 函数下方(第 66 行)发现了对 j 函数的调用。

同时,在同一个 g 函数中找到了绑定的控制函数 onIsSwitch 的相关代码,根据 g 函数的结构和 UI 的界面推断:第 61~63 行代码是对开关图片的事件绑定,而第 64~66 行是对开关下方文字的事件绑定。

所以 onIsSwitch 就是我们需要的函数。

image.png

onIsSwitch 函数中,可以明显看到 onOff()onOpen() 函数。

继续分析这两个函数,但直接搜索后发现,发现这两个函数并未在 chunk_1.appservice.js 中定义,因此需要找到这两个函数所属对象(o)的定义位置。

向上查看代码,发现了 o 的包装位置,并且包装函数 h 也位于其下方方。

image.png

image.png

image.png

image.png

image.png

image.png

image.png

onOpen()aa0f010101a4 (类型: 0f,数据: 01,响应: 0f) - 开启按钮。

onOff()aa0f010100a5 (类型: 0f,数据: 00,响应: 0f) - 开启按钮。

onOpenAirplane()aa07010101ac (类型: 07,数据: 01,响应: 07) - 旅行模式(开)

onOffAirplane()aa07010100ad (类型: 07,数据: 00,响应: 07) - 旅行模式(关)

onOpenVibration()aa0b010101a0 (类型: 0b,数据: 01,响应: 0b) - 换区提醒(开)

onOffVibration()aa0b010100a1 (类型: 0b,数据: 00,响应: 0b) - 换区提醒(关)

onOpenTreble()aa0e010101a5 (类型: 0e,数据: 01,响应: 0e) - 高频模式(开)

onOffTreble()aa0e010100a4 (类型: 0e,数据: 00,响应: 0e) - 高频模式(关)

image.png

image.png

虽然本次只逆向了电动牙刷某一款的蓝牙协议,但是在该小程序中,无论是该品牌的吹风机还是其他款的牙刷,它们的蓝牙通信指令均未进行鉴权或加密。

由于这些设备缺少鉴权,理论上直接使用官方小程序即可连接附近的同类设备,这可能是产品设计阶段未充分考虑设备侧鉴权,毕竟牙刷和吹风机这种东西除了在宿舍这种场景,通常也较少出现连接他人设备的实际场景。

AA [TYPE] [LEN] [FUNC] [DATA] [CRC]
│   │      │     │      │      └─ CRC校验码 (XOR异或)
│   │      │     │      └──────── 数据字节
│   │      │     └─────────────── 功能码
│   │      └───────────────────── 数据长度
│   └──────────────────────────── 指令类型
└──────────────────────────────── 帧头 (固定)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import asyncio

from bleak import BleakClient, BleakScanner

WRITE_CHAR = "0000ff02-0000-1000-8000-00805f9b34fb"
NOTIFY_CHAR = "0000ff02-0000-1000-8000-00805f9b34fb"

CMD_OPEN = "aa0f010101a4"
CMD_OFF = "aa0f010100a5"
CMD_AIRPLANE_ON = "aa07010101ac"
CMD_AIRPLANE_OFF = "aa07010100ad"
CMD_VIBRATE_ON = "aa0b010101a0"
CMD_VIBRATE_OFF = "aa0b010100a1"
CMD_TREBLE_ON = "aa0e010101a5"
CMD_TREBLE_OFF = "aa0e010100a4"

class Controller:
    def __init__(self, mac):
        self.mac = mac.upper()
        self.client = None
        self.response = None
        self.waiting = False

    async def connect(self):
        print(f"搜索:{self.mac}")
        devices = await BleakScanner.discover(timeout=10.0)

        target = None
        for dev in devices:
            if dev.address.upper() == self.mac:
                target = dev
                print(f"找到:{dev.name}")
                break

        if not target:
            print("未找到设备")
            return False

        print("连接中……")
        self.client = BleakClient(target.address)
        await self.client.connect()

        if self.client.is_connected:
            print("连接成功")
            await self.client.start_notify(NOTIFY_CHAR, self._on_notify)
            return True

        return False

    def _on_notify(self, sender, data):
        self.response = [f"{byte:02x}" for byte in data]
        self.waiting = False
        print(" ".join(self.response))

    async def send(self, cmd, expected=None):
        if not self.client or not self.client.is_connected:
            return None

        print(cmd)
        self.waiting = True
        self.response = None

        await self.client.write_gatt_char(
            WRITE_CHAR,
            bytes.fromhex(cmd),
        )

        timeout = 0
        while self.waiting and timeout < 30:
            await asyncio.sleep(0.1)
            timeout += 1

        if timeout >= 30:
            return None

        if expected and self.response and len(self.response) > 1:
            return self.response[1] == expected

        return self.response

    async def disconnect(self):
        if self.client and self.client.is_connected:
            await self.client.disconnect()

    async def turn_on(self):
        result = await self.send(CMD_OPEN, "0f")
        print("已打开" if result else "操作失败")

    async def turn_off(self):
        result = await self.send(CMD_OFF, "0f")
        print("已关闭" if result else "操作失败")

    async def airplane(self, enable):
        cmd = CMD_AIRPLANE_ON if enable else CMD_AIRPLANE_OFF
        status = "开启" if enable else "关闭"
        result = await self.send(cmd, "07")
        print(f"旅行模式已{status}" if result else "操作失败")

    async def vibrate(self, enable):
        cmd = CMD_VIBRATE_ON if enable else CMD_VIBRATE_OFF
        status = "开启" if enable else "关闭"
        result = await self.send(cmd, "0b")
        print(f"换区提醒已{status}" if result else "操作失败")

    async def treble(self, enable):
        cmd = CMD_TREBLE_ON if enable else CMD_TREBLE_OFF
        status = "开启" if enable else "关闭"
        result = await self.send(cmd, "0e")
        print(f"高频模式已{status}" if result else "操作失败")

async def menu(ctrl):
    while True:
        print("\n" + "=" * 40)
        print("1. 打开    2. 关闭")
        print("3. 旅行    4. 换区")
        print("5. 高频    q. 退出")
        print("=" * 40)

        choice = input("选择:").strip()

        if choice == "1":
            await ctrl.turn_on()
        elif choice == "2":
            await ctrl.turn_off()
        elif choice == "3":
            await ctrl.airplane(input("1 开 / 0 关:") == "1")
        elif choice == "4":
            await ctrl.vibrate(input("1 开 / 0 关:") == "1")
        elif choice == "5":
            await ctrl.treble(input("1 开 / 0 关:") == "1")
        elif choice.lower() == "q":
            break

        input("按 Enter 继续……")

async def main():
    mac = input("MAC 地址:").strip()
    if not mac:
        return

    ctrl = Controller(mac)
    if await ctrl.connect():
        try:
            await menu(ctrl)
        finally:
            await ctrl.disconnect()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n已中断")

说是"越权",其实产品设计时可能根本没考虑鉴权。

  • 微信小程序:某品牌智能设备控制小程序。
  • wux1an\wxapkg 微信小程序解包工具 + 3.9 版本 Windows 端微信。

  • GitHub Copilot 代码辅助分析 AI + VS Code 编辑器。

  • Wireshark 流量分析工具。

  • Sniffle 抓包环境

  • nRF Connect

  • 使用 3.9 版本 Windows 端微信打开目标小程序。

  • 使用 wxapkg 工具对微信小程序进行解包。

  • 微信小程序本质上还是前端 JavaScript 代码,所以这里不需要使用 IDA 等二进制分析工具,直接使用 VS Code 打开解包后的项目文件夹,对微信小程序的项目结构进行整体分析。
  • 发现该小程序解包后的项目结构还是较为清晰的,主要关注 PagepackageHairdrierpackageBrush 这几个文件夹以及主目录下的这些 .js 文件。

  • 通过文件名大致可以猜测 packageHairdrier 为该品牌吹风机控制的相关代码,packageBrush 为该品牌电动牙刷控制的相关代码。

  • 而在 Page 文件夹中,有如下页面,应该是用户登录、设备连接等通用功能代码。

  • 因为身边只有该品牌的电动牙刷,为了方便验证,这里对电动牙刷的相关代码优先进行分析,即 packageBrush 文件夹下的内容。
  • 由于解包逆向的缘故,pages 文件夹下的 .html 代码并未有关键的内容,只有对其他文件的引用,所以逆向分析的重心还是放在 packageBrush 下的 .js 文件。
  • .js 的代码量不算少,为了能快速定位小程序控制电动牙刷的关键代码,需要结合前端显示的文字定位关键代码。

  • 先逆向追踪 开关 这个按钮的功能。

  • 直接在 VS Code 编辑器中搜索 开关 关键字。

  • 因为 开关 按钮的开关文字是单独出现的,因此可以大致确定相关代码位于 .\packageBrush\chunk_7.appservice.js.\packageBrush\app-service.js 两个文件中。

  • 根据以往微信小程序解包的经验,app-service.js 一般包含所有分片代码,上述两个文件中所出现的 开关 相关代码实际是同一份代码。

  • 因此,接下来只需要对 .\packageBrush\chunk_7.appservice.js 相关代码进行逆向分析。

  • 发现 开关 位于 btnList 列表中,所以尝试去搜索这个列表的调用。
  • 发现 btnList 在这里被 F() 函数调用,根据经验猜测,F() 函数应该对应类似 for 循环的结构,对 btnList 的按钮进行逐个渲染,并且在 F() 的参数中,发现传入了 i 函数,推测它就是具体的渲染函数。

[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 20小时前 被Haicaji编辑 ,原因: 优化一下markdown格式, 把一些 - 节点改为 # 标题
收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回