El equipo de McAfee Labs Advanced Threat Research está comprometido a descubrir problemas de seguridad tanto en software como en hardware para ayudar a los desarrolladores a proporcionar productos más seguros para empresas y consumidores. Recientemente investigamos un sistema de control industrial (ICS) producido por Delta Controls. El producto, llamado "enteliBUS Manager", se utiliza para varias aplicaciones, incluida la gestión de edificios. Nuestra investigación sobre el controlador Delta condujo al descubrimiento de un desbordamiento de búfer no reportado en la biblioteca "main.so". Esta falla, identificada por CVE-2019-9569, finalmente permite la ejecución remota de código, que podría ser utilizada por un atacante malicioso para manipular el control de acceso, salas de presión, HVAC y más. Reportamos esta investigación a Delta Controls el 7 de diciembreth, 2018. Dentro de unas pocas semanas, Delta respondió, y comenzamos un diálogo continuo mientras se creaba, probaba y desplegaba una solución de seguridad a fines de junio de 2019. Felicitamos a Delta por sus esfuerzos y asociación durante todo el proceso.
La versión de firmware vulnerable probada por el equipo de Advanced Threat Research de McAfee es 3.40.571848. Es probable que las versiones anteriores del firmware también sean vulnerables, sin embargo, ATR no las ha probado específicamente. Hemos confirmado que la versión de firmware parcheada 3.40.612850 remedia efectivamente la vulnerabilidad.
Este blog está destinado a proporcionar un análisis técnico profundo y exhaustivo de la vulnerabilidad y su impacto potencial. Para un recorrido no técnico de alto nivel de esta vulnerabilidad, consulte nuestra publicación de resumen del blog aquí.
Explorando la superficie de ataque
La primera tarea cuando se investiga un nuevo dispositivo es comprender cómo funciona desde una perspectiva de software y hardware. Al igual que muchos dispositivos en el ámbito de ICS, este dispositivo tiene tres componentes principales de software; el gestor de arranque, las aplicaciones del sistema y la programación definida por el usuario. Si bien buscar software en un vector de ataque es importante, no nos enfocamos en ninguna superficie definida por los usuarios, ya que esto podría cambiar para cada instalación. Por lo tanto, queremos centrarnos en el gestor de arranque y las aplicaciones del sistema. Con el sistema operativo, es común que los fabricantes implementen código personalizado para operar el dispositivo independientemente de la programación de un usuario individual. Este código personalizado es a menudo donde existen la mayoría de las vulnerabilidades y se extiende a toda la base de instalación del producto. Sin embargo, ¿cómo accedemos a este código? Como se trata de un sistema crítico, el firmware y el software no están disponibles públicamente y la documentación es limitada. Por lo tanto, estamos limitados al reconocimiento externo del software del sistema subyacente. Dado que las vulnerabilidades más críticas son remotas, tenía sentido comenzar con un simple escaneo de red del dispositivo. Un escaneo TCP no mostró puertos abiertos y un escaneo UDP solo mostró que los puertos 47808 y 47809 estaban abiertos. Refiriéndonos a la documentación, determinamos que es muy probable que se use para un protocolo llamado Building Automation Control Network (BACnet). Usando un script de enumeración de red específico de BACnet, determinamos un poco más de información:
root @ kali: ~ # nmap –script bacnet-info -sU -p 47808 192.168.7.15
A partir de Nmap 7.60 ( https://nmap.org ) a 2018-10-01 11:03 EDT
Informe de escaneo de Nmap para 192.168.7.15
El host está activo (latencia de 0.00032s).
SERVICIO DEL ESTADO DEL PUERTO
47808 / udp bacnet abierto
El | bacnet-info:
El | Identificación del vendedor: Delta Controls (8)
El | Nombre del vendedor: Delta Controls
El | Identificador de objeto: 29000
El | Firmware: 571848
El | Software de aplicación: V3.40
El | Nombre del Modelo: eBMGR-TCH
La siguiente pregunta es, ¿qué podemos aprender del hardware? Para responder a esta pregunta, primero se desarmó cuidadosamente el dispositivo, como se muestra en la Figura 1.
Figura 1
El controlador tiene una placa para administrar la pantalla y una placa base principal que contiene un chip System on a Module (SOM) que contiene tanto el procesador como los módulos flash. Con una mirada más cercana a la placa base, hicimos algunas observaciones clave. Primero, el procesador es un procesador central ARM926EJ, el módulo flash es un chip de matriz de rejilla esférica (BGA) y hay varios encabezados despoblados en el tablero.
Figura 2
Para examinar el software de manera más efectiva, necesitábamos determinar un método para extraer el firmware. El chip BGA utilizado por el sistema para la memoria flash probablemente contenga el firmware; Sin embargo, esto plantea otro desafío. A diferencia de otros chips, los chips BGA no proporcionan pines externos a los que se pueden conectar. Esto significa que para acceder al chip directamente, necesitaríamos desoldar el chip de la placa. Esto no es ideal ya que corremos el riesgo de dañar el sistema.
También notamos varios encabezados despoblados en el tablero. Esto fue prometedor ya que pudimos encontrar un método alternativo para exigir el firmware utilizando uno de estos encabezados. Soldando pines a cada uno de los encabezados despoblados y utilizando un analizador lógico, determinamos que el encabezado de 4 pines en el centro de la placa es un encabezado receptor-transmisor asíncrono universal (UART) que funciona a una velocidad en baudios de 115200.
figura 3
Usando el tablero de Exodus XI Breakout (grita a @Logan_Brown y el éxodo equipo) para conectarnos a los encabezados UART, nos encontramos con un indicador raíz desprotegido en el sistema. Ahora con acceso completo al sistema, podríamos comenzar a obtener una comprensión más profunda de cómo funciona el sistema y extraer el firmware.
Figura 4
Extracción de firmware y análisis del sistema
Con la interfaz UART, ahora podríamos explorar el sistema en tiempo real, pero ¿cómo podríamos extraer el firmware para el análisis fuera de línea? El dispositivo tiene dos puertos USB que pudimos usar para montar una unidad USB. Esto nos permitió copiar lo que se está ejecutando en la memoria usando dd en una unidad flash, extrayendo efectivamente el firmware. La siguiente pregunta fue: qué ¿copiamos?
Usando "/ proc / mtd" para obtener información sobre cómo se divide la memoria, podríamos ver los sistemas de archivos ubicados en mtd4 y mtd5. Nosotros usamos dd para copiar las particiones mtd4 y mtd5. Más tarde descubrimos que una de las imágenes es una copia de seguridad utilizada como un retroceso del sistema si se detecta un problema persistente. Este sistema de archivos copiado se volvió cada vez más útil a medida que el proyecto continuaba
Con la conexión UART activa, ahora era posible investigar más sobre cómo funciona el sistema. Como pudimos determinar previamente que el dispositivo solo está escuchando en los puertos 47808 y 47809, cualquier aplicación que escuche en estos puertos sería el único punto de ataque para un exploit remoto. Esto se confirmó rápidamente usando "netstat -nap" de la consola UART.
Notamos que el puerto 47808 estaba siendo usado por una aplicación llamada "dactetra". Con una investigación adicional mínima, se determinó que este es un binario específico del controlador Delta responsable de las funciones principales del dispositivo.
Encontrar una vulnerabilidad
Con una escucha binaria específica del dispositivo en la red a través de un puerto abierto, teníamos un lugar ideal para comenzar a buscar una vulnerabilidad. Utilizamos el enfoque común de fuzzing de red para comenzar nuestra investigación. Para implementar fuzzing de red para BACnet, recurrimos a una herramienta producida por Synopsys llamada Defensics, que tiene un módulo diseñado para servidores BACnet. Aunque este dispositivo no es un servidor BACnet y funciona más como un enrutador, este conjunto de pruebas proporcionó varios casos de prueba universales que nos dieron un excelente lugar para comenzar. BACnet utiliza varios tipos de paquetes de difusión para comunicarse. Dos de estos paquetes de transmisión, "Who-Is" y "I-Am", son universales para todos los dispositivos BACnet y Defensics proporciona módulos para trabajar con ellos. Al usar el fuzzer Defensics para crear mutaciones de estos paquetes, pudimos observar que el dispositivo encontraba un punto de falla, producía un volcado de núcleo e inmediatamente se reiniciaba, como se muestra en la Figura 5.
Figura 5
El caso de prueba que causó el bloqueo se aisló y se ejecutó varias veces más para confirmar que el bloqueo era repetible. Descubrimos durante este proceso que se necesitan 96 paquetes adicionales enviados después del paquete con formato original para causar el bloqueo. El paquete mal formado en la serie era un paquete "I-Am", como se ve a continuación. El paquete completo no se muestra debido a su tamaño.
Figura 6
Examinando más a fondo, pudimos ver rápidamente que el fuzzer creó un paquete con un tamaño de capa BACnet de 8216 bytes, usando "0x22". También pudimos ver que el fuzzer reconoció el tamaño máximo aceptable para la capa de aplicación BACnet como solo 1476 bytes. Pruebas adicionales mostraron que enviar solo este paquete no produjo los mismos resultados; solo cuando se enviaron los 97 paquetes se produjo el bloqueo.
Analizando el choque
Como el sistema proporciona un volcado del núcleo al fallar, era lógico analizarlo para obtener más información. Desde el volcado del núcleo (reproducido en la Figura 7), pudimos ver que el dispositivo encontró un Fallo de segmentación. También vimos que el registro R0 contenía lo que parecían datos copiados de nuestro paquete con formato incorrecto, junto con la traza inversa potencialmente corrompida.
Figura 7
El volcado del núcleo también nos proporcionó la ubicación precisa del accidente. Usando el mapa de memoria del dispositivo, fue posible determinar que la dirección 0x4026e580 se encuentra en memcpy. Como el dispositivo no implementa la asignación aleatoria del diseño del espacio de direcciones (ASLR), la dirección de la memoria no cambió durante nuestras pruebas. Como extrajimos con éxito el firmware, utilizamos IDA Pro para intentar obtener más información sobre por qué estaba ocurriendo este bloqueo. Los desarrolladores no eliminaron los archivos binarios durante el tiempo de compilación, lo que ayudó a simplificar el proceso de reversión en IDA.
Figura 8
El desmontaje nos dijo que memcpy estaba intentando escribir lo que estaba en R3 en la "dirección" almacenada en R0. En este caso, sin embargo, habíamos corrompido esa dirección, causando la falla de segmentación. El contenido de varios otros registros también proporcionó información adicional. El valor 0x81 en R3 fue potencialmente el primer byte de un paquete BACnet de la capa BACnet Virtual Link Control (BVLC), identificando el paquete como BACnet. Al observar R3 y los valores en la dirección en R5 juntos, confirmamos con mayor certeza que esta era de hecho la capa BVLC. Esto implicaba que los datos que se copiaban eran del último paquete enviado y el destino de los datos copiados se tomaba del primer paquete con formato incorrecto. Los registros R8 y R10 contenían los números de puerto de origen y destino, respectivamente, que en este caso eran ambos 0xBAC0 (que explica la resistencia), o 47808, el puerto BACnet estándar. R4 contenía una dirección de memoria que, cuando se examinaba, mostraba una sección de memoria que parece haberse sobrescrito. Aquí vimos datos de nuestro paquete con formato incorrecto (0x22); En algunas áreas, la memoria se sobrescribió parcialmente con nuestros paquetes de datos. El valor para el destino de la memoria parecía provenir de esta región de la memoria. Sin ASLR habilitado, podríamos contar nuevamente con este aterrizaje siempre en la misma ubicación.
Figura 9
En este punto, con la información proporcionada por el volcado del núcleo, los paquetes y la IDA, estábamos bastante seguros de que el bloqueo encontrado era un desbordamiento del búfer. Sin embargo, memcpy es una función muy común, por lo que necesitábamos determinar de dónde provenía exactamente este bloqueo. Si la dirección de destino de la memoria se corrompía, entonces el bloqueo en la memoria era simplemente un daño colateral del desbordamiento del búfer, entonces, ¿qué código estaba causando el desbordamiento del búfer? Un buen lugar para comenzar este análisis sería la traza inversa; sin embargo, como se vio anteriormente, la traza inversa se corrompió de nuestra entrada. Dado que este dispositivo usa un procesador ARM, podríamos buscar pistas en los registros LR sobre qué código llama esta memoria. Aquí, LR apuntaba a 0x401e68a8 que, al hacer referencia al mapa de memoria del proceso, cae en "main.so". Después de calcular el desplazamiento a utilizar para el análisis estático, llegamos al código en la Figura 10.
Figura 10
El registro LR apuntaba a la instrucción que se llama después de que regresa memcpy. En este caso, estábamos interesados en la instrucción justo antes de la dirección a la que apunta LR, en el desplazamiento 0x15C8A4. A primera vista, nos sorprendió no ver la llamada de memoria esperada; sin embargo, al profundizar un poco más en la función scNetMove, encontramos que scNetMove es simplemente un contenedor para memcpy.
Figura 11
Entonces, ¿cómo se pasó la dirección de destino incorrecta a memcpy? Para responder a esto, necesitábamos una mejor comprensión de cómo el sistema procesa los paquetes entrantes junto con qué código es responsable de configurar los buffers enviados a memcpy. Nosotros podemos usar PD para evaluar el sistema mientras se ejecuta para ver que el proceso principal genera 19 hilos:
tabla 1
La función en la que encontramos el "scNetMove" se llamaba "scBIPRxTask" y solo se hacía referencia en otra ubicación fuera del binario principal; la función de inicialización para la red de la aplicación, que se muestra en la Figura 12.
Figura 12
En el desmontaje de scBIPRxTask, vimos que se creaba un nuevo hilo o "tarea" para ambas interfaces IP BACnet en los puertos 47808 y 47809. Estos hilos generados manejarían todos los paquetes entrantes en sus respectivos puertos. Cuando el sistema recibiría un paquete, el hilo responsable de scBIPRxTask se dispararía para cada paquete. Usando el descompilador IDA Pro, podríamos ver qué ocurre para cada paquete. Primero, la función usa memset poner a cero un búfer asignado en la pila y leer desde el zócalo de la red a este búfer. Este búfer se convierte en la fuente de la siguiente llamada de memcpy. El nuevo búfer se crea con un tamaño estático de 1732 bytes y solo 1732 bytes se leen adecuadamente del socket.
Figura 13
Después de leer los datos del socket, la función establece un lugar para almacenar el paquete que acaba de recibir. Aquí utiliza una función llamada "pk_alloc", que toma el tamaño del paquete para crear como único argumento. Notamos que el tamaño era otro valor estático y no el tamaño recibido de la función de lectura del socket. Esta vez el valor estático pasado es 1476 bytes. Este búfer asignado es lo que se convertirá en el destino de la memoria.
Figura 14
Con un búfer de origen y de destino asignado, se llama a "scNetMove" y posteriormente se llama a memcpy, pasando ambos búferes junto con el parámetro de tamaño tomado del valor de retorno de lectura del socket.
Figura 15
Esta ruta de código explica por qué y cómo se produce la vulnerabilidad. Para cada paquete enviado, se copia de la pila a la memoria; sin embargo, si el paquete tiene más de 1476 bytes, por cada byte de más de 1476 y menos de o igual a 1732, se sobrescribirán muchos bytes en la memoria después del final del búfer de destino. Dentro de la memoria que se sobrescribe, hay una dirección para el destino de una llamada de memoria posterior. Esto significa que hay una vulnerabilidad de desbordamiento de búfer que conduce a un arbitrario escribir condición. El primer paquete con formato incorrecto sobrescribe una sección de memoria con datos definidos por el atacante, en este caso, la dirección dónde el atacante desea escribirle. Después de que el sistema lea 95 paquetes adicionales, la dirección controlada por el atacante se colocará en la memoria como el búfer de destino. Los datos en el último paquete, que no necesitan estar malformados, son qué se escribirá en la ubicación establecida en el paquete anterior con formato incorrecto. Suponiendo que el último paquete también está controlado por el atacante, ahora es un escribir qué condición.
Pateando el perro
Con una comprensión firme de la vulnerabilidad descubierta, el siguiente paso lógico fue intentar crear un exploit funcional. Al desarrollar un exploit, la capacidad de depurar dinámicamente el objetivo es extremadamente valiosa. Para este fin, el equipo primero tuvo que compilar de forma cruzada herramientas de depuración como gdbserver para el núcleo y la arquitectura específicos del dispositivo. Dado que el dispositivo ejecuta una versión anterior del kernel de Linux, utilizamos una versión anterior de Buildroot para compilar gdbserver y luego otras aplicaciones.
Usando una unidad USB para transferir gdbserver al dispositivo, se realizó un intento inicial de depurar la aplicación en ejecución. Unos segundos después de conectar el depurador a la aplicación, el dispositivo inició un reinicio, como se muestra en la Figura 16.
Figura 16
Un mensaje de error nos dio una pista de por qué ocurrió el bloqueo, lo que indica una falla del temporizador de vigilancia. Los temporizadores de vigilancia son comunes en los dispositivos integrados críticos que, si el sistema se cuelga durante un período de tiempo predeterminado, toma medidas para tratar de corregir el problema. En este caso, la acción elegida por los desarrolladores es reiniciar el sistema. La búsqueda en los binarios del sistema para este mensaje de error reveló la sección de código que se muestra en la Figura 17. Los mensajes de error reales se han redactado a petición del proveedor.
Figura 17
La función está disminuyendo tres contadores. Si alguno de los contadores llega a cero, se genera un error y luego se reinicia el sistema. Examinar el código además muestra que múltiples procesos llaman a esta función para verificar los contadores con mucha frecuencia. Esto significa que no podremos depurar dinámicamente el sistema sin descubrir cómo deshabilitar este programa de vigilancia de software.
Un enfoque común para este problema es parchear los binarios. Cuando se busca parchear un archivo binario, es importante asegurarse de que el parche que utilice no presente ningún efecto secundario no deseado. Esto generalmente significa que desea hacer el menor cambio posible. En este caso, el cambio significativo más pequeño que se le ocurrió al equipo fue modificar "restar por 5" a "restar por 0". Esto no cambiaría la forma en que funcionó el programa general; sin embargo, cada vez que se llamaba a la función para disminuir el contador, el contador simplemente nunca se volvería más pequeño. El código parcheado se proporciona en la Figura 18. Observe que el descompilador IDA ha eliminado por completo la declaración de resta del código, ya que ya no tiene sentido.
Figura 18
Con el perro guardián del software parcheado, el equipo intentó nuevamente depurar dinámicamente la aplicación. Inicialmente, se pensó que la prueba era exitosa, ya que era posible conectarse a gdbserver y comenzar a depurar la aplicación. Sin embargo, después de tres minutos, el sistema se reinició nuevamente. La Figura 19 muestra el mensaje que el equipo captó al reiniciar después de varios experimentos repetidos con los mismos resultados.
Figura 19
Esto indica que en la fase de inicio del inicio, un watchdog de hardware está configurado en 180 segundos (o tres minutos). El sistema tiene dos temporizadores de vigilancia, uno de hardware y otro de software; solo habíamos desactivado uno de los temporizadores. El mismo método de parchear el binario que se usó para deshabilitar el temporizador de vigilancia del software no funcionaría para el temporizador de vigilancia del hardware; la aplicación también necesitaría patear el perro guardián para evitar un reinicio. Armados con este conocimiento, recurrimos a los archivos binarios de Delta en el dispositivo para obtener un código que pudiera ayudarnos a "patear" el perro guardián del hardware. Con los símbolos de depuración restantes, fue relativamente fácil encontrar una función responsable de administrar el perro guardián del hardware.
Existen varios enfoques que podrían usarse para intentar deshabilitar el watchdog de hardware. En este escenario, decidimos aprovechar el hecho de que el código que trataba con el perro guardián del hardware estaba en una biblioteca compartida y se exportaba. Esto permitió la creación de un nuevo programa utilizando el código existente de vigilancia. Al crear un segundo programa que pateará el perro guardián del hardware, podríamos depurar la aplicación Delta sin reiniciar el sistema.
Este programa se colocó en el script de inicio del sistema, por lo que se ejecutaría en el arranque y continuamente "patearía al perro", deshabilitando efectivamente el perro guardián del hardware. Nota: ningún perro real fue dañado en la investigación o creación de esta hazaña. En todo caso, se les dio golosinas adicionales y contribuyeron a la codificación del parche de vigilancia. Aquí hay algunas fotos muy recientes de los perros de este investigador como prueba.
Figura 20
Con los temporizadores de vigilancia de hardware y software pacificados, podríamos continuar determinando si nuestra vulnerabilidad descubierta anteriormente era explotable.
Escribiendo el exploit
Antes de intentar la explotación, queríamos investigar si el sistema tenía alguna mitigación o limitación de explotación que debiéramos tener en cuenta. Comenzamos ejecutando un script de código abierto llamado "checksec.sh". Este script, cuando se ejecuta en un binario, informará si alguna de las mitigaciones de exploits comunes están en su lugar. La Figura 21 muestra la salida del script cuando se ejecuta en el binario Delta primario, denominado "dactetra".
Figura 21
El cheque regresó con solo NX habilitado. Esto también fue válido para cada una de las bibliotecas compartidas donde se encuentra el código vulnerable.
Como se discutió anteriormente, la vulnerabilidad permite una condición de escribir qué, dónde, lo que nos lleva a la pregunta más importante: ¿qué queremos escribir dónde? En última instancia, queremos escribir shellcode en algún lugar de la memoria y luego saltar a ese shellcode. Como el atacante controla el último paquete enviado, es posible que el atacante pueda tener su código shell en la pila. Si colocamos shellcode en la pila, tendríamos que pasar por alto la protección No eXecute (NX) descubierta usando la herramienta checksec. Aunque esto es posible, nos preguntamos si había un método más simple.
Reexaminando el volcado de memoria en la ubicación de memoria que ha sido sobrescrita por el gran paquete mal formado, encontramos una pequeña sección contigua de memoria de almacenamiento dinámico, con un total de 32 bytes, que el atacante podría controlar. Llegamos a esta conclusión debido a la presencia de 0x22 bytes, el contenido de la carga útil del paquete con formato incorrecto. En el momento en que se produce el desbordamiento, una mayor parte de esta región está llena de 0x22, pero cuando se activa nuestra condición de escribir en qué lugar, muchos de estos bytes se bloquean, dejándonos con la sección de 32 bytes que se muestra en la Figura 22.
Figura 22
Al ser una memoria de almacenamiento dinámico, esta región también era ejecutable, un detalle que pronto será importante. Reemplazar los 0x22 en el paquete con formato incorrecto con un patrón no repetitivo reveló dónde en la carga útil colocar nuestro código de shell y confirmó que los bytes en esta región eran todos únicos.
Con un lugar potencial para colocar nuestro código de shell, el siguiente componente principal para abordar fue controlar la ejecución. La condición de escribir qué dónde nos permitió escribir en cualquier lugar de la memoria; sin embargo, no nos dio el control de la ejecución. Una técnica para abordar este problema es aprovechar la tabla de compensación global (GOT). En Linux, el GOT redirige un puntero de función a una ubicación absoluta y se ubica en la sección .got de un objeto ELF ejecutable o compartido. Dado que la sección .got se escribe en el momento de la ejecución, generalmente todavía se puede escribir más tarde durante la ejecución. La reubicación de solo lectura (RELRO) es una mitigación de exploits que marca la sección .got cargada de solo lectura una vez que se asigna; sin embargo, como se vio anteriormente, esta protección no estaba habilitada convenientemente. Esto significaba que era posible usar la condición de escribir lo que eran para escribir la dirección de nuestro shellcode en la memoria en el GOT, reemplazando un puntero de función de una llamada de función futura. Una vez que se llama al puntero de función reemplazado, se ejecutará nuestro shellcode.
¿Pero qué puntero de función debemos reemplazar? Para garantizar la mayor probabilidad de éxito, decidimos que sería mejor reemplazar el puntero a una función que se llame lo más cerca posible de la sobrescritura. Esto se debe a que queríamos minimizar los cambios en el diseño de la memoria durante la ejecución del programa. Examinando el código nuevamente desde el retorno de la función "scNetMove", vemos en unas pocas instrucciones que se llama "scDecodeBACnetUDP". Por lo tanto, esto se convierte en la opción ideal de puntero para sobrescribir en el GOT.
Figura 23
Sabiendo qué escribir dónde, luego consideramos las condiciones que debían cumplirse para que se tomara la ruta de código correcta para desencadenar la vulnerabilidad. Echando otro vistazo al código en memcpy que permite que ocurra el desbordamiento del búfer, notamos que la sobrescritura sí tiene una condición, como se muestra en la Figura 24.
Figura 24
El código que produce la sobrescritura en la memoria solo se toma si el valor en R0, cuando AND bit a bit con el valor inmediato 3, no es igual a 0. Desde nuestro volcado por caída, sabíamos que el valor en R0 es la dirección del destino que querer copiar Esto potencialmente plantea un problema. Si la dirección en la que quisiéramos escribir estuviera alineada con 4 bytes, lo cual era muy probable, no se tomaría la ruta del código para nuestra vulnerabilidad. Podríamos asegurarnos de que nuestra ruta de código se tomó restando uno de la dirección en la que deseamos escribir en el GOT y luego reparando el último byte de la entrada anterior. Esto garantiza que se tome la ruta de código correcta y que no dañemos involuntariamente un segundo puntero de función.
Shellcode
Si bien descubrimos un lugar para colocar nuestro código de shell, solo descubrimos una cantidad muy pequeña de espacio, específicamente 32 bytes, para escribir la carga útil, que se muestra en la Figura 24. ¿Qué podemos lograr en una cantidad de espacio tan pequeña? Un método que no requiere un código shell extenso es usar un ataque de "retorno a libc" para ejecutar el sistema mando. Para que nuestro exploit funcione fuera de la caja, cualquier comando o programa que ejecutemos sistema debe estar presente en el dispositivo de forma predeterminada. Además, la cadena de comando en sí misma debe ser bastante corta para acomodar el número limitado de bytes con los que tenemos que trabajar.
Un escenario ideal sería la ejecución de código que permitiría el acceso de shell remoto al dispositivo. Afortunadamente, Netcat está presente en el dispositivo y esta versión de Netcat admite tanto el indicador "-ll", para escuchar de forma persistente en un puerto para una conexión, como el indicador "-e", para ejecutar un comando en la conexión. Por lo tanto, podríamos usar sistema ejecutar Netcat para escuchar en algún puerto y ejecutar un shell cuando se realiza una conexión. Antes de escribir el código de shell para ejecutar sistema con este comando, primero probamos varios comandos de Netcat en el dispositivo directamente para determinar el comando de Netcat más corto que aún nos daría un shell. Después de algunas iteraciones, pudimos acortar el comando Netcat a 13 bytes:
nc -llp9 -esh
Dado que las instrucciones deben estar alineadas con 4 bytes y tenemos 32 bytes para trabajar, solo nos preocupa la longitud de la cadena redondeada al múltiplo más cercano de 4, por lo que en este caso 16 bytes. Restando esto de nuestro total de 32 bytes, tenemos 16 bytes restantes, o 4 instrucciones en total, para configurar el argumento para sistema y saltar a eso. Un método común para ajustar más instrucciones en un espacio pequeño en la memoria en ARM es cambiar al modo Thumb. Esto se debe a que el modo Thumb de ARM utiliza instrucciones de 16 bits (2 bytes), en lugar de las instrucciones ARM normales de 32 bits (4 bytes). Desafortunadamente, el procesador en este dispositivo no era compatible con el modo Thumb y, por lo tanto, esta no era una opción.
El desafío para lograr nuestra tarea en solo 4 instrucciones ARM es el límite que ARM coloca en los valores inmediatos. Saltar a sistema, necesitábamos usar un valor inmediato como la dirección para saltar, pero la dirección de memoria generalmente no son valores pequeños. Los valores inmediatos en ARM están limitados a 12 bits; ocho de estos bits son para el valor en sí y los otros 4 se utilizan para el desplazamiento de bits. Esto significa que un valor inmediato solo puede tener un byte de largo (dos dígitos hexadecimales), pero ese byte puede ser rellenado con cero de la manera que desee. Por lo tanto, cargar una dirección de memoria completa de 4 bytes usando valores inmediatos tomaría las 4 instrucciones, ya sea usando MOV o ADD. Si bien tenemos 4 instrucciones para jugar, también necesitamos al menos una instrucción para cargar la dirección de nuestra cadena de comandos en R0, el registro utilizado como primer parámetro para el sistema y al menos una instrucción para ramificar a la dirección, lo que requiere Un total de 6 instrucciones.
Una forma de reducir la cantidad de instrucciones necesarias es comenzar copiando un registro que ya contiene un valor cercano a la dirección que queremos en el momento en que se ejecuta el shellcode. Si esto es factible depende del valor de la dirección a la que queremos saltar en comparación con las direcciones que tenemos disponibles en los registros justo antes de que se ejecute nuestro código de shell.
Comenzando con la dirección a la que debemos llamar, descubrimos tres direcciones a las que podríamos saltar para llamar sistema.
- 0x4006425C – la dirección de un BL sistema (bifurcar a sistema) instrucción en boot.so.
- 0x40054510 – la dirección de la sistema entrada en "boot.so" GOT.
- 0x402874A4 – la dirección directa de sistema en libuClibc-0.9.30.so.
Luego, comparamos estas opciones con los valores en los registros en el momento en que el shellcode está a punto de ejecutarse usando GDB, como se muestra en la Figura 25.
Figura 25
De los registros a los que tenemos acceso en el momento en que se ejecuta nuestro código shell, el que nos da el delta más pequeño entre su contenido y una de estas tres direcciones que podemos usar para llamar sistema es R4. R4 contiene 0x40235CB4, dando un delta de 0x517F0 en comparación con la dirección para una llamada directa a sistema. El último mordisco es 0 es ideal, ya que eso significa que no tenemos que dar cuenta del último bit, gracias al mecanismo de rotación inherente a los valores inmediatos de ARM. Esto significa que solo necesitamos dos valores inmediatos para convertir el contenido de R4 en nuestra dirección deseada: uno para 0x51000, el otro para 0x7F0. Como podemos aplicar un desplazamiento inmediato cuando MOV coloca un registro en otro, deberíamos poder cargar un registro con la dirección de sistema en solo dos instrucciones. Con una instrucción para realizar la bifurcación y 16 bytes para la cadena de comando, esto significa que podemos obtener todo nuestro código de shell en 32 bytes, suponiendo que podamos cargar R0 con la dirección de nuestra cadena en una instrucción.
Al iniciar nuestra cadena ASCII para el comando directamente después de la cuarta y última instrucción, podemos copiar la PC en R0 con el desplazamiento apropiado para que apunte a la cadena. Un beneficio adicional de este enfoque es que hace que la dirección de la cadena sea independiente del lugar donde se coloca el código de shell en la memoria, ya que es relativa a la PC. La Figura 26 muestra el aspecto del shellcode teniendo en cuenta todas las restricciones.
Dibujo 26
Es importante tener en cuenta que la directiva de ensamblador ".asciz" se utiliza para colocar un literal de cadena ASCII terminado en nulo en la memoria. Se eligió R12 como el registro para contener la dirección de la sucursal, ya que R12 es el registro de espacio reutilizable intraprocesal (IP) en la arquitectura ARM. Esto significa que R12 a menudo se usa como un registro de propósito general dentro de las subrutinas, lo que indica que es casi seguro que sea seguro para nuestros propósitos sin experimentar efectos adversos inesperados.
Juntando todo junto
Con una comprensión firme de la vulnerabilidad, la explotación y el shellcode necesarios, ahora podríamos intentar la explotación. Al observar la secuencia de paquetes utilizados para causar este ataque, no se trata de un ataque de paquete único, sino de un ataque de paquete múltiple. El desbordamiento del búfer inicial está contenido en el paquete grande con formato incorrecto, entonces, ¿qué datos incorporamos? Este paquete sobrescribe memoria pero no proporciona control sobre la ejecución; por lo tanto, esto puede considerarse el paquete de "configuración" o "provisional". Aquí es donde memcpy buscará la dirección del búfer de destino para nuestro último paquete. La dirección que queremos sobrescribir va en este paquete seguido de nuestro código de shell. Como se explicó anteriormente, la dirección que estamos buscando sobrescribir es la dirección del puntero de función scDecodeBACnetUDP en GOT menos uno, para garantizar que la dirección no esté alineada en 4 bytes. Al reparar el último byte del puntero de función anterior y sobrescribir esta dirección, podemos obtener el control de ejecución.
The large malformed packet contains “where” we want to “write” to and puts our shellcode into memory yet does not contain “what” we want to write. The “what”, in this case, is the address of our shellcode, so our last packet needs to contain this address. The final challenge is deciding where in the last packet the address belongs.
Recall from the core dump shown previously that the crash happens on memcpy attempting to write the value 0x81 to the bad address. 0x81 is the first byte of the BVLC layer, indicating this where our address needs to go within the last packet to ensure that only the address we want is overwritten. We also need to ensure there are not any bytes after our address, otherwise we will continue to overwrite the GOT past our target address. Since this application is a multi-threaded application, this could cause the application to crash before our shellcode has a chance to execute. Since the BVLC layer is typically how a packet is identified as a BACnet packet, a potential problem with altering this layer is that the last packet will no longer look like a BACnet packet. If this is the case, will the application still ingest the packet? The team tested this and discovered that the application will ingest any broadcast packet regardless of type, since the vulnerable code is executed before the code that validates the packet type.
Taking everything into account and sending the series of 97 packets, we were able to successfully exploit the building manager by creating a bind shell. Below is a video demonstrating this attack:
(embed)https://www.youtube.com/watch?v=HNAfATQGTRo(/embed)
A Real-world Scenario
Although providing a root shell to an attacker proves the vulnerability is exploitable, what can an attacker do with it? A shell by itself does not prove useful unless an attacker can control the normal operation of the system or steal valuable data. In this case, there is not a lot of useful data stored on the device. Someone could download information about how the system is configured or what it’s controlling, which may have some value, but this will not hold significant impact on its own. It is also plausible to delete essential system files via a denial-of-service attack that could easily put the target in an unusable state, but pure destruction is also of low value for various reasons. First, as mentioned previously, the device has a backup image that it will fall back to if a failure occurs during the boot process. Without physical access to the device, an attacker wouldn’t have a clear idea of how the backup image differs from the original or even if it is exploitable. If the backup image uses a different version of the firmware, the exploit may no longer work. Perhaps more importantly, a denial-of-service attack suffers from its inherent lack of subtlety. If the attack immediately causes alarms to go off when executed, the attacker can expect that their persistence in the system will be short-lived.
What if the system could be controlled by an attacker while being undetected? This scenario becomes more concerning considering the type of environments controlled by this device.
Normal Programming
Controlling the standard functions of the device from just a root shell requires a much deeper understanding of how the device works in a normal setting. Typically, the Delta eBMGR is programmed by an installer to perform a specific set of tasks. These tasks can range from managing access control, to building lighting, to HVAC, and more. Once programmed, the controller is connected to several external input/output (I/O) modules. These modules are utilized for both controlling the state of an attached device and relaying information back to the manager. To replicate these “normal conditions”, we had a professional installer program our device with a sample program and attach the appropriate modules.
Figure 27 shows how each component is connected in our sample programming. For our initial testing, we did not actually have the large items such as the pump, boiler and heating valve. The state of these items can be tracked through either LEDs on the modules or the touchscreen interface, hence it was unnecessary for us to acquire them for testing purposes. Despite this, it is still important to note which type of input or output each “device”, virtual or otherwise, is connected to on the modules.
Figure 27
The programming to control these devices is surprisingly simple. Essentially, based on the inputs, an output is rendered. Figure 28 shows the programming logic present on the device during our testing.
Figure 28
There are three user-defined software variables: “Heating System”, “Room Temp Spt”, and “Heating System Enable Spt”. Here, “spt” indicates a set point. These can be defined by an operator at run time and help determine when an output should be turned on or off. The “Heating System” binary variable simply controls the on/off state of the system.
Controlling the Device
Like when we first started looking for vulnerabilities, we want to ensure our method of controlling the device is not dependent on code which could vary from controller to controller. Therefore, we want to find a method that allows us to control all the I/O devices attached to a Delta eBMGR, ensuring we are not dependent on this device’s specific programming.
As on any Linux-based system, the installer-defined programming at its lowest level utilizes system calls, or functions, to control the attached hardware. By finding a way to manipulate these functions, we would therefore have a universal method of controlling the modules regardless of the installer programming. A very common way of gaining this type of control when you have root access to a system is through the use of function hooking. The first challenge for this approach is simply determining which function to hook. In our case, this required an extensive amount of reverse engineering and debugging of the system while it was running normally. To help reduce the scope of functions we needed to investigate, we began by focusing our attention on controlling binary output (BO). Our first challenge was how to find the code that handles changing the state of a binary output.
A couple of key factors helped point us in the right direction. First, the documentation for the controller indicates the devices talk to the I/O modules over a Controller Area Network Bus (CAN bus), which is common for PLC devices. As previously seen, the Delta binaries all have symbols included. Thus, we can use the function names provided in the binaries to help reduce the code surface we need to look at – IDA tells us there are only 28 functions with “canio” as the first part of their name. Second, we can assume that since changing the state of a BO requires a call to physical hardware, a Linux system call is needed to make that change. Since the device is making a change to an IO device, it is highly likely that the Linux system call used is “ioctl”. When cross-referencing the functions that start with “canio” and that call “ioctl”, our prior search space of 28 drops to 14. One function name stood out above the rest: “canioWriteOutput”. The decompiled version of the function has been reproduced in Figure 29.
Figure 29
Using this hypothesis, we set a break point on the call to “ioctl” inside canioWriteOutput and use the touchscreen to change the state of one of the binary outputs from “off” to “on”. Our breakpoint was hit! Single stepping over the breakpoint, we were able to see the correct LED light up, indicating the output was now on.
Now knowing the function we needed to hook, the question quickly became: How do we hook it? There are several methods to accomplish this task, but one of the simplest and most stable is to write a library that the main binary will load into memory during its startup process, using an environment variable called LD_PRELOAD. If a path or multiple paths to shared objects or libraries are set in LD_PRELOAD before executing a program, that program will load those libraries into its memory space before any other shared libraries. This is significant, because when Linux resolves a function call, it looks for function names in the order in which the libraries are loaded into memory. Therefore, if a function in the main Delta binary shares a name and signature with one defined in an attacker-generated library that is loaded first, the attacker-defined function will be executed in its place. As the attacker has a root shell on the device, it is possible for them to modify the init scripts to populate the LD_PRELOAD variable with a path to an attacker-generated library before starting the Delta software upon boot, essentially installing malware that executes upon reboot.
Using the cross-compile toolchain created in the early stages of the project, it was simple to test this theory with the “library” shown in Figure 30.
Figure 30
The code above doesn’t do anything meaningful, but it does confirm if hooking this method will work as expected. We first defined a function pointer using the same function prototype we saw in IDA for canioWriteOutput. When canioWriteOutput is called, our function will be called first, creating an output file in the “opt” directory and giving us a place to write text, proving that our hook is working. Then, we search the symbol table for the original “canioWriteObject” and call it with the same parameters passed into our hook, essentially creating a passthrough function. The success of this test confirmed this method would work.
For our function hook to do more than just act as a passthrough, we needed to understand what parameters were being passed to the function and how they affect execution. By using GDB, we could examine the data passed in during both the “on” and “off” states. For canioWriteObject, it was discovered that the state of binary output was encoded into the second parameter passed to the function. From there, we could theoretically control the state of the binary output by simply passing the desired state as the second parameter to the real function, leaving the other parameters as-is. In practice, however, the state change produced using this method persisted only for a split second before the device reset the output back to its proper state.
Why was the device returning the output to the correct state? Is there some type of protection in place? Investigating strings in the main Delta binary and the filesystem on the device led us to discover that the device software maintains databases on the filesystem, likely to preserve device and state information across reboots. At least one of these databases is used to store the state of binary outputs along with, presumably, other kinds of I/O devices. With further investigation using GDB, we discovered that the device is continuously polling this database for the state of any binary outputs and then calling canioWriteOutput to publish the state obtained from the database, clobbering whatever state was there before. Similarly, changes to this state made by a user via the touchscreen are stored in this same database. At first, it may appear that the simplest solution would be to change the database value since we have root access to the device. However, the database is not in a known standard format, meaning we would need to take the time to reverse this format and understand how the data is stored. As we already have a way to hook the functions, controlling the outputs at the time canioWriteOutput is called is simpler.
To accomplish this, we updated our malware to keep track of whether the attacker has made a modification to the output or not. If they have, the hook function replaces the correct state, stored in canioWriteOutput’s second parameter, with the state asserted by the attacker before calling the real canioWriteOutput function. Otherwise, the hook function acts as a simple passthrough for the real deal. A positive side effect of this, from the attacker’s perspective, is the touchscreen will show the output as the state the user last requested even after the malware has modified it. Implementing this simple state-tracking resolved our prior issue of the attacker-asserted state not persisting.
With control of the binary output, we moved on to looking at each of the other types of inputs and outputs that can be connected to the modules. We used a similar approach in identifying the methods used to read or write data from the modules and then hooking them. Unfortunately, not every function was as simple as canioWriteOutput. For example, when reversing the functions used to control analog outputs, we noticed that they utilized custom data structures to hold various information about the analog device, including its state. As a result, we had to first reverse the layout of these data structures to understand how the analog information was being sent to the outputs before we could modify their state. By using a combination of static and dynamic analysis, we were able to create a comprehensive malicious library to control the state of any device connected to the manager.
Taking our Malware to the Next Level
Although making changes from a root shell certainly proves that an attacker can control the device once it has been exploited, it is more practical and realistic for the attacker to have complete remote control not contingent on an active shell. Since we were already loading a library on startup to manipulate the I/O modules, we decided it would also be feasible to use that same library to create a command-and-control type infrastructure. This would allow an attacker to just send commands remotely to the “malware” without having to maintain a constant connection or shell access.
To bring this concept to life, we needed to create a backdoor and an initialization function was probably the best place to put one. After some digging, we found “canioInit”, a function responsible for initializing the CAN bus. Since the CAN bus is required to make any modifications to the operation of the device, it made sense to wait for this function to be called before starting our backdoor. Unlike some of the previous hooks mentioned, we don’t make any changes to this call or its return data; we only use it as a method to ensure our backdoor is started at the proper time.
Figure 31
When canioInit is called, we first spawn a new thread and then execute the real canioInit function. Our new thread opens a socket on UDP port 1337 and listens for very specific commands, such as “bo0 on” to indicate to “turn on binary output 0” or “reset” to put the device back in the user’s control. Based on the commands provided, the “set_io_state” method called by this thread activates the necessary hooking methods to control the I/O as described in the previous section.
Figure 32
With a fully functioning backdoor in the memory space of the Delta software, we had full control of the device with a realistic attack chain. Figure 33 outlines the entire attack.
Figure 33
The entire process above, from sending out the malicious packets to gaining remote control, takes under three minutes, with the longest task being the reboot. Once the attacker has established control, they can operate the device without impacting what information the user is provided, allowing the attacker to stay undetected and granting them ample opportunity to cause serious damage, depending on what kind of hardware the Delta controller manages.
Real World Impact
What is the impact of an attack like this? These controllers are installed in multiple industries around the world. Via Shodan, we have observed nearly 600 internet-accessible controllers running vulnerable versions of the firmware. We tracked eBMGR devices from February 2019 to April 2019 and found that there were a significant number of new devices available with public IP addresses.
As of early April 2019, 492 eBMGR devices remained reachable via internet-wide scans using Shodan. Of those found, a portion are almost certainly honeypots based on user-applied tags found in the Shodan data, leaving 404 potentially vulnerable victims. If we include other Delta Controls devices using the same firmware and assume a high likelihood they are vulnerable to the same exploit, the total number of potential targets balloons to over 1600. We tracked 119 new internet connected eBMGR devices since February 2019; however, these were outpaced by the 216 devices that have subsequently gone offline. We believe this is a combination of standard practice for ICS systems administrators to connect these devices to the Internet, coupled with a strategy by the vendor (Delta Controls) proactively reaching out to customers to reduce the internet-connected footprint of the vulnerable devices. Most controllers appear to be in North America with the US accountable for 53% of online devices and Canada accounting for 35%. It is worth noting the fact that in some cases the IP address, and hence the geographic location of the device from Shodan, is traced back to an ISP (Internet Service Provider), which could result in skewed findings for locations.
Some industries seem more at risk than others given the accessibility of devices. We were only able to map a small portion of these devices to specific industries, but the top three categories we found were Education, Telecommunications, and Real Estate. Education included everything from elementary schools to universities. In academic settings, the devices were sometimes deployed district-wide, in numerous facilities across multiple campuses. One example is a public-school system in Canada where each school building in the district had an accessible device. Telecommunications was comprised entirely of ISPs and/or phone companies. Many of these could be due to the ISPs being listed as a service provider. The real estate category generally included office and apartment buildings. From available metadata in the search results, we also managed to find instances of education, healthcare, government, food, hospitality, real estate, child care and financial institutions using the vulnerable product.
With a bit more digging, we were easily able to find other targets through publicly available information. While it is not common practice to post sensitive documents online, we’ve found many documents available that indicate that these devices are used as part of the company’s building automation plans. This was particularly true for government buildings where solicitations for proposals are issued to build the required infrastructure. All-in-all we have collected around 20 documents that include detailed proposals, requirements, pricing, engineering diagrams, and other information useful for reconnaissance. One particular government building had a 48-page manual that included internal network settings of the devices, control diagrams, and even device locations.
Redacted network diagram found on the Internet specifying ICS buildout
What does it matter if an attacker can turn on and off someone’s AC or heat? Consider some of the industries we found that could be impacted. Industries such as hospitals, government, and telecommunication may have severe consequences when these systems malfunction. For example, the eBMGR is used to maintain positive/negative pressure rooms in medical facilities or hospitals, where the slightest change in pressurization could have a life-threating impact due to the spread of airborne diseases. Suppose instead a datacenter was targeted. Datacenters need to be kept at a cool temperature to ensure they do not overheat. If an attacker were to gain access to the vulnerable controller and use it to raise heat to critical levels and disable alarms, the result could be physical damage to the server hardware in mass, as well as downtime costs, not to mention potential permanent loss of critical data. According to the Ponemon Institute (https://www.ponemon.org/library/2016-cost-of-data-center-outages), the average cost of a datacenter outage was as high as $740,357 in 2016 and climbing. Microsoft was a prime example of this; in 2018, the company suffered a massive datacenter outage (https://devblogs.microsoft.com/devopsservice/?p=17485) due to a cooling failure, which impacted services for around 22 hours.
To show the impact beyond LED lights flashing, McAfee’s ATR contracted a local Delta installer to build a small datacenter simulation with a working Delta system. This includes both heating and cooling elements to show the impact of an attack in a true HVAC system. In this demonstration we show both normal functionality of the target system, as well as the full attack chain, end-to-end, by raising the temperature to dangerous levels, disabling critical alarms and even faking the controller into thinking it is operating normally. The video below shows how this simple unpatched vulnerability could have devastating impact on real systems.
(embed)https://www.youtube.com/watch?v=ZHxw5Q11Aio(/embed)
We also leverage this demo system, now located in our Hillsboro research lab, to highlight how an effective patch, in this case provided by Delta Controls, is used to immediately mitigate the vulnerability, which is ultimately our end goal of this research project.
Conclusion
Discoveries such as CVE-2019-9569 underline the importance of secure coding practices on all devices. ICS devices such as this Delta building manager control critical systems which have the potential to cause harm to businesses and people if not properly secured.
There are some best practices and recommendations related to the security of products falling into nonstandard environments such as industrial controls. Based on the nature of the devices, they may not have the same visibility and process control as standard infrastructure such as web servers, endpoints and networking equipment. As a result, industrial control hardware like the eBMGR PLC may be overlooked from various angles including network or Internet exposure, vulnerability assessment and patch management, asset inventory, and even access controls or configuration reviews. For example, a principle of least privilege policy may be appropriate, and a network isolation or protected network segment may help provide boundaries of access to adversaries. An awareness of security research and an appropriate patching strategy can minimize exposure time for known vulnerabilities. We recommend a thorough review and validation of each of these important security tenants to bring these critical assets under the same scrutiny as other infrastructure.
One goal of the McAfee Advanced Threat Research team is to identify and illuminate a broad spectrum of threats in today’s complex and constantly evolving landscape. As per McAfee’s vulnerability public disclosure policy, McAfee’s ATR informed and worked directly with the Delta Controls team. This partnership resulted in the vendor releasing a firmware update which effectively mitigates the vulnerability detailed in this blog, ultimately providing Delta Controls’ consumers with a way to protect themselves from this attack. We strongly recommend any businesses using the vulnerable firmware version (571848 or prior) update as soon as possible in line with your patch policy and testing strategy. Of special importance are those systems which are Internet-facing. McAfee customers are protected via the following signature, released on August 6th: McAfee Network Security Platform 0x45d43f00 BACNET: Delta enteliBUS Manager (eBMGR) Remote Code Execution Vulnerability.
We’d like to take a minute to recognize the outstanding efforts from the Delta Controls team, which should serve as a poster-child for vendor/researcher relationships and the ability to navigate the unique challenges of responsible disclosure. We are thrilled to be collaborating with Delta, who have embraced the power of security research and public disclosure for both their products as well as the common good of the industry. Por favor refiérase a la siguiente declaración de Delta Controls, que proporciona información sobre la colaboración con McAfee y el poder de la divulgación responsable.