Post

Ingeniería Inversa a la Mi Band 4. Descifrando su Protocolo BLE para Tomar el Control de los Sensores.

Los dispositivos IoT, como las pulseras de actividad, son cajas negras para la mayoría de los usuarios. Funcionan, pero ¿cómo? ¿Qué secretos guardan sus protocolos de comunicación? A veces, para construir, primero hay que deconstruir.

Este post es un caso práctico de ingeniería inversa. Vamos a darle una segunda vida a una Mi Band 4, no solo para usarla, sino para entenderla.

Nuestro objetivo final: interceptar y utilizar los datos de sus sensores directamente desde un PC con Linux, sentando las bases para crear una herramienta de control por gestos.

Para ello, nos sumergiremos en el protocolo Bluetooth Low Energy (BLE), descifraremos su mecanismo de autenticación y escribiremos un script en Python que nos dará el control.

Entendiendo BLE y el Protocolo GATT


Antes de escribir una sola línea de código, debemos entender el lenguaje en el que hablan los dispositivos IoT. La Mi Band 4 utiliza Bluetooth Low Energy (BLE), un protocolo diseñado para la comunicación de bajo ancho de banda y consumo energético mínimo.

En una conexión BLE, tenemos dos roles:

  • Central (Nuestro PC): El dispositivo que inicia la conexión y solicita los datos.
  • Periférico (La Mi Band 4): El dispositivo que ofrece los datos y espera conexiones.

La comunicación de datos sobre BLE no es un flujo caótico, está estructurada por un reglamento llamado GATT (Generic Attribute Profile). La mejor forma de entender GATT es imaginarlo como un sistema de archivos simple:

  • Servicios (Carpetas): Un Servicio agrupa funcionalidades relacionadas. Por ejemplo, la Mi Band tiene un “Servicio de Batería”, un “Servicio de Ritmo Cardíaco”, etc. Cada servicio se identifica por un UUID (Universally Unique Identifier), que es su dirección única.
  • Características (Archivos): Dentro de cada Servicio, hay una o más Características. Estas contienen los datos reales. El “Servicio de Batería” (UUID 0x180F) contiene una “Característica de Nivel de Batería” (UUID 0x2A19), cuyo valor es un byte que representa el porcentaje de 0 a 100.

Nuestro objetivo es, por tanto, navegar por este “sistema de archivos” GATT para leer el “archivo” que contiene el dato que nos interesa. Pero antes, necesitamos permiso para acceder.

La Auth Key y el Proceso de Autenticación


Aquí es donde entra en juego la ingeniería inversa. Los fabricantes no quieren que cualquiera pueda conectarse y leer datos sensibles (como tu ritmo cardíaco). Para evitarlo, la Mi Band 4 implementa un mecanismo de autenticación que depende de una Clave de Autenticación (Auth Key)

¿Qué es y cómo funciona la Auth Key?

La Auth Key es un secreto compartido de 16 bytes (32 caracteres hexadecimales) que se genera durante el primer emparejamiento entre la pulsera y la aplicación oficial (ej. Zepp Life). Una vez generada, se almacena tanto en el teléfono como en la pulsera.

💡 Tip: A pesar de almacenarse en el teléfono, no podemos acceder a ella si no tenemos un teléfono rooteado.

El proceso de autenticación en cada nueva conexión funciona así:

  1. Conexión Inicial: Nuestro script (el Central) establece una conexión BLE básica con la pulsera (el Periférico).
  2. El Desafío: La pulsera bloquea el acceso a la mayoría de sus servicios GATT y nos envía un “desafío”: un número aleatorio.
  3. La Respuesta: Nuestro script debe tomar ese número aleatorio y cifrarlo utilizando un algoritmo (normalmente AES/ECB) con la Auth Key como clave secreta. El resultado cifrado se envía de vuelta a la pulsera a través de una característica específica de autenticación.
  4. Verificación: La pulsera realiza exactamente la misma operación de cifrado en su lado con su copia de la Auth Key.
  5. Acceso Concedido: Si su resultado coincide con el que le hemos enviado, la pulsera considera que somos un dispositivo de confianza y “desbloquea” el acceso a los servicios protegidos.

Sin esta clave, cualquier intento de leer datos sensibles será rechazado. Nuestro script, por tanto, debe “suplantar” a la aplicación oficial presentando la clave correcta.

¿Cómo Obtenemos la Clave? (El “Hackeo”)

Aquí es donde aplicamos técnicas de ingeniería inversa para extraer la clave del ecosistema cerrado de la app:

  • Análisis de la Base de Datos (Requiere Root): En un dispositivo Android rooteado, la app oficial almacena la Auth Key en una base de datos SQLite local, sin cifrar. Es posible acceder al sistema de archivos del teléfono y extraerla directamente.
  • Aplicaciones Modificadas: La comunidad ha descompilado la app oficial, inyectado código para que escriba la Auth Key en un archivo de texto en un directorio accesible, y la ha vuelto a compilar. Usar estas apps es el método más común, pero conlleva un riesgo de seguridad, ya que confías en código de terceros.
  • Sniffing de Tráfico BLE (Avanzado): Sería posible capturar el intercambio de la clave durante el emparejamiento inicial. Sin embargo, los protocolos de emparejamiento modernos como LE Secure Connections utilizan criptografía de clave pública para proteger este intercambio, haciendo este método muy complejo.

Conexión y Script


Ahora que entendemos el funcionamiento de nuestra pulsera, vamos a la práctica.

Preparando el Entorno en Ubuntu

Instalamos las dependencias para la comunicación BLE en Python:

1
2
3
sudo apt-get update
sudo apt-get install libglib2.0-dev
pip install bleak cryptography

Reconocimiento: Encontrando la Dirección MAC

Necesitamos la dirección del objetivo. Con el Bluetooth de tu móvil desactivado para que la pulsera sea visible, ejecutamos un escaneo:

1
sudo hcitool lescan

Busca la línea de Mi Smart Band 4 y anota su dirección MAC (XX:XX:XX:XX:XX:XX).

PoC: El Script para Leer la Batería

Este script será nuestra prueba de concepto (Proof of Concept). Demostrará que hemos logrado la autenticación y podemos leer datos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import asyncio
import logging
import struct
from bleak import BleakClient, BleakScanner
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

# --- CONFIGURACIÓN ---
# REEMPLAZA ESTO CON TUS DATOS REALES
MAC_ADDRESS = "XX:XX:XX:XX:XX:XX" 
AUTH_KEY_HEX = "8f3a2b..." # Tu clave de 32 caracteres hex (ej. '8f3a2b4c...')

# --- UUIDs DE LA MI BAND 4 ---
# UUIDs Base de Huami
UUID_SERVICE_MIBAND2 = "0000fee1-0000-1000-8000-00805f9b34fb"
UUID_CHAR_AUTH = "00000009-0000-3512-2118-0009af100700"

UUID_SERVICE_MIBAND1 = "0000fee0-0000-1000-8000-00805f9b34fb"
# Característica específica de Xiaomi para info detallada de batería (no la estándar 0x2a19)
UUID_CHAR_BATTERY = "00000006-0000-3512-2118-0009af100700"

# --- CONSTANTES DEL PROTOCOLO ---
AUTH_SEND_KEY = b'\x01\x00'
AUTH_REQUEST_RANDOM_AUTH_NUMBER = b'\x02\x00'
AUTH_SEND_ENCRYPTED_AUTH_NUMBER = b'\x03\x00'
AUTH_RESPONSE = b'\x10'
AUTH_SUCCESS = b'\x01'
AUTH_FAIL = b'\x04'

# Configuración de Logs
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

class MiBandMiddleware:
    def __init__(self, mac, auth_key):
        self.mac = mac
        self.auth_key = bytes.fromhex(auth_key)
        self.client = None
        self.auth_event = asyncio.Event() # Para esperar respuesta de la pulsera
        self.auth_success = False

    def encrypt_aes(self, data):
        """Cifra los datos usando AES ECB con la Auth Key."""
        cipher = Cipher(algorithms.AES(self.auth_key), modes.ECB(), backend=default_backend())
        encryptor = cipher.encryptor()
        return encryptor.update(data) + encryptor.finalize()

    async def notification_handler(self, sender, data):
        """Maneja las notificaciones de la pulsera (Callback)."""
        # La pulsera responde con prefijo \x10
        if data.startswith(AUTH_RESPONSE):
            # UUID_CHAR_AUTH response
            cmd_id = data[1]
            status = data[2]
            
            if cmd_id == 2 and status == 1: 
                # Respuesta a Solicitud de Aleatorio (0x02) -> Nos da el número random
                logger.info("Recibido número aleatorio de la pulsera.")
                random_number = data[3:]
                
                # Ciframos el número y lo enviamos de vuelta
                encrypted_number = self.encrypt_aes(random_number)
                payload = AUTH_SEND_ENCRYPTED_AUTH_NUMBER + encrypted_number
                logger.info("Enviando desafío cifrado...")
                await self.client.write_gatt_char(UUID_CHAR_AUTH, payload)
                
            elif cmd_id == 3 and status == 1:
                # Respuesta a Envío Cifrado (0x03) -> Éxito
                logger.info("¡Autenticación Exitosa!")
                self.auth_success = True
                self.auth_event.set()
                
            elif status == 4:
                logger.error("Error de autenticación: Clave incorrecta o error en el handshake.")
                self.auth_success = False
                self.auth_event.set()

    async def authenticate(self):
        """Realiza el flujo de autenticación."""
        logger.info("Iniciando autenticación...")
        
        # 1. Suscribirse a notificaciones de autenticación
        await self.client.start_notify(UUID_CHAR_AUTH, self.notification_handler)
        
        # 2. Solicitar número aleatorio
        # Nota: Si es la primera vez absoluta, se enviaría AUTH_SEND_KEY, 
        # pero asumimos que ya tienes la KEY y la pulsera está 'pairada' lógicamente.
        logger.info("Solicitando número aleatorio...")
        await self.client.write_gatt_char(UUID_CHAR_AUTH, AUTH_REQUEST_RANDOM_AUTH_NUMBER)
        
        # 3. Esperar a que el handler procese la lógica
        try:
            await asyncio.wait_for(self.auth_event.wait(), timeout=10.0)
        except asyncio.TimeoutError:
            logger.error("Tiempo de espera de autenticación agotado.")
            return False
            
        return self.auth_success

    async def read_battery(self):
        """Lee el estado de la batería una vez autenticado."""
        if not self.auth_success:
            logger.error("No se puede leer batería sin autenticación.")
            return

        logger.info("Leyendo datos de batería...")
        # Leemos la característica propietaria 0x0006
        battery_data = await self.client.read_gatt_char(UUID_CHAR_BATTERY)
        
        # Parseo de datos para Mi Band 4 (Firmware v1.0.9.x)
        # Estructura usual: [Nivel, Estado, UltimaCarga?, ...]
        level = battery_data[1]
        status_byte = battery_data[2]
        
        status_str = "Desconocido"
        if status_byte == 0: status_str = "Normal"
        elif status_byte == 1: status_str = "Cargando"
        
        logger.info(f"--- ESTADO BATERÍA ---")
        logger.info(f"Nivel: {level}%")
        logger.info(f"Estado: {status_str}")
        logger.info(f"Raw Data: {battery_data.hex()}")

    async def run(self):
        logger.info(f"Conectando a {self.mac}...")
        
        async with BleakClient(self.mac) as client:
            self.client = client
            if client.is_connected:
                logger.info("Conectado. Verificando servicios...")
                
                # Paso 1: Autenticar
                if await self.authenticate():
                    # Paso 2: Leer Sensores/Batería
                    await self.read_battery()
                    
                    # Aquí podrías añadir bucles para leer acelerómetro, ritmo cardíaco, etc.
                    # await asyncio.sleep(5) 
                else:
                    logger.error("Falló la autenticación. Desconectando.")

if __name__ == "__main__":
    # Asegúrate de poner tu clave REAL aquí
    if "XX" in MAC_ADDRESS:
        print("ERROR: Edita el script y pon tu MAC Address y Auth Key.")
    else:
        middleware = MiBandMiddleware(MAC_ADDRESS, AUTH_KEY_HEX)
        asyncio.run(middleware.run())

Ejecución

Ejecutamos el script: python3 script.py

Si ves el nivel de la batería en tu terminal, ¡lo has conseguido! Has realizado con éxito una conexión autenticada a un dispositivo BLE, has superado su seguridad y has extraído datos.

💡 Tip: La salida debería ser similar a la siguiente:

INFO - Conectando a XX:XX:XX:XX:XX:XX...
INFO - Conectado. Verificando servicios...
INFO - Iniciando autenticación...
INFO - Solicitando número aleatorio...
INFO - Recibido número aleatorio de la pulsera.
INFO - Enviando desafío cifrado...
INFO - ¡Autenticación Exitosa!
INFO - Leyendo datos de batería...
INFO - --- ESTADO BATERÍA ---
INFO - Nivel: 77%
INFO - Estado: Normal
INFO - Raw Data: 0f4d00e9070c09001b2a04e9070c09002d390464


Próximos Pasos


Este es solo el primer paso. Con la autenticación resuelta, el camino está libre para acceder a sensores más interesantes como el acelerómetro y el giroscopio, que son la base para nuestro futuro proyecto. Has convertido una caja negra en una herramienta de código abierto

H4Ppy H4ck1ng!

This post is licensed under CC BY 4.0 by the author.