Introduction

I recently purchased a bunch of garbage smart devices from Temu with intent to reverse engineer and hack some of them. The first to arrive was a Bluetooth smart scale. I paid $4 for this and was curious on what I could find.

Quicklinks

Viewing BLE Traffic with BTMON

I paired the smart scale with its companion mobile app, OKOK, available on the App Store. Within the app’s device settings, the scale’s Bluetooth MAC address is displayed. In my case, the device reported the following address: A8:0B:6B:F4:23:38

Here is an example BLE event from the provided mac address:

> HCI Event: LE Meta Event (0x3e) plen 50                  #30 [hci0] 19.133377  
     LE Extended Advertising Report (0x0d)  
       Num reports: 1  
       Entry 0  
         Event type: 0x0010  
           Props: 0x0010  
             Use legacy advertising PDUs  
           Data status: Complete  
         Legacy PDU Type: ADV_NONCONN_IND (0x0010)  
         Address type: Public (0x00)  
         Address: A8:0B:6B:F4:23:38 (Chipsea Technologies (Shenzhen) Corp.)  
         Primary PHY: LE 1M  
         Secondary PHY: No packets  
         SID: no ADI field (0xff)  
         TX power: 127 dBm  
         RSSI: -80 dBm (0xb0)  
         Periodic advertising interval: 0.00 msec (0x0000)  
         Direct address type: Public (0x00)  
         Direct address: 00:00:00:00:00:00 (OUI 00-00-00)  
         Data length: 0x18  
       10 ff c0 10 00 00 00 00 00 00 24 00 00 00 00 00  ..........$.....  
       00 06 09 59 6f 64 61 31                          ...Yoda1           
       Company: not assigned (4288)  
         Data[13]: 00000000000024000000000000  
       Name (complete): Yoda1  

Creating a Python Script to Read The Scales Bytes

Since we know the mac address and its broadcasting BLE events lets make a python script to read the raw bytes of each event.

from bleak import BleakScanner  
  
TARGET_MAC = "A8:0B:6B:F4:23:38"  
  
def detection_callback(device, advertisement_data):  
   if device.address.upper() != TARGET_MAC:  
       return  
  
   mfg = advertisement_data.manufacturer_data  
   if not mfg:  
       return  
  
   print("---- SCALE UPDATE ----")  
   for company_id, data in mfg.items():  
       print(f"Company ID: {company_id}")  
       print(f"Raw Data: {data.hex()}")  
  
async def main():  
   scanner = BleakScanner(detection_callback)  
   await scanner.start()  
   print("Listening for scale updates...")  
   await asyncio.sleep(60)   # listen for 1 minute  
   await scanner.stop()  
  
asyncio.run(main())

I then ran the script and stepped on the scale…

$ python3 bleakky.py  
Listening for scale updates...  
---- SCALE UPDATE ----  
Company ID: 5568  
Raw Data: 00000000000024000000000000  
---- SCALE UPDATE ----  
Company ID: 5568  
Raw Data: 09061388000025000000000000

Finding A “Stepped On” State

I then repeatedly stepped on the scale while monitoring the captured BLE advertisements, looking for patterns that correlated with user interaction. During this process, I observed that the following manufacturer payload appeared every time the scale was stepped on, regardless of the measured weight:

---- SCALE UPDATE ----
Company ID: 6848  
Raw Data: 00000000000024000000000000  

This payload suggests some type of state change. Signaling the scale has been stepped on.

To make this easier to detect programmatically, I defined the observed payload as a constant and added a conditional check in the BLE detection callback. When this specific frame is observed, the script prints a message indicating that the scale has been stepped on.

STEPPED_ON_HEX = "00000000000024000000000000"  

def detection_callback(device, advertisement_data):  
   if device.address.upper() != TARGET_MAC:  
       return  
  
   mfg = advertisement_data.manufacturer_data  
   if not mfg:  
       return  
  
   for company_id, data in mfg.items():  
       hex_data = data.hex()  
       if hex_data == STEPPED_ON_HEX:  
           print("[+] Scale has been stepped on")  
           continue  
  
       print(f"Company ID: {company_id}")  
       print(f"Hex Data: {data.hex()}")  

Converting the Bytes to KG

Now lets go ahead and move on to finding out which bytes represent the weight of the user.

[+] Scale has been stepped on  
Company ID: 8640  
Hex Data: 1cd91388000025000000000000  
Company ID: 8896  
Hex Data: 1bb21388000025000000000000  
Company ID: 9152  
Hex Data: 1b491388000025000000000000  
[+] Scale has been stepped on  
Company ID: 8640  
Hex Data: 1cd91388000025000000000000  
Company ID: 8896  
Hex Data: 1bb21388000025000000000000  
Company ID: 9152  
Hex Data: 1b491388000025000000000000  
Company ID: 9408  
Hex Data: 1bb21388000025000000000000  
Company ID: 8640  
Hex Data: 1cd91388000025000000000000  
Company ID: 8896  
Hex Data: 1bb21388000025000000000000  
Company ID: 9152  
Hex Data: 1b491388000025000000000000  
Company ID: 9408  
Hex Data: 1bb21388000025000000000000

By removing this constant tail from each payload, only the first two bytes remained as candidates for the actual weight value. These bytes changed slightly with each measurement and tracked consistently with changes in the displayed weight on the scale.

1bb21388000025000000000000
    1388000025000000000000
    
1bb2 == Weight

At this point, the problem space was significantly narrowed: the user’s weight appeared to be encoded entirely within the first two bytes of the manufacturer data.

I then tossed it over to ChatGPT to help me figure out what decoding method I should try. Thats when it provided the following line:

weight_raw = int.from_bytes(data[0:2], byteorder="big")   # 0x1be4 -> 7140

This would convert the following example:

1be41388000025000000000000

0x1be4 -> 7140 which divided by 100 is 71.40 which closely matches the actual weight displayed by the scale.

We can then make a nice print message to display the weight in pounds and kilograms.

def get_weight_from_bytes(data: bytes) -> tuple[float, float]:
	weight_raw = int.from_bytes(data, byteorder="big")
	weight_kg = weight_raw / 100.0
	weight_lb = weight_kg * 2.2046226218
	return weight_kg, weight_lb

def detection_callback(device, advertisement_data):
	if device.address.upper() != TARGET_MAC:
		return

	mfg = advertisement_data.manufacturer_data
	if not mfg:
		return

	for _, data in mfg.items():
		hex_data = data.hex()
		if hex_data == STEPPED_ON_HEX:
			print("[+] Scale has been stepped on")
			continue

		kg, lb = get_weight_from_bytes(data[0:2])
		print(f"[+] User weighed in at {kg}kgs {lb}lbs")

Running the script:

$ python3 ble_sniffer.py
[+] Scale has been stepped on
[+] User weighed in at 85.25 kg (187.94 lb)
[+] User weighed in at 85.25 kg (187.94 lb)
[+] Scale has been stepped on
[+] User weighed in at 85.25 kg (187.94 lb)
[+] Scale has been stepped on
[+] User weighed in at 85.25 kg (187.94 lb)

Adding BLE Scale Discovery

I then wanted to implement a small method to discover a scale on the network when stepped on. This can be done by simply looping through BLE traffic and if matches the STEPPED_ON hex value we can mark it as a scale.

    async def discover_scale(self, timeout: float = 10.0) -> str:
        """
        Scan using a callback until we see STEPPED_ON_HEX in manufacturer_data.
        Returns the MAC address of that device.
        """
        found = asyncio.Future()
        
                # Bleak scanner callback
        def cb(device, advertisement_data):
            # manufacturer_data is on advertisement_data (stable across Bleak versions)
            mfg = advertisement_data.manufacturer_data or {}
            for _, payload in mfg.items():
                if payload.hex() == STEPPED_ON_HEX and not found.done():
                    found.set_result(device.address)

        try:
            scanner = BleakScanner(cb)
        except BleakBluetoothNotAvailableError:
            exit("[-] Failed to find bluetooth adapter :(")

                # Function to fetch mac address from found list
        async def get_mac_address() -> str | None:
            try:
                await scanner.start()
                try:
                    return await asyncio.wait_for(found, timeout=timeout)
                except asyncio.TimeoutError:
                    return None
            finally:
                await scanner.stop()
                
                # DISCOVERY LOOP HAPPENS HERE!
        while True:
            try:
                mac_address = await get_mac_address()
                if not mac_address:
                    continue
                print(f"[+] Found scale with MAC Address: {mac_address}")
                return mac_address
            except (KeyboardInterrupt, asyncio.CancelledError):
                exit("[-] CTRL+C detected!")

Conclusion / Code Repository

This project served as a hands-on introduction to BLE discovery and passive advertisement analysis using btmon and bleak. The full source code is available on GitHub: https://github.com/Drew-Alleman/SmartScaleBluetoothSniffer