OSX: Cómo controlar dispositivos USB con Latch desde "userland"

viernes, 11 de julio de 2014

Desde hace varias versiones de OSX es posible tener un cierto grado de control de los dispositivos USB sin necesidad de instalar ningún driver en el kernel (en OSX se llaman extensiones). Vamos a aprovechar esto para crear un hack que permita controlar los dispositivos USB con Latch sin necesidad de programar drivers ni hookear funciones. En esta entrada se mostrará el código C desarrollado internamente para conseguirlo.

Este control del núcleo, al situarse en userland nos proporciona ciertas ventajas obvias:
  • No se necesitan permisos de administrador.
  • No es necesario instalar ningún controlador, por lo que en caso de algún fallo la aplicación creada simplemente se cerrará pero no afectará a la estabilidad del sistema operativo.
Aunque en realidad sí que se está utilizando un driver en el kernel, el propio sistema operativo permite la abstracción al ofrecer una instancia de IOUSBDevice e IOUSBInterface en userland que permite interactuar con el hardware a través de una API llamada IOUSBLib.

Generalmente los USBs no están continuamente conectados a una máquina por lo que la propia API ofrece un servicio de notificaciones que avisa cuando se conecta o desconecta un dispositivo USB para poder realizar las acciones que se deseen. Ahí es donde entra Latch; si es posible detectar cuándo se conecta o desconecta un dispositivo USB, es posible crear un sistema de control de puertos USB, donde dependiendo del estado del latch configurado, se podrá permitir el uso del dispositivo en el equipo, bloquear todos o ninguno o incluso filtrar por tipo de dispositivo o dispositivo concreto.

Más allá, gracias a la versatilidad de latch sería posible filtrar por cualquier tipo de dispositivo e incluso crear diferentes latches donde por ejemplo con uno se controle la webcam y con otro el resto de dispositivos USB. O quizás permitir exclusivamente el disco USB concreto donde se realicen las copias de respaldo, pero ninguno más porque generalmente para poder identificar cualquier dispositivo USB se suele utilizar su vendorIDproductID y serial number, que en principio (aunque luego no es siempre así) son únicos. Las posibilidades son infinitas.

Es el momento de mostrar el código para entender cómo funciona, en 10 sencillos pasos...

Paso 1: Establecer la comunicación

Lo primero es crear un puerto especial para establecer la comunicación entre el kernel (I/O Kit) y la aplicación de usuario. Además es necesario configurar un bucle que esté esperando a que lleguen las notificaciones de cambios a las que será necesario registrarse más adelante:

Paso 2: Registrar las notificaciones

A continuación se deben registrar las notificaciones a las que "nos vamos a suscribir". Para ello, primero es necesario indicar qué tipo de dispositivos USB interesa. En este caso kIOUSBDeviceClassName indica que interesan todos los dispositivos USB, pero se podría indicar solamente los dispositivos de audio, de vídeo, de almacenamiento masivo, o incluso alguno en concreto.

Paso 3: Suscribirse a las notificaciones (si se conecta)

Ahora ya es posible suscribirse a las notificaciones. Primero nos suscribimos a cuándo se conecta un dispositivo USB (kIOMatchedNotification) e indicamos cuál es el callback que llamará la notificación; en este caso usb_device_added.

Paso 4: Suscribirse a las notificaciones (si se desconecta) 

Se hace lo mismo para suscribirse a las notificaciones cuando un dispositivo USB es desconectado (kIOTerminatedNotification), en este caso llamando al callback usb_device_removed.

Paso 5: Obtener la información del dispositivo

Ambos callbacks (usb_device_added y usb_device_removed) permiten realizar las operaciones que se deseen después de estas notificaciones. En este caso solamente se utilizará el primero (cuando un dispositivo USB se conecta) para poder comprobar el estado de un latch concreto que se haya configurado. Para ello primero será necesario obtener información sobre el dispositivo USB concreto; cuando llega la notificación realmente llega un objeto io_iterator_t que se debe recorrer para obtener información sobre el dispositivo USB en cuestión. En este caso se va a obtener la información del dispositivo con la función propia get_usb_device_info:

Paso 6: Recoger los datos concretos del dispositivo

Esta función obtiene información sobre el dispositivo USB. Para ello, sería necesario obtener toda la información posible:

Paso 7: Preguntar a Latch

Una vez que se han obtenido todos los datos relacionados con el dispositivo USB es cuando se debe preguntar a Latch por el estado del pestillo correspondiente para ver si se deja conectar este dispositivo o no y poder realizar la operación oportuna. La solución no es perfecta puesto que simplemente se están recibiendo notificaciones del sistema operativo pero no se posiciona en medio del proceso de conexión de un dispositivo USB, con lo que siempre va a existir una pequeña ventana (de milisegundos) entre que el dispositivo se conecta, y la operación que queramos realizar. Estas operaciones dependiendo del tipo de dispositivo pueden ser de diferente índole: por ejemplo en el caso de un disco duro USB es posible explusarlo nada más que se conecta, con lo que no se permitiría conectar el dispositivo, o en el caso de una webcam u otro USB se puede suspender el dispositivo USB, con lo que tampoco se permitiría que funcionara correctamente.

Paso 8. Expulsar el dispositivo

En el primero caso, en el del disco duro USB, es posible esperar hasta que el sistema operativo reconoce el disco duro, y una vez que lo intenta montar, ejecutar el eject para desmontarlo ipso facto. Para ello se debe saber qué dispositivo virtual ha asignado el sistema operativo al disco duro USB (generalmente disk1); esto se consigue obteniendo la propiedad kIOBSDNameKey:

Paso 9: Suspender el dispositivo

En el caso de otro tipo de dispositivo, cabe la posibilidad de suspender el dispositivo USB donde se encuentra para que no funcione. Como dato relevante, destacar que realizando pruebas con diferentes dispositivos el resultado ha resultado a veces extraño, puesto que dispositivos como una webcam sí que dejaban de funcionar, pero otros dispositivos intentaban conectarse continuamente aunque se les obligara a suspenderse. Como curiosidad podemos comentar que tanto la webcam como el teclado de los MacBook están conectados por medio de USB y por tanto es posible controlarlos como cualquier otro dispositivo USB (aunque su desconexión física es más complicada):

Paso 10: Configuración con ficheros plist

Como se ha comprobado, es posible detectar la conexión de dispositivos USB desde userland de forma rápida pero también se observa que no es la solución perfecta, puesto que no se posiciona en el medio del proceso USB y por tanto se reacciona a las notificaciones que llegan desde el kernel (I/O Kit). OSX permite realizar también la misma funcionalidad de forma más limpian utilizando XPC. Se trata de una especie de IPC (InterProcess Communication) muy poco conocida que introdujo OSX desde la versión 10.8 y permite a los procesos que se ejecutan con launchd suscribirse a estas notificaciones de I/O Kit simplemente indicándolo en su fichero plist de configuración como se ve en el ejemplo siguiente, con el que se recibirían notificaciones de conexión del dispositivo USB con vendorId 725 y productId 2794:




En definitiva, gracias a las posibilidades de Latch, y a las notificaciones que ofrece OSX en userland, se puede conseguir una forma sencilla de controlar todos los dispositivos USB del equipo, disfrutando de un mayor control de nuestra máquina y evitando por ejemplo la infección de malware o el robo de datos mediante estos dispositivos. Es una opción adecuada para las personas que no deseen  instalar ningún driver en el kernel o que no disponen de permisos de administrador en su equipo.

Para una próxima entrada explicaremos cómo realizar un mejor control de estos dispositivos, pero en este caso mediante un driver en el kernel (extensión).

David Barroso
david.barroso@11paths.com

3 comentarios:

  1. Como hago para que esto funcione? Tendría que poner en Xcode el código y luego ejecutarlo? Es que no soy un experto en el tema. Muchas Gracias

    ResponderEliminar
  2. Hola!
    Sí, efectivamente tienes que tener Xcode instalado para poder compilarlo y así poder ejecutarlo.

    ResponderEliminar
  3. No me funciona.. Lo e copiado en xcode y al ejecutarlo pone build failed.

    ResponderEliminar