Post-Image

Analizando y hackeando el OBD en Ford

En este artículo exploraremos un proyecto de hacking automotriz en el que controlamos el ventilador de alta velocidad (HFC) de un Ford en función de la temperatura del refrigerante. Para ello, utilizamos Python, un adaptador OBD-II ELM327 y el análisis de paquetes con Wireshark.

Este proyecto me permitió aprender sobre comunicación CAN, protocolos automotrices y cómo combinar herramientas de red y programación para resolver problemas prácticos.

El objetivo inicial era entender el mecanismo de activación y desactivación del ventilador de alta velocidad. Investigando en la red, descubrí que el PCM (Powertrain Control Module) utiliza el protocolo J2190 sobre CAN a 500 kbps.

El Unified Diagnostic Services (UDS) es un protocolo utilizado en diagnósticos automotrices que permite la comunicación con las ECUs. Dentro de UDS, existen diferentes modos que permiten leer datos, ejecutar pruebas y enviar comandos.

Modo 22 – Lectura de Datos Específicos (Read Data by Identifier):
Se usa para consultar parámetros en tiempo real del vehículo, como la temperatura del refrigerante o la velocidad del motor. Ford emplea un esquema de PIDs extendidos, lo que significa que algunos identificadores pueden diferir del estándar.

Modo 2F – Control de Rutinas (Routine Control):
A diferencia del modo 22, que solo permite la lectura de datos, el modo 2F se usa para ejecutar rutinas específicas dentro de la ECU.

Dado que Ford utiliza identificadores de datos específicos y PIDs extendidos, es fundamental analizar paquetes CAN o usar herramientas avanzadas de diagnóstico para comprender y manipular correctamente los comandos que controlan los distintos sistemas del vehículo.

Recurrimos a FORScan, una aplicación especializada en diagnósticos avanzados para vehículos Ford, que resultó invaluable para nuestro proyecto. Con FORScan conectado al coche a través del adaptador ELM327, pudimos explorar los módulos del PCM y confirmar los PIDs (Parameter IDs) soportados.

Para obtener los comandos exactos, recurrimos a Wireshark, una herramienta poderosa para analizar tráfico de red, adaptada en este caso para capturar paquetes Bluetooth.

Inicialmente, capturamos un mensaje que parecía ser el comando para encender el HFC:

Forscan and Wireshark capture to power on HFC

Payload: 00011b00170041000bff250132463039394230333030343030303430310d86
  1. Encabezado L2CAP:
  • 1b 00 (Longitud: 27 bytes).
  • 10 00 37 d2 84 ae ff ff 00 00 00 00 09 00 (Encabezado Bluetooth/L2CAP, probablemente dirección y canal).
  • 00 01 00 01 00 02 03 1f 00 00 00 00 01 (Configuración L2CAP).
  • 1b 00 17 (Longitud del canal: 23 bytes).
  • 00 41 (Canal L2CAP: 0x0041, usado para enviar comandos al PCM).
  1. Comando:
0bff250132463039394230333030343030303430310d86
  • 0b ff 25 01: Encabezado o metadatos L2CAP/RFCOMM.
  • 32 46 30 39 39 42 30 33 30 30 34 30 30 30 34 30 31: Esto es el comando en ASCII: 2F099B03004000401.
    • 2F → Prefijo (posiblemente parte del ID CAN o Service 2F de UDS).
    • 099B → PID o identificador CAN relacionado con el ventilador (coincide con el PID 22099B1 usado para leer el estado).
    • 03 → Posiblemente un byte de control o modo.
    • 00 40 00 40 01 → Datos específicos del comando (diferente de 04004001 en nuestro payload actual).
  • 0d (\r): Terminador.
  • 86: Posible checksum.

El payload 2F099B03004000401 parece ser el comando correcto para activar el ventilador (HFC ON), basado en la captura.

Forscan and Wireshark response for HFC

Obtenemos una respuesta a este comando:

Payload: 002117001300400009ef1f3646303939423033303141300d0d3e40
  1. Encabezado L2CAP:
  • 00 21: Longitud del mensaje (33 bytes).
  • 17 00 13 00 40 00: Configuración L2CAP, canal 0x0040 (respuesta del PCM).
  • 09 ef 1f: Metadatos adicionales.
  1. Respuesta:
3646303939423033303141300d0d3e40
  • 36 466F (respuesta al modo 2F de UDS).
  • 30 39 39 42099B (identificador CAN/PID del ventilador).
  • 30 33 30 31 41 30031A0 (datos de la respuesta).
  • 0d 0d\r\r (retornos de carro).
  • 3e 40>@@ (ojalá saberlo).

Analizando toda la información recopilada, si deseamos validar esta teoría sobre un componente inofensivo del vehículo, podríamos ejecutar el siguiente código, diseñado para monitorear la temperatura del líquido refrigerante mediante un protocolo específico de Ford que utiliza el modo 22. Este código consultará el PID 22F4051 para obtener los datos correspondientes.

En primer lugar vamos a conectarnos al dispositivo bluetooth y vincularlo con un puerto serial.

Inicia el Bluetooth:

1
  bluetoothctl

Dentro de bluetoothctl, ejecuta:

1
2
  power on
  scan on

Busca tu adaptador OBD-II (puede aparecer como “OBDII”, “ELM327”, o similar). Anota su dirección MAC (formato XX:XX:XX:XX:XX:XX).

Emparejar el dispositivo:

1
  pair XX:XX:XX:XX:XX:XX

Si pide un PIN, el predeterminado suele ser 1234 o 0000. Luego:

1
  trust XX:XX:XX:XX:XX:XX

Vincular al puerto serial: Sal de bluetoothctl (exit) y vincula el dispositivo a /dev/rfcomm0:

1
  sudo rfcomm bind 0 XX:XX:XX:XX:XX:XX 1

Verificar: Comprueba que el puerto existe:

1
  ls /dev/rfcomm*

Deberías ver /dev/rfcomm0.

NOTA Si reinicias el programa y el puerto está ocupado, libera /dev/rfcomm0 manualmente: sudo rfcomm release 0

 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
import serial
import time
import re

# Configuración
BLUETOOTH_PORT = "/dev/rfcomm0"  # Ajusta en Windows a COMX
BAUDRATE = 38400

# Inicializar conexión serial y ELM327
def initialize_obd():
    ser = serial.Serial(BLUETOOTH_PORT, baudrate=BAUDRATE, timeout=5)  # Timeout optimizado
    ser.write(b'ATZ\r'); time.sleep(1); print(f"Inicialización: {ser.read_all().decode(errors='ignore')}")
    ser.write(b'ATE0\r'); time.sleep(1); print(f"Eco desactivado: {ser.read_all().decode(errors='ignore')}")
    ser.write(b'ATSP6\r'); time.sleep(1); print(f"Protocolo CAN 11/500: {ser.read_all().decode(errors='ignore')}")
    ser.write(b'ATRV\r'); time.sleep(1); print(f"Voltaje: {ser.read_all().decode(errors='ignore')}")
    return ser

# Leer temperatura del refrigerante
def get_coolant_temp(ser):
    try:
        if not ser.is_open:
            print("⚠️ Conexión OBD-II cerrada.")
            return None
        ser.write(b'22F4051\r')
        print("Comando enviado: 22F4051")
        time.sleep(1)
        response = ser.read_all().decode('utf-8', errors='ignore').strip()
        print(f"Respuesta cruda: {response}")

        # Parsear la respuesta: esperamos "62 F4 05 XX ..." (modo 22, PID F405, datos)
        match = re.findall(r"62 F4 05 ([0-9A-Fa-f]+)", response)
        if match:
            temp_hex = match[0]
            # Convertir el valor hexadecimal a decimal
            temp_value = int(temp_hex, 16)
            print(f"Valor crudo (decimal): {temp_value}")
            # Fórmula de conversión: suponemos inicialmente valor - 40 (como 0105)
            temp = temp_value - 40
            print(f"Temperatura calculada: {temp}°C")
            return temp
        else:
            print(f"Respuesta inesperada: {response}")
            return None
    except Exception as e:
        print(f"Error en get_coolant_temp: {e}")
        return None

# Bucle principal
def main():
    ser = initialize_obd()

    try:
        while True:
            temp = get_coolant_temp(ser)
            if temp is not None:
                print(f"Temperatura actual: {temp}°C")
            else:
                print("No se pudo leer la temperatura, reintentando...")
            time.sleep(10)
    except KeyboardInterrupt:
        print("\nPrograma terminado por el usuario")
    finally:
        ser.close()
        print("Conexión serial cerrada")

if __name__ == "__main__":
    main()