¡Llama a un exorcista! ¡Mi robot está poseído!


Visión general

Como parte de nuestro objetivo continuo de ayudar a los desarrolladores a proporcionar productos más seguros para empresas y consumidores, aquí en McAfee Advanced Threat Research (ATR) investigamos recientemente temi, un robot de teleconferencia producido por Robotemi Global Ltd. Nuestra investigación nos llevó a descubrir cuatro vulnerabilidades distintas en el robot temi, que este artículo describirá con gran detalle. Éstos incluyen:

  1. CVE-2020-16170: uso de credenciales codificadas de forma rígida
  2. CVE-2020-16168 – Error de validación de origen
  3. CVE-2020-16167 – Falta autenticación para función crítica
  4. CVE-2020-16169: Omisión de autenticación mediante una ruta de canal alternativa

En el momento en que se hicieron estos hallazgos, la aplicación de Android temi estaba ejecutando la versión 1.3.3 y el propio temi estaba ejecutando la versión de firmware 20190419.165201, Launcher OS versión 11969 y Robox OS versión 117.21.

Juntas, un actor malintencionado podría usar estas vulnerabilidades para espiar las videollamadas de temi, interceptar llamadas destinadas a otro usuario e incluso operar temi de forma remota, todo sin autenticación.

Según la política de divulgación de vulnerabilidades de McAfee, informamos nuestros hallazgos a Robotemi Global Ltd. el 5 de marzo de 2020. Poco tiempo después, respondieron y comenzaron un diálogo continuo con ATR mientras trabajaban para adoptar las mitigaciones que describimos en nuestro informe de divulgación. A partir del 15 de julio de 2020, estas vulnerabilidades se han parcheado con éxito y felicitamos a Robotemi por su apertura y disposición para colaborar a lo largo de este proceso. Llegaríamos a decir que este ha sido uno de los proveedores más receptivos, proactivos y eficientes con los que McAfee ha tenido el placer de trabajar.

Este documento tiene la intención de ser un análisis técnico extenso del proceso de descubrimiento de vulnerabilidades, los exploits posibilitados por vulns y el impacto potencial que dichos exploits pueden tener. Aquellos interesados ​​en una descripción general menos técnica y de alto nivel de estos hallazgos deben consultar nuestra publicación de blog resumida aquí.

Contenido

Visión general

Contenido

Que es temi

Funcionamiento normal de temi

Reconocimiento inicial

Escaneo de puertos

Captura de tráfico

Conseguir un caparazón

Encontrar el código de temi

Invertir el código para videollamadas

Fuerza bruta del nombre del canal como vector de ataque

Exploración de vectores de ataque MQTT

Visión general

Modificación de la aplicación temi Phone

La relación entre los ID de robots y los temas de MQTT

¿Cómo se publican los mensajes de invitación a llamadas MQTT?

Interceptar llamadas

Problema: temi no responde a mis llamadas

Solución: conviértase en PROPIETARIO

Agregar un PROPIETARIO: perspectiva de la aplicación de teléfono

Agregar un PROPIETARIO: perspectiva de temi

Desvío: infiltrarse en la lista de contactos de temi

Obtener privilegios de PROPIETARIO

Refinando las hazañas

Impacto

Conclusión

¿Qué es temi?

Robots.

La frontera final.

Para el "cerebro" de una tableta Android sobre un robot de 4 pies de altura, temi incluye muchos sensores en un factor de forma pequeño. Estos incluyen LIDAR de 360 ​​°, tres cámaras diferentes, cinco sensores de proximidad e incluso un sensor de unidad de medición inercial (IMU), que es una especie de acelerómetro + giroscopio + magnetómetro todo en uno. Todos estos trabajan juntos para darle a temi algo parecido a la capacidad de moverse de forma autónoma a través de un espacio mientras evita cualquier obstáculo. Si no fuera por las nefastas fuerzas de escaleras y bordillos, temi sería imparable.

Robotemi comercializa su robot como utilizado principalmente para teleconferencias. Los artículos vinculados desde el sitio web de temi describen las aplicaciones del robot en varias industrias: Connected Living se asoció recientemente con temi para su uso en el cuidado de personas mayores, el café Kellog's en Nueva York adoptó el temi para "mejorar la experiencia de venta minorista" y la empresa de personal corporativo Collabera usa temi para " mejorar la comunicación entre oficinas ". A pesar de su lema de "robot personal", parece que temi está diseñado tanto para consumidores y aplicaciones empresariales, y es esta última la que realmente hizo que McAfee Advanced Threat Research nos interesara como objetivo de investigación. Su creciente presencia en el espacio médico, que los creadores de temi han acomodado aumentando la producción a 1.000 unidades al mes, es especialmente interesante dada la gran demanda de visitas médicas remotas. ¿Qué significaría un temi comprometido para sus usuarios, ya sea la madre en el trabajo o el paciente al que se le diagnostica mediante un proxy robótico? Hicimos nuestro pedido por adelantado y nos dispusimos a averiguarlo.

Funcionamiento normal de temi

Una vez que finalmente llegó, pudimos configurarlo como lo haría cualquier usuario: lo desempaquetamos, lo conectamos a su base de carga y lo conectamos a WiFi. El funcionamiento normal del robot temi se realiza mediante el uso de su aplicación de teléfono inteligente, y en el primer inicio, temi nos solicitó escanear un código QR con la aplicación. El teléfono utilizado para escanear el código QR se convierte en el "administrador" de temi, lo que le permite controlar el robot de forma remota con solo llamarlo. Temi puede tener muchos contactos fuera de su administrador singular, y convertirse en un contacto es bastante sencillo. Siempre que inicia la aplicación de teléfono temi, escanea los contactos de su teléfono y agrega automáticamente cualquier número que se haya registrado con la aplicación temi a la lista de contactos de la aplicación. Si alguno de esos contactos es un administrador de temi, puede llamar a su temi simplemente haciendo clic en ese contacto, como se muestra en Figura 1.

Figura 1: Seleccionar un contacto de la aplicación de teléfono temi

De esta manera, los usuarios de la aplicación de teléfono además del administrador de temi aún pueden llamar a un robot temi usando este método. Dado que iniciar una llamada con un temi te permite operarlo de forma remota además de ver a través de su cámara y escuchar a través de su micrófono, darle a cualquiera la capacidad de llamar a tu temi podría resultar… problemático. Los creadores de Temi abordan este problema exacto en su página de preguntas frecuentes, como se muestra en Figura 2.

Figura 2: "¿Alguien puede conectarse a mi temi?", De la página de preguntas frecuentes del temi

La primera línea aquí es un poco engañosa, ya que agregar un administrador de temi como contacto telefónico parece ser suficiente para llamar a ese temi; no es necesario que el administrador también te agregue como contacto telefónico para que esto funcione. Dicho esto, esto no significa que cualquiera pueda empezar a controlar tu temi; Se requiere un "permiso físico del lado del robot" para las llamadas realizadas por usuarios que no son administradores de temi o que no están autorizados explícitamente por dicho administrador. En la práctica, esto corresponde a una notificación de llamada en la pantalla del temi que el usuario puede responder o rechazar. En otras palabras, no es ni más ni menos "seguro" que recibir llamadas en frío en su teléfono celular.

En cuanto a la última línea, que se refiere a la capacidad de un administrador para otorgar a ciertos usuarios la opción de "saltar" al robot, esto también se hace a través de la aplicación del teléfono simplemente seleccionando la opción "Invitar a un nuevo miembro" en la entrada de contacto del temi. en la aplicación y, luego, seleccionar uno o más usuarios de la lista de contactos de la aplicación para "invitar", como se muestra en figura 3.

Figura 3: Cómo otorgar permisos especiales a los usuarios desde la aplicación del teléfono

Una vez que se ha establecido una llamada entre el robot y un usuario del teléfono, que cualquiera de las partes puede iniciar, el usuario del teléfono puede hacer cosas como conducir manualmente el robot, agregar, eliminar y navegar a ubicaciones guardadas, patrullar entre ubicaciones guardadas, controlar su volumen y más.

Figura 4: Conduciendo el temi usando la aplicación del teléfono

Debido al nivel de control otorgado a las personas que llaman de temi, la funcionalidad de llamada se convirtió rápidamente en una prioridad durante nuestra investigación del robot temi.

Reconocimiento inicial

Escaneo de puertos

Si bien un robot era ciertamente un poco diferente de nuestros objetivos típicos, comenzamos nuestro reconocimiento con métodos que han resistido la prueba del tiempo: escaneos de puertos, capturas de paquetes y un shell local.

Figura 5: Ejecutando Nmap en el temi

Un escaneo de Nmap reveló solo un puerto abierto: el puerto TCP 4443. Inmediatamente supimos que la clasificación de Nmap de este puerto que se usaba para Pharos, una "solución de impresión de oficina segura para su negocio", era casi con certeza incorrecta. En cambio, era probable que este puerto se estuviera utilizando para la comunicación TLS como una alternativa al estándar 443. Si bien es bueno saberlo, esto no nos dijo mucho sobre el tipo de tráfico que se esperaba en este puerto, o incluso qué servicio ( s) estaba manejando ese tráfico, así que seguimos adelante.

Captura de tráfico

Lo siguiente en nuestra lista de verificación fue obtener algunas capturas del tráfico de red del robot, centrándose en el tráfico generado durante el arranque, durante una actualización y durante una videollamada.

El tráfico capturado cuando temi se estaba iniciando pero, por lo demás, inactivo, mostró que se accedía a tres direcciones IP externas únicas: 98.137.246.7, 54.85.186.18 y 34.206.180.208.

Corriendo nslookup en estas direcciones IP reveló que la primera apuntaba a algunos Yahoo! servidor de medios, probablemente utilizado para su aplicación de noticias, mientras que los otros dos parecían ser instancias de AWS.

Figura 6: Ejecución de nslookup en IP a las que accede temi

En cuanto a los datos enviados / recibidos, no había mucho que ver. El Yahoo! se estaba accediendo a la dirección a través de HTTP (puerto 80), pero los paquetes TCP no tenían carga útil. En cuanto a las direcciones de AWS, se accede a ellas a través de TLS (puerto 443) y los datos se cifran.

Para obtener actualizaciones, temi accedió a “temi-ota-updates.s3-accelerate.amazonaws.com”, que casi con certeza era una instancia de AWS personalizada configurada por la gente de Robotemi. Al igual que con el resto del tráfico a AWS, las actualizaciones se cifraron.

Conseguir un caparazón

Para profundizar más, necesitábamos un caparazón local en nuestro temi. Afortunadamente, las conexiones inalámbricas Puente de depuración de Android o ADB, podría habilitarse a través de la configuración de temi desde su pantalla táctil. Mejor aún, el shell proporcionado a través de ADB tenía privilegios de root, aunque el dispositivo en sí no estaba "rooteado". Esto probablemente se hizo para ayudar a facilitar los "desarrolladores de temi", ya que el sitio web de temi ha un portal completo dedicado a ayudar a los usuarios a desarrollar aplicaciones personalizadas para su robot.

Desafortunadamente, el shell predeterminado otorgado a través de ADB fue bastante reducido, y cada reinicio significaría tener que volver a abrir manualmente el puerto ADB de temi desde su pantalla táctil antes de poder conectarse nuevamente. Además, este método requería que el software temi se estuviera ejecutando para poder acceder a nuestro shell, lo que nos dejaría sin recurso si nuestro pinchazo de alguna manera bloqueara ese software. Esto estaba lejos de ser ideal.

Mientras que los comandos como adb push hizo binarios ARM personalizados en movimiento para bash, caja ocupada, y ssh en temi bastante trivial, conseguir que cualquier cosa se ejecute en el arranque resultó más difícil. Para empezar, temi no tenía el directorio estándar /etc/init.d/. Además, los scripts principales que se ejecutarían en el arranque, como /init.rc, se cargarían directamente desde la imagen de arranque, lo que significa que las ediciones realizadas en ellos no persistirían durante los reinicios. Probablemente, esto se debió al diseño como parte del modelo de seguridad SELinux de Android; tendríamos que ser un poco más creativos si quisiéramos convencer a temi de que iniciara un servidor SSH en el arranque.

Sin embargo, hurgar en el contenido de /init.rc nos dio una pista:

Figura 7: Entrada interesante en /init.rc

Para aquellos que no estén familiarizados con el lenguaje Init de Android,

“El lenguaje init se usa en archivos de texto sin formato que tienen la extensión de archivo .rc. Por lo general, hay varios de estos en varias ubicaciones del sistema, que se describen a continuación.

/init.rc es el archivo .rc principal y el ejecutable init lo carga al comienzo de su ejecución. Es responsable de la configuración inicial del sistema ". – Documentos de Android.

La entrada que se muestra en Figura 7, uno de los cientos contenidos en init.rc de temi, le dice al sistema que inicie el servicio "flash_recovery" durante el arranque con el argumento "/system/bin/install-recovery.sh". La "clase principal" es solo una etiqueta que se usa para agrupar entradas, y "one-shot" solo significa "no reiniciar el servicio cuando salga". Esta entrada nos llamó la atención porque invocaba un script ubicado en / system / bin /, que era no cargado desde la imagen de arranque, lo que significa que cualquier cambio debería mantenerse. Si bien la partición / system es de solo lectura de forma predeterminada, nuestros privilegios de root hicieron que fuera trivial volver a montar / system temporalmente como lectura-escritura para agregar la siguiente línea al final: “/ system / bin / sshd”. Con eso, teníamos un medio confiable de obtener un shell raíz en nuestro temi para explorar más a fondo.

Encontrar el código de temi

Lo primero que hicimos con nuestra nueva libertad fue ejecutar netstat:

Figura 8: Ejecución de netstat

Parecía que la mayoría de las redes, incluido el puerto abierto 4443 que encontramos usando Nmap anteriormente, estaba siendo manejada por “com.roboteam.teamy.usa”. Basado solo en el nombre, este era probablemente el software temi principal que se ejecutaba en el robot. Además, el nombre se parecía más al nombre de un paquete de Android que a un binario nativo. Confirmamos esto ejecutando:

Figura 9: Intentando encontrar el binario de com.roboteam.teamy.usa en / proc

app_process32 (o app_process64, según la arquitectura), es el binario usado por Android para ejecutar aplicaciones de Android, lo que significa que estábamos buscando un APK, no un ELF. Bajo esta suposición, en su lugar, intentamos encontrar el código para este proceso utilizando la pm mando:

Figura 10: Buscando el APK de "com.roboteam.teamy.usa"

Efectivamente, teníamos nuestro APK.

Por lo general, cada aplicación instalada en Android tiene un directorio de datos correspondiente, y pudimos encontrar el de este paquete en /data/data/com.roboteam.teamy.usa:

Figura 11: directorio de datos para "com.roboteam.teamy.usa"

El directorio lib / contenía código nativo utilizado por el paquete:

Figura 12: código nativo utilizado por "com.roboteam.teamy.usa"

Después de examinar varias de estas bibliotecas, libagora-rtc-sdk-jni.so en particular se destacó para nosotros ya que era parte de Ágora, una plataforma que proporciona SDK para servicios de voz, video y mensajería en tiempo real (RTM). Es probable que temi estuviera usando Agora para implementar su funcionalidad de videollamadas, la funcionalidad que más nos interesaba.

Al observar las cadenas de este binario, también pudimos determinar que temi estaba usando la versión 2.3.1 del Agora Video SDK:

Figura 13: Encontrar la versión del SDK de Agora mediante cadenas

Por supuesto, estábamos igualmente interesados ​​en el código de la aplicación de teléfono temi, que obtuvimos mediante el uso de Extractor de APK y ADB. También teníamos curiosidad por ver si la aplicación de teléfono Android usaba la misma versión del Agora SDK, que verificamos comparando sus hashes MD5:

Figura 14: Comparación de hashes MD5 para libagora entre el robot temi y la aplicación de teléfono temi

Efectivamente, eran idénticos.

Invertir el código para videollamadas

El siguiente paso fue comenzar a revertir estas aplicaciones para comprender mejor cómo funcionaban. Decidimos comenzar mirando el código de la aplicación del teléfono, ya que era más fácil probar el comportamiento de la aplicación del teléfono en comparación con el robot.

Para descompilar y analizar el APK, utilizamos JADX. Aunque hay muchos descompiladores de Java que funcionan con código de Android, JADX se destacó por sus diversas características:

  • Puede abrir archivos APK directamente (convertirá .dex a .jar y fusionará varios archivos .dex automáticamente)
  • Maneja las características modernas de Java, como las clases anidadas y las funciones lambda en línea, mejor que la mayoría de los otros descompiladores.
  • Para cualquier clase que se esté viendo, puede hacer clic en la pestaña "pequeño" para ver el código pequeño.
  • Muestra números de línea sincronizados con las directivas .line correspondientes en el código de bytes, lo que facilita la asignación del código Java descompilado al código de bytes original.
  • Puede hacer clic con el botón derecho en cualquier método, miembro o clase para ver su uso o declaración

La aplicación de teléfono Android temi, como la mayoría de las aplicaciones de Android no triviales, fue enorme. En lugar de buscar a tientas en la oscuridad y esperar encontrar algún código interesante, decidimos adoptar un enfoque más específico. Desde el principio, sabíamos que nuestro enfoque estaba en la funcionalidad de llamada de temi, ya que esto proporcionaría el mayor impacto para cualquier atacante. Ahora también sabíamos que temi estaba usando el SDK de Agora para implementar esta funcionalidad de llamada. Después de mirar a través Referencia de la API de Java de Agora para Android (v2.3.1), decidimos que la función joinChannel () sería un buen lugar para comenzar nuestra investigación:

Figura 15: Documentación de Agora para joinChannel ()

Al abrir libagora-rtc-sdk-jni.so en IDA y observar sus exportaciones, pudimos encontrar una función llamada nativeJoinChannel () en el desplazamiento 0xD4484:

Figura 16: Algunas de las exportaciones de libagora

Usando la función de búsqueda de JADX, encontramos una función con el mismo nombre en el código descompilado, ubicada en la línea 960 en la clase RtcEngineImpl:

Figura 17: Métodos nativos de Agora en RtcEngineImpl

A partir de aquí, comenzamos el tedioso proceso de trabajar hacia atrás para rastrear la cadena de llamadas de las videollamadas hasta sus puntos de entrada. Hicimos esto buscando el método que invocaba nativeJoinChannel (), luego buscando el método que invocaba ese método, y así sucesivamente. El retorno de la inversión de nuestros incansables esfuerzos fue el siguiente:

Figura 18: Diagrama de flujo de código para las diversas formas de realizar videollamadas con la aplicación de teléfono temi

En un nivel alto, el código para las llamadas salientes tiene cuatro puntos de entrada, y estos se asignan 1 a 1 con las cuatro formas de iniciar una llamada desde la aplicación de teléfono temi:

  1. Seleccione un contacto y presione "Llamar". Esto llamará directamente al teléfono de ese contacto, no a su temi. Esto corresponde a la ruta del código "Detalles de contacto → Llamada de contacto" que se describe en el gráfico (región verde).
  2. Seleccione un contacto y, si tiene un robot temi, presione "Conectar". Esto llamará a su temi y corresponde a la ruta del código "Detalles de contacto → Llamada de robot" que se describe en el gráfico (región naranja).
  3. Vaya a la pestaña "Recientes" de la aplicación y seleccione uno de los contactos / robots a los que ha llamado o le ha llamado recientemente. Esto llamará a ese contacto o robot y corresponde a la ruta del código "Llamadas recientes → Llamada" que se describe en el gráfico (región azul).
  4. Si es un administrador, seleccione su temi debajo del encabezado "Mi temi" en la pestaña "Contactos" y presione el botón "Conectar" en la siguiente pantalla, también conocida como la pantalla Detalles del robot. Esto llamará a su temi y corresponde a la ruta del código “Detalles del robot → Llamar” que se describe en el gráfico (región roja).

Las clases y métodos contenidos dentro de las regiones coloreadas manejan la representación de las pantallas desde las cuales se pueden iniciar las llamadas y la vinculación de estos botones al código que realmente maneja las llamadas. Independientemente del punto de entrada, todas las llamadas salientes convergen en TelepresenceService.initiateCall (), por lo que vale la pena analizar más de cerca este método:

Figura 19: TelepresenceService.initiateCall ()

Y aquí están las cuatro formas en que se invoca:

Detalles de contacto → Llamada de contacto

Figura 20: Invocación de initiateCall () desde Detalles de contacto → Llamada de contacto

Detalles de contacto → Llamada de robot

Figura 21: Invocación de initiateCall () desde Detalles de contacto → Llamada de robot

Llamadas recientes → Llamar

Figura 22: Invocación de initiateCall () desde Llamadas recientes → Llamada

Detalles del robot → Llamar

Figura 23: Invocación de initiateCall () desde Detalles del robot → Llamada

El primer parámetro es un ID que se utiliza para identificar al destinatario. Para los robots temi, esto parece ser algo llamado "ID de robot", mientras que para los contactos se obtiene mediante una llamada a getContactId (). Curiosamente, para llamadas recientes, que puede ser un contacto o un robot, la identificación se obtiene mediante una llamada a getMd5PhoneNumber (), lo cual es un poco extraño ya que temi no tiene (hasta donde sabemos) un número de teléfono. Ampliaremos esto más adelante.

El segundo parámetro es una cadena que indica el tipo de la persona que llama. Dado que aquí estábamos mirando exclusivamente el código de la aplicación de teléfono, asumimos que "typeUser" denota una persona que llama utilizando la aplicación de teléfono.

El tercer parámetro es el nombre para mostrar del destinatario, bastante sencillo.

El cuarto y último parámetro es de un tipo de enumeración personalizado que denota el destinatario tipo. Si llama a un temi, su tipo es CallContactType.TEMI_CALL; de lo contrario, es CallContactType.CONTACT_CALL. Tenga en cuenta que para el punto de entrada Llamadas recientes, este valor se obtiene de forma dinámica, lo que tiene sentido ya que esta ruta maneja las llamadas tanto a contactos como a temis.

También vale la pena señalar que initiateCall () invoca InvitationManager.createInvitation () para crear un objeto de invitación que representa la "invitación de llamada" que se enviará al destinatario:

Figura 24: InvitationManager.createInvitation ()

A partir de ahí, como se ve en la línea 223 de Figura 19, initiateCall () pasa el resultado de Utils.getRandomSessionId () como el primer parámetro para createInvitation (), que se convierte en sessionId:

Figura 25: Utils.getRandomSessionId ()

Todo lo que está haciendo es generar aleatoriamente un número entero entre 100,000 y 999,999, inclusive. En otras palabras, sessionId es siempre un número decimal positivo de seis dígitos.

Al rastrear este valor en toda la cadena de llamadas hasta el nativeJoinChannel () de Agora, descubrimos que este sessionId se convierte en el parámetro "channelName" descrito en Figura 15 – el identificador único de la sala de chat para esa llamada en particular. La propagación de este nombre de canal se puede ver en Figura 18; el negrita El parámetro en cada llamada al método contiene el nombre del canal.

Fuerza bruta del nombre del canal como vector de ataque

Entonces, ¿por qué estábamos tan interesados ​​en el nombre del canal Agora? Bueno, si miramos hacia atrás Figura 15, podemos ver que es uno de los dos campos necesarios para unirse a un canal, ya que el tercer y cuarto campo están etiquetados como opcionales. El único otro campo obligatorio es el "token", pero una mirada más detallada a la documentación revela que "en la mayoría de las circunstancias, el ID de aplicación estático es suficiente", y en esos casos el token "es opcional y se puede establecer como nulo":

Figura 26: Documentación de Agora para el parámetro "token" de joinChannel

Nuestra siguiente pregunta fue, ¿temi usa un token o usa una ID de aplicación estática? Rápidamente respondimos esa pregunta mirando cómo la aplicación de teléfono temi llamaba a joinChannel ():

Figura 27: Se llama a la función de API joinChannel () de Agora desde AgoraEngine.joinChannel ()

Efectivamente, el parámetro token se establece en nulo.

Esto comenzaba a parecer prometedor: si la ID de la aplicación estática y el nombre del canal son todo lo que se necesita para unirse a una videollamada de Agora y ya sabíamos que los nombres de los canales de temi están restringidos a valores de seis dígitos, entonces podría ser plausible unirse a un temi existente. llamadas a través de medios de fuerza bruta. Todo lo que necesitaríamos es este "ID de aplicación".

Mirando hacia atrás en los documentos de Agora, buscamos qué funciones de API realmente usan este ID de aplicación. Al final resultó que, solo había uno: RtcEngine.create ().

Figura 28: Documentación de Agora para RtcEngine.create ()

En pocas palabras, el ID de la aplicación es un valor estático emitido a los desarrolladores que es único para cada proyecto y se utiliza como una especie de espacio de nombres, separando a los diferentes usuarios de los servidores de Agora. Esto asegura que los usuarios de temi solo puedan usar Agora para llamar a otros usuarios de temi. Dado que cualquier usuario de la aplicación de teléfono temi debería poder llamar a cualquier temi (u otro usuario), debería haber una única ID de aplicación compartida por todos los robots temi. Decidimos echar un vistazo a cómo el código de temi estaba invocando RtcEngine.create () para ver si podíamos rastrear el ID de la aplicación:

Figura 29: AgoraEngine.ensureRtcEngineReadyLock ()

Bueno, eso no es bueno.

En nuestra opinión, esto ya era una vulnerabilidad, denotada por CVE-2020-16170 y con una puntuación CVSS de 8.2. Un atacante dedicado no tendría problemas para iterar sobre los 900.000 posibles nombres de canal. Peor aún, al hacerlo, el atacante “rociar y rezar”, lo que le permitirá conectarse a cualquier llamada temi en curso sin necesidad de saber nada sobre sus víctimas.

Si bien ciertamente no pudimos probar un exploit de este tipo contra un servidor de producción, decidimos probar si un atacante podía unirse a una llamada temi existente conociendo solo el ID de la aplicación y el nombre del canal de antemano. Para facilitar esto, usamos un teléfono Android para llamar a nuestro temi, asegurándonos de ejecutar logcat en el teléfono antes de que comenzara la llamada. Al hacerlo, pudimos capturar el mensaje de invitación que contenía el nombre del canal (etiquetado como "sessionId") mientras se enviaba al OkHttp cliente, que luego lo registró:

Figura 30: Encontrar el nombre del canal para la llamada usando logcat

Usando el ID de la aplicación codificado y el nombre del canal obtenido de los registros, pudimos unirnos con éxito a una llamada en curso y obtener audio y video de al menos una de las otras personas que llaman, lo que demuestra que este es un vector de ataque viable.

Aunque la explotación de esta vulnerabilidad utiliza la API de videollamadas de Agora, su existencia es el resultado de la implementación específica del SDK por parte de temi. Si bien la decisión de codificar la ID de la aplicación Agora de temi en la aplicación del teléfono es la causa principal de esta vulnerabilidad, su impacto también podría haberse mitigado sustancialmente mediante la utilización de un token o al permitir una gama más amplia de nombres de canales. Cualquiera de estos habría hecho que el vector de ataque de fuerza bruta fuera increíblemente difícil, si no imposible.

Exploración de vectores de ataque MQTT

Visión general

Fuera de un método de fuerza bruta que implica unirse a llamadas temi existentes probando todos los ID de canal posibles, sería útil tener un vector de ataque más específico. Además, si bien unirse a una llamada utilizando el método de fuerza bruta permitiría a un atacante espiar a los usuarios, no les otorgaría el control del robot en sí; queríamos un método que nos permitiera hacer ambas cosas. Afortunadamente, este nivel de control ya está disponible durante el funcionamiento normal de la aplicación temi robot + phone.

Aunque temi usa Agora para facilitar las videollamadas, notificar a los usuarios de las llamadas entrantes, a través del timbre o de otro modo, no es una función implementada por Agora. En el caso de temi, esta funcionalidad se implementa usando MQTT. MQTT es un protocolo de conectividad de publicación / suscripción (pub / sub) diseñado para la comunicación "máquina a máquina (M2M) / Internet de las cosas". En él, las comunicaciones se clasifican en "temas" y los clientes pueden "suscribirse" a estos temas para recibir todos sus mensajes relacionados, "publicar" sus propios mensajes sobre estos temas, o ambos. Estos temas están estructurados en una jerarquía delineada de una manera muy similar a los esquemas de nomenclatura de sistemas de archivos de estilo UNIX, con una barra inclinada ("/") que separa los diferentes "niveles" de temas. La comunicación entre clientes se facilita a través de un sistema conocido como "corredor“, Generalmente implementado como un servidor remoto, que se encarga de establecer y cerrar conexiones, entrega de mensajes y autenticación de clientes.

Desafortunadamente, había varios obstáculos que tendríamos que superar para aprovechar la funcionalidad de llamada de temi como un "atacante" no autorizado.

En primer lugar, un método de ataque dirigido requiere, como mínimo, una forma fiable de identificar de forma única al objetivo. En otras palabras, si está interesado en piratear el temi de Bob, necesita una forma de distinguir el temi de Bob de todos los demás. Un requisito más suave pero igualmente importante es que este identificador debe ser uno que un atacante pueda obtener de manera plausible. Cuanto más difícil es obtener el identificador, más artificial e irreal se vuelve cualquier vector de ataque que se base en este identificador. Por ejemplo, un nombre o un número de teléfono podría ser un identificador plausible; un número de seguridad social, menos.

La inversión de la implementación de MQTT de temi en el código de su aplicación telefónica reveló un identificador potencial prometedor. Descubrimos que cada robot tiene sus propios temas MQTT que escucha, que se identifican mediante su ID de cliente MQTT, también conocido como su ID de robot. La identificación del robot para un temi se puede obtener fácilmente a través del funcionamiento normal de la aplicación del teléfono simplemente agregando a uno de sus usuarios como contacto telefónico, un método que describimos anteriormente. Esto se debe a que la aplicación de teléfono temi permite al usuario llamar a cualquier temi registrado en un número de teléfono en la lista de contactos del usuario, lo que requiere la identificación del robot de ese temi. Si la aplicación del teléfono ya tiene acceso a esta información localmente, entonces debería ser hipotéticamente posible extraer esta información de la aplicación.

En segundo lugar, necesitábamos una forma de comunicarnos con temi. Si las llamadas se facilitan mediante la publicación de mensajes MQTT sobre ciertos temas, necesitábamos encontrar una manera de publicar nuestros propios mensajes sobre estos mismos temas. En esa nota, si todos los mensajes MQTT deben pasar primero por el corredor, necesitábamos una forma de engañar al corredor para que pensara que éramos un cliente MQTT confiable y omitir cualquier autenticación que pudiera existir.

Una forma de superar este obstáculo sería alterar la aplicación de teléfono temi existente. Modificar aplicaciones de Android de terceros es un proceso bien conocido y nos permitiría aprovechar el código ya presente para enviar mensajes MQTT en lugar de escribir este código desde cero. Además, dado que la aplicación del teléfono no requiere más que el número de teléfono de uno de los usuarios de temi para llamarlo (y, por lo tanto, publicar mensajes MQTT sobre sus temas), esto significa que la aplicación del teléfono debe tener una forma de autenticarse con el corredor. . Si pudiéramos enviar mensajes MQTT personalizados a temas arbitrarios desde el contexto de una aplicación de teléfono "confiable", entonces probablemente no tendríamos que preocuparnos por la autenticación.

Esto nos dejó con nuestro tercer y último obstáculo: la escalada de privilegios. Aunque llamar en frío a un robot temi no es difícil de lograr, no nos otorga la capacidad de operar el robot de forma remota por sí solo. Esto se debe a que llamar a temi de esta manera hace que suene y requiere que la llamada sea aceptada en el extremo de temi a través de su pantalla táctil. Solo el administrador de temi, el usuario que lo registró a través del escaneo del código QR y los usuarios privilegiados seleccionados manualmente por este administrador pueden controlar directamente temi sin ninguna interacción del usuario en el otro extremo. Por lo tanto, necesitábamos encontrar una manera de aumentar nuestro privilegio para que Temi contestara nuestras llamadas automáticamente.

Si MQTT es el medio principal de comunicación entre temi y los usuarios de su aplicación de teléfono, y los administradores pueden administrar los niveles de privilegios de los usuarios directamente desde la aplicación de teléfono, es lógico que la administración de privilegios se realice a través de MQTT. Por lo tanto, si pudiéramos alterar la aplicación del teléfono para falsificar un mensaje MQTT de escalada de privilegios, también podríamos superar este obstáculo.

Modificación de la aplicación temi Phone

Dado que todo nuestro plan dependía de que pudiéramos alterar el código de bytes de la aplicación del teléfono sin romper la aplicación o evitar que se autenticara con los diversos servidores remotos con los que se comunica, decidimos primero confirmar que esto era posible. Para lograr esto, usamos una combinación de ADB, Apktool, Herramienta clavey Jarsigner.

Comenzamos desempaquetando el archivo APK para la aplicación de teléfono temi usando Apktool:

Figura 31: Desembalaje de la aplicación de teléfono temi

A continuación, buscamos en el APK descomprimido el archivo o código que queríamos modificar. Para nuestra prueba de concepto, decidimos simplemente cambiar la etiqueta del botón "Llamar", ya que sería inmediatamente obvio si funcionaba o no. En muchas aplicaciones de Android, las cadenas que se utilizan para los botones no suelen estar codificadas y, en cambio, se cargan desde un archivo de recursos. En este caso, se cargaron desde el archivo res / values ​​/ strings.xml:

Figura 32: Búsqueda de la etiqueta del botón de llamada en strings.xml

Parecía que la línea 108 contenía la etiqueta que queríamos cambiar. Simplemente reemplazamos "Llamar" con "PWN" y guardamos nuestros cambios.

Nota: Para modificaciones menos triviales, como las que tendríamos que hacer más adelante, este proceso naturalmente sería más complicado. Dado que incluso los descompiladores de Java más sofisticados no pueden producir código que realmente se compile para aplicaciones no triviales, las modificaciones significativas generalmente significan tener que leer y modificar smalli, el ensamblador del código de bytes Dalvik de Android. That being said, we found that the best approach to making meaningful changes to a complex app like temi’s is to read Java, write smali. By this, we mean that it’s better to do your reversing on decompiled Java code and only look at the nigh-hieroglyphic smali code once you know exactly what changes you want to make and what class to make it in.

Once our modification had been made, we used Apktool again to repack the APK:

Figure 33: Repacking our modified app

Our next step was to sign the modified APK. This is because Android refuses to install any unsigned APKs, even through ADB. First, we generated a key using Keytool:

Figure 34: Generating the key we will use to sign the modified app

and then we used our key to sign the APK using Jarsigner:

Figure 35: Signing the modified app

Finally, we installed the modified APK onto an Android phone using ADB:

Figure 36: Installing the modified app

After launching the app and selecting one of our contacts, we were greeted with a pleasant sight:

Figure 37: Testing the modified app

Furthermore, the change we made did not seem to impact the app’s functionality; we could still make and receive calls, add contacts, etc.

In our view, this was a vulnerability, later denoted by CVE-2020-16168 and having a CVSS score of 6.5. An altered and potentially malicious app could still access the various cloud resources and user information made available to the temi app because no integrity checks were performed by either the app or the remote servers to ensure that the app had not been modified. As we’ll demonstrate later, the presence of this vulnerability made various other attack vectors possible.

The Relationship Between Robot IDs and MQTT Topics

In the overview section, we made the claim that “each robot has its own MQTT topics that it listens on, which are identified using temi’s MQTT client ID, otherwise known as its robot ID.” Here we will outline how we came to this conclusion and the specific format of a few of the topics that temi and its phone app subscribe/publish to.

Since we knew that temi uses MQTT to handle calling notifications, a natural place to start our investigation was the code used by the phone app to initiate calls. Moving down the call chain, we saw that the robot ID was being passed to the sendInitInvitation() method of the TelepresenceService class:

Figure 38: InvitationManagerImpl.sendInitInvitation()

Here, the robot ID is used in two different places. On line 82, an MqttDelegateApi.Topic object is created with the name “users//status”, implying that each robot has its own MQTT topic category for its status and the robot ID itself is used to uniquely identify these topics. Next, on line 87, we see one of many RxJava functions (the others have been omitted) with the robot ID passed as a parameter to it. This function’s only purpose is to call InvitationManagerImpl.sendInviteMsg(), passing along the Invitation and the robot ID as its arguments:

Figure 39: InvitationManagerImpl.sendInviteMsg()

This function is of particular interest because we see the construction of another MQTT topic name on line 332, this time taking the form “client//invite”. Presumably, this is the topic format used when publishing call invitations to specific temi robots (and, likely, phone contacts).

Additionally, the anonymous function executed via doOnSuccess() uses MqttManager.publish() (line 359) to actually publish the call invitation on the callee’s call invite topic. This information would become useful when we later tried to send custom MQTT messages to our temi in order to facilitate privilege escalation.

How are MQTT Call Invite Messages Published?

If we were to publish our own MQTT messages, we would need a robust understanding of the code used by the temi phone app to publish arbitrary MQTT messages, which appears to be this MqttManagerImpl.publish() method.

To determine what was actually being passed to publish(), we needed to go through what each of the RxJava functions in InvitationManagerImpl.sendInviteMsg() was doing. Referring back to Figure 39:

  • On lines 331-339, Single.zip() is called, which simply creates a Pair from the MQTT topic string (“client//invite”) and the Invitation object.
  • On lines 340-355, Single.flatMap() is called, which adds a timestamp to the Invitation object inside the Pair.
  • Presuming successful execution of the previous flatMap() call, Single.doOnSuccess() is called on lines 356-372. As mentioned previously, this is where the call to MqttManagerImpl.publish() occurs. Since this doOnSuccess() operates on the value returned by the previous call to flatMap(), the arguments being passed to publish() are:
Argument Descripción
(String) pair.first The MQTT topic string
new Gson().toJson((Invitation) pair.second) The Invitation object as JSON
0 The integer “0”
false The boolean “false”

While it was obvious that the first argument is the MQTT topic the message is being published on and the second argument is the message itself, it was not immediately obvious what the third and fourth arguments were for. Digging down into the source code of the MQTT package being used (org.eclipse.paho.client.mqttv3) revealed their purpose:

Figure 40: MqttAsyncClient.publish()

After passing through a couple MqttManagerImpl methods, the four arguments listed above become the first four arguments passed to this internal publish() method. The JSON string (second argument), is converted from a string to a byte array in the interim; the rest of the arguments are unchanged.

Knowing this, it was clear that the second argument is indeed the MQTT message, the third argument is the QoS, and the fourth argument is a flag that specifies whether or not the message should be retained. A QoS value of “0” means that the message will be delivered to clients subscribed to the topic at most once. A retained flag of “false” means that new subscribers to the topic will not receive the most recent published message upon subscription.

Intercepting Calls

As we’ve already established, every temi robot has a unique MQTT client ID and many of the topics it subscribes to contain this ID to indicate that they are intended for that specific robot. If users of the temi phone app can receive calls in addition to making them, it stands to reason that they must also have a unique MQTT client ID – a robot ID equivalent. If there was an easy way to discover the client ID of another phone app user, it might be possible to subscribe to their topics and thus receive calls intended for them, making it worthy of investigation.

If we refer back to Figure 22, we saw that if a call is initiated from the Recent Calls screen, a method called getMd5PhoneNumber() is used to obtain the client ID of the callee. While a temi doesn’t have a phone number to speak of, we began to suspect that the client ID for users of the temi phone app might just be an MD5 hash of their phone number.

Although we could have followed the code in order to track down exactly where this value comes from, we thought it might be easier to simply verify our suspicions. To do this, we first took the MD5 hash of the temi admin’s phone number and then performed a string search for this hash in every temi-related file we had access to.

Figure 41: The Google Voice number used to register with temi

Figure 42: Taking the MD5 hash of the phone number

Sure enough, this hash appeared in two places. First, it appeared in the primary SQLite 3 database file for the temi app running on the robot itself. More specifically, it appears in the “RecentCallModel” table under the “userId” column for the table’s only entry. Based on the table’s name and sole entry, this is almost certainly used to store temi’s recent calls, as its admin was the only user it had called at this point.

Figure 43: The matching string in temi’s RecentCallModel table, part of its SQLite 3 Database

The second match was in the log output we saved after running logcat on our temi earlier, the same log output seen in Figure 30:

Figure 44: Matching strings in the logcat output recorded earlier

This log output appears to conclusively confirm our suspicions. With what we knew now, these log messages appeared to be JSON data being sent to/received from the MQTT broker. Moreover, the “topic” string in the first result exactly matched the MQTT topic format string found on line 82 of Figure 38. The only difference is that in that case, the string between “users/” and “/status” was the robot ID; here, it was an MD5 hash of the user’s phone number.

Since we now knew that

  1. temi robots and phone app users received calls on the topic “client//invite”, where “” is the MQTT client ID of the callee,
  2. the MQTT client ID for phone app users was simply an MD5 hash of the phone number used to register with the app, and
  3. we could alter the existing phone app,

it stood to reason that we could modify the app to subscribe to another user’s call invite topic in order to intercept calls intended for that user as long as we knew the user’s phone number. In theory, this would allow an attacker to effectively impersonate another user and spy on them by intercepting their calls. The question then became: What code needs to be modified in order to get this to happen? Well, we’re looking at a situation where temi initiates a call with its admin and an attacker attempts to intercept that call. In the case of calls initiated by the phone app, we discovered that call invitations are sent via InvitationManagerImpl.sendInviteMsg(), which publishes the call invite message on the topic “client//invite”. We suspected a similar approach was being used when a call is initiated from a temi to a phone user and decided to investigate to confirm.

Luckily for us, the exact same InvitationManagerImpl.sendInviteMsg() method could be found in the temi robot’s code, and it even seemed to function identically. Thus, it was probably safe to assume that the robot initiates calls with phone users in the same way: by publishing a call invitation to the topic “client//invite”, CLIENT_ID being the MQTT client ID of the callee.

If the caller publishes their call invites to a certain MQTT topic, it follows that the callee must subscribe to that same topic to receive the invite. Now that we knew the format of the call invite topic, the next step was to track down the code used by the Android app to subscribe to this topic so we could alter it to subscribe to the temi admin’s topic instead.

Performing a string search for this pattern in the decompiled phone app’s code produced three unique results:

Figure 45: Searching for “client/.+/invite”

The second result we recognized as being part of the code used to generate an outgoing call invitation. Thus, we were only interested in the first and third results.

We began by looking at the first result, found on line 170 of InvitationManagerImpl.java:

Figure 46: InvitationManagerImpl.sendInviteAbortMsg()

This reference is part of the method InvitationManagerImpl.sendInvitationAbortMsg(). Since we were interested in the code that subscribes to the call invite topic and not the code that publishes messages to it, we moved on.

The third result was found on line 523 of MqttManagerImpl.java:

Figure 47: MqttManagerImpl.buildInviteTopic()

This didn’t tell us anything about how the generated topic is used, so we took a step back and looked at what code invokes this method:

Figure 48: MqttManagerImpl.lambda$initMqttClient$13()

The call to buildInviteTopic() can be seen on line 408. There’s a lot going on in this method, but at a high level it appears that it is responsible for setting the callback functions for the MqttManager, which are being defined inline. More specifically, the invite topic string generated by buildInviteTopic() is used in the connectComplete() callback function, where it is passed as the first parameter to MqttManagerImpl.subscribe().

As expected, MqttManager’s subscribe() method is used to a subscribe to a particular MQTT topic, with the first parameter being the topic string:

Figure 49: MqttMangerImpl.subscribe()

Thus, it appeared that we had found the code being used to subscribe to the call invite MQTT topic. Based on these findings, we decided the simplest approach would be to change the call to MqttManagerImpl.subscribe() on line 408 of Figure 48 – instead of passing it the topic string returned by MqttManagerImpl.buildInviteTopic(), we would instead hard-code it to pass the temi admin’s call invite MQTT topic string.

Using this approach, we were able to construct a modified temi phone app that would receive all calls intended for another user, as shown in the following video:

(embed)https://www.youtube.com/watch?v=dyHcxiZ_z9E(/embed)

Here, the vulnerability was the lack of any authentication when publishing or subscribing to arbitrary topics, denoted by CVE-2020-16167 and having a CVSS score of 8.6. At a minimum, a check should have been made to ensure that clients cannot subscribe to another client’s topics.

Problem: temi Won’t Answer My Calls

Our next goal was to gain the ability initiate a call with a temi and have it automatically answer. As mentioned previously, this capability is typically reserved for the temi’s admin and users explicitly authorized by the admin. Thus, if an attacker wishes to spy on a temi user, they would need to trick temi into thinking the call is originating from one of these authorized users.

With no other leads, we began searching through the robot’s codebase for keywords related to user permissions, ownership, admin, etc. until we found a very promising enum class:

Figure 50: Class used for delineating the various roles held by temi’s users

This enum is used in com.roboteam.teamy.users.User, the class used to store information about individual temi users, with User.role being an object of the Role enum class:

Figure 51: Class used to describe individual temi users/contacts

Going back to Figure 50, if we assume that Role.ADMIN refers to the user that originally registered temi and that Role.CONTACT refers to a normal user, then Role.OWNER likely refers to a user that has been authorized to “hop in” to the robot by the admin. To verify this, we looked for places in the code where this enum is used. There are 59 unique references to the Role class, but we’ll only be looking at the pertinent ones here.

The first reference we’ll be looking at appears on lines 351 and 357 of ConferencePresenter.handleViewForCallTypes():

Figure 52: ConferencePresenter.handleViewForCallTypes()

On line 351, a string comparison is performed between this.resourcesLoader.getString(R.string.privacy_support_support_caller_name) and this.callerDisplayName; if the two match, a new User is created with a role of CONTACT. So just what is this string that the ‘if’ statement is checking against? We decided to take a look at where this constant is defined:

Figure 53: Searching for “privacy_support_support_caller_name”

Taken together, this means that if the caller’s display name is exactly “Support”, a new User is created with CONTACT privileges. This check likely exists in the event that a member of temi’s support staff calls a user. While this was certainly interesting, it is a niche scenario.

What happens if the caller’s name is not “Support”? In that case, the else statement on line 352 is hit, which simply sets user to the result of getUserbyPeerId():

Figure 54: ConferencePresenter.getUserByPeerId()

This method tries to obtain the User associated with the current caller by performing a lookup in temi’s UsersRepository using the caller’s MQTT client ID. If the lookup succeeds, the found User is returned; if it fails, a new User is created with Role.CONTACT privileges.

As mentioned previously, Figure 52 contains two references to the Role class. Let’s now look at the second reference, found on line 357. Here, the role of the user is checked. If they are either an ADMIN or an OWNER, temi:

  • waits 1.5 seconds (line 362)
  • checks if the call hasn’t already ended (line 365)
  • if it hasn’t, answers the incoming call (line 370)

Otherwise, the function returns after setting the app’s view for an incoming call.

Solution: Become an OWNER

To recap what we’ve learned thus far:

  • The role of ADMIN appears to be reserved for the user that originally registers temi via QR code scan.
  • If the user calling temi is not recognized, a new User object is created for them with CONTACT privileges.
  • If the user calling temi es recognized, their role is checked:
    • If they are a CONTACT, temi waits for the call to be answered via touchscreen.
    • If they are either an ADMIN or an OWNER, temi answers the call automatically. This is the behavior we want.

Scanning the temi’s QR code for the purposes of registration is only possible when temi is first powered on or after performing a factory reset. Thus, the attacker cannot realistically make themselves an ADMIN. And since CONTACT privileges are insufficient to get temi to pick up automatically, our best bet was to figure out how to obtain the role of OWNER.

Adding an OWNER: Phone App’s Perspective

Although we knew that it was possible to promote a user to an OWNER using the phone app and we suspected that was achieved via MQTT, we still weren’t sure of what goes on “under the hood” to make that happen.

After some searching, we found that AddOwnersPresenter.addOwners() is called whenever an admin selects one or more users to be granted OWNER privileges:

Figure 55: AddOwnersPresenter.addOwners(), trimmed

Here, selectedIds refers to the MQTT client IDs of the users selected to be promoted and robotId refers to the MQTT client ID of the temi robot this method is granting permissions for.

The majority of this method’s body has been trimmed because we’re really only concerned with what’s happening on lines 104-106, which seems to handle sending the request to add an OWNER – the rest of the method is dedicated to logging and updating local databases to reflect the new OWNERs.

This request is sent by first fetching the unique private key used to identify this app’s instance (line 101), and then creating a new AddRemoveOwnersRequest using the selectedIds, robotId, and this private key:

Figure 56: AddRemoveOwnersRequest

The constructor for AddRemoveOwnersRequest creates a timestamp for the request, then creates a new AddOwnersRequestRequest, which contains the body of the request, and finally uses the passed in privateKey in order generate a signature for the AddOwnersRequestRequest. In other words, AddRemoveOwnersRequest is nothing more than a wrapper for the real request and is used to store its signature.

We decided to look at this AddOwnersRequestRequest object next. While the ownerIds, robotId, and timestamp members were mostly self-explanatory, source and type were less so. Looking back at line 8, we saw that source was being set to a hardcoded value of “ADMIN”, which seemed to imply that this was the origin of the request. Looking back at line 6, we saw that type is simply the passed in OwnersRequestType enum (in our case, OWNERS_ADD_TYPE) converted to a string. This enum, defined near the bottom of the class, can take on two values: OWNERS_ADD_TYPE and OWNERS_REMOVE_TYPE. This implied that this same structure was recycled for requests meant to demote OWNERs back to CONTACTs.

Thus, we determined that AddRemoveOwnersRequests had the following structure:

Figure 57: Anatomy of an AddRemoveOwnersRequest

Now that we knew the structure of these requests, we next wanted to know how they were being sent and where. To this end, we decided to look at OwnersApi.addOwners(), which, according to Figure 55, is actually sending the AddRemoveOwnersRequest.

Figure 58: OwnersApi.addOwners()

The body of this method didn’t tell us much, but the imports for the OwnersApi class gave us a clue: this was using the Retrofit 2 HTTP Client for Android, which is typically used to send HTTP requests to a REST API. De acuerdo a this Retrofit tutorial, the @POST annotation “indicates that we want to execute a POST request when this method is called” and “the argument value for the @POST annotation is the endpoint.” The @Body annotation, on the other hand, indicates that we want the AddRemoveOwnersRequest to serve as the body of the POST request.

Okay, so this method is simply sending the request to add an OWNER via POST to some REST server at the endpoint “ownership/admin/add/owners”. Our next question became: Where is this REST server located?

Part 5 of that same Retrofit tutorial told us that the getClient() method is typically used to obtain/create a Retrofit instance, and the only argument it takes is a string containing the URL for the REST server. Searching for “getClient()” in the phone app’s code led us to ApiClient.getClient():

Figure 59: ApiClient.getClient()

Working backwards from this method, we were able to track down the server’s URL:

Figure 60: URLs for the MQTT broker and REST server

This URL confirmed that the recipient of this request was no the temi robot and it was no being sent via MQTT, contrary to our initial assumptions. This begged the question: If the phone app wasn’t sending these requests to temi, how was temi being notified of any updates to the privileges of its users? We hypothesized that this REST server was simply a middleman whose job was to authenticate all requests to add/remove OWNERs by checking the request’s signature against the admin’s public key that was saved during temi’s initial registration process. This extra level of authentication made sense since privilege management was a particularly sensitive functionality. Presumably, this same REST server would then forward the request to the robot if it determined that the request’s signature was valid.

We took some time trying to send these requests from a user that wasn’t temi’s admin, but they failed, lending some credence to our theory. This was looking like a dead end.

Adding an OWNER: temi’s Perspective

Well, if we couldn’t successfully spoof the request the phone app sends to the REST server, perhaps we could instead spoof the request the server sends to temi, bypassing the authentication mechanism altogether. We started looking for the code temi used to handle these requests.

Searching specifically for “Role.OWNER” in the temi’s decompiled code led us to OwnersController$getUsersRepository$2.apply():

Figure 61: OwnersController$getUsersRepository$2.apply()

Starting with OwnersController$getUsersRepository$2, we moved up the call chain in an attempt to discover how temi processes requests to add OWNERs. More concretely, this was accomplished through a liberal use of the “Find Usage” feature in JADX, which can be done by simply right-clicking virtually any symbol. Although convenient, “Find Usage” would often fail when the method in question was not invoked directly, such as when an implementation of an abstract class/interface served as the middleman. In such cases, we would perform a string search for instances where the method was invoked in the smali code. To help separate invocations from declarations, we took advantage of the smali syntax for method calls, which consisted of the name of the class the method belonged to, followed by a right arrow (->), followed by the method’s signature.

As an example, “Find Usage” failed for the accept() method of the class UsersAdminTopicListener$listenToUserAdminTopic$1, so to find where it’s invoked, we ran:

Figure 62: Searching for UsersAdminTopicListener$listenToUserAdminTopic$1.accept() in the smali code

For especially indirect invocations, like dependency injection, more creative means had to be used, but a combination of “Find Usage” and string searches such as these got us 99% of the way there.

Using this approach, we found that the mechanism begins with the UsersAdminTopicListener class, which, as the name suggests, handles listening on temi’s “users admin” topic. Moving down the call chain from here, we found out how messages received on this topic are processed by temi and ultimately used to alter the Role, or privilege level, of certain contacts. Based on our earlier analysis, it was likely that the REST server would forward the request sent by the phone app to this MQTT topic.

We found that the bulk of the work is performed by a method called listenToUserAdminTopic():

Figure 63: UsersAdminTopicListener.listenToUserAdminTopic()

This function does several things. First, on line 50, it creates a Flowable object by calling UsersAdminTopicListener.getOwnerRequestFlowable(). Next, on lines 51-56, it subscribes to this Flowable and for each emitted OwnersRequest, it calls OwnersController.handle$app_usaDemoRelease() upon success or simply logs upon failure.

We decided to first look at the code for getOwnerRequestFlowable():

Figure 64: UsersAdminTopicListener.getOwnerRequestFlowable()

  • It begins by first converting an Observable obtained via mqttPipeline.observe() into a Flowable.
  • Next, it throws out all emitted MqttMessages whose topic doesn’t match the regex “users/.+/admin” via RxJava’s filter() method.
  • RxJava’s map() method is then used convert the body of the MqttMessage from JSON to an object of the OwnersMessage
  • Finally, map() is used a second time to extract and return the OwnersRequest from each OwnersMessage.

At this point, we decided it would be useful to understand the structure of OwnersRequests and OwnersMessages, since these seem to be key to temi’s privilege management mechanisms:

Figure 65: Anatomy of an OwnersMessage

Put briefly, each OwnersMessage is nothing more than a wrapper for an OwnersRequest, which consists of ownerIds, a list of the MQTT client IDs of the users whose privileges are being modified, and type, which indicates whether the request is trying to promote a CONTACT to an OWNER (OWNERS_ADD) or demote an OWNER back to a CONTACT (OWNERS_REMOVE).

Comparing this to Figure 57, an OwnersMessage appears to be an AddRemoveOwnersRequest without the signature. Similarly, an OwnersRequest appears to be a stripped-down AddOwnersRequestRequest, with the robotId, source, and timestamp omitted. This meshes well with our earlier hypothesis that the REST server’s job is to authenticate and forward AddRemoveOwnersRequests to the temi robot. The signature, timestamp, and source would be omitted since they’ve already been verified by the server; the robotId, while needed by the server to know where to forward the request, becomes redundant once the request reaches temi.

Our next step was to figure out what handle$app_usaDemoRelease() does. Since it’s quite the rabbit hole, however, we will summarize its effects in lieu of venturing down it:

  1. It queries the Users table in temi’s local database for all users with IDs matching any of the ones in the OwnersRequest’s ownerIds
  2. It replaces the Role for each of these users with one corresponding to the OwnersRequest’s type: OWNERS_ADD→ OWNER, OWNERS_REMOVE → CONTACT.
  3. It updates temi’s Users table with the updated user information.

This was promising, since it meant that we could potentially trick temi into promoting an arbitrary user/contact to an OWNER simply by crafting a custom OwnersRequest and publishing it on temi’s “users//admin” topic, thereby bypassing the authentication server entirely.

Unfortunately, part 1 above reveals a potential obstacle for this plan: since temi will only process OwnersRequests for users already present in its local Users table, we must first add ourselves to this table for this strategy to succeed. Recalling our earlier analysis of how temi handles unrecognized callers, one way of accomplishing this was to simply cold call temi, which would cause it to automatically add the caller to its contacts list. This was far from ideal, however, since cold calling  temi without already having the role of OWNER or ADMIN would cause it to ring and display the caller’s username on its screen, potentially alerting temi’s users that something weird is going on.

Detour: Sneaking Onto temi’s Contact List

Before continuing on, we decided to take a brief detour to find a better way for an attacker to add themselves to temi’s contact list.

From our prior investigation into how temi implements its various privilege levels through the use of Roles, we discovered that temi uses the User class to define its various users. Thus, it follows that any code used to add a new user to temi’s local contact list would first create a new User object, so that’s exactly what we searched for.

Figure 66: Searching temi’s code for “new User(“, trimmed

Figure 66 shows the result that we followed up on, the others have been omitted. The name of the containing class, SyncContactsController, sounded promising on its own since syncing contacts from temi’s ADMIN would likely involve adding new contacts without needing to start a call, which was exactly what we were trying to do.

Using largely the same strategy we employed for tracing the code flow for adding OWNERs (JADX’s “Find Usage” feature + grepping the smali code), we were able to trace the code flow all the way back to the app’s entry point. With a more holistic understanding of the mechanism used to sync contacts, we realized that the process is ultimately kicked off by SyncContactsTopicListener.subscribeMqttPipeline():

Figure 67: SyncContactsTopicListener.subscribeMqttPipeline()

The first thing this method does is take this.mqttPipeline and turns it first into an Observable (using MqttPipeline.observe()) and then into a Flowable (using RxJavaInterop.toV2Flowable()), as seen on lines 29 and 30.

Essentially, this.mqttPipeline acts as a relay. Incoming MQTT messages are pushed to the relay using its push() method, which it then relays to all its observers.

The output of this relay is then filtered on lines 31-38 to only return MQTT messages received on topics matching the regex “synccontacts/.+”. Based on the other MQTT topic strings we’ve seen up to this point –

  • “users//status”
  • “users//admin”
  • “client//invite”

– we were fairly certain temi’s client ID goes after the forward slash. Thus, temi appeared to be listening on the MQTT topic “synccontacts/” for messages regarding contact synchronization.

On lines 39-43, the now-filtered MQTT messages emitted by the relay are passed to a call to RxJava’s map() function, which converts each incoming MQTT message from JSON to an object of the SyncContactsMessage class. We quickly determined that SyncContactsMessages had the following structure:


Figure 68: Anatomy of a SyncContactsMessage

Put briefly, each SyncContactsMessage consisted of a senderClientId, a string holding the MQTT client ID of the request’s sender, and contacts, a list of ContactEntry objects. Each ContactEntry object in the list corresponded to a contact to be synced to temi’s contact list, containing both their clientId and their name.

Finally, on lines 45-49, SyncContactsController.save() would be called on each SyncContactsMessage spit out by the prior call to map():

Figure 69: SyncContactsController.save()

This method is doing a lot, but we’ll focus on only the most pertinent side-effects:

  • On lines 53-62, all messages where the senderClientId does not match the temi admin’s client ID are discarded. This will become important later.
  • On lines 63-75, the list of contacts is extracted from the SyncContactsMessage and is used to build a list of User objects – one per contact. The Users produced by this method are initialized in the following manner:
Miembro Assigned Value
User.id ContactEntry.clientId
User.name ContactEntry.name
User.picUrl “”
User.role Role.CONTACT
User.userId SyncContactsMessage.senderClientId
  • On lines 81-85, our newly-minted list of User objects is passed to insertOrUpdateContact(). This method writes the list of Users to the Users table in temi’s SQLite 3 database, completely overwriting temi’s old contacts list in the process.

So now that we knew how an ADMIN’s contacts are synced to their temi, how could we leverage that knowledge to add ourselves to temi’s contact list in a discrete fashion? Well, if we could publish a message to temi’s synccontacts MQTT topic, we could add ourselves as a contact. Although temi does perform a check to make sure that the sender of the message is its ADMIN, it makes the mistake of trusting that the contents of the message accurately reflect the actual sender. In theory, there’s nothing stopping us from publishing a SyncContactsMessage from one client ID and setting the senderClientId field in the message to a completely different ID – the ADMIN’s client ID, for example.

Based on our understanding of how the temi robot parses MQTT requests to sync contacts, we crafted a JSON message that should decode into a valid SyncContactsMessage object and would add our “attack” phone to temi’s contacts:

Figure 70: Our custom SyncContactsMessage in JSON format

This message was crafted using the following rationale:

  1. Objects in JSON are indicated via curly braces (). Since the entire message contents are being converted into a single SyncContactsMessageobject, it follows that the contents should be contained within a pair of curly braces, representing the SyncContactsMessage object itself.
  2. A SyncContactsMessagecontains a senderClientId, a string indicating the client ID of the “sender” of the message. Thus, we added an element to our object with the key “senderClientId” and the string “060f62296e1ab8d0272b623f2f08f915” – the client ID of the temi’s admin – as its value.
  3. A SyncContactsMessagealso contains contacts, a list of ContactEntry Thus, we added an element with the key “contacts” and a new list as its value. In JSON, lists are indicated via square brackets (()).
  4. In our case, the contacts list contains only a single ContactEntry – one corresponding to the “attack” phone, so we added a single pair of curly braces to the list to indicate the contents of this single ContactEntry.
  5. Each ContactEntry contains a clientId. Thus, we added an element to the object with the key “clientId” and the string “fe5d7af42433f0b6fb6875b6d640931b” – the client ID of the “attack” phone – as its value.
  6. Each ContactEntry also contains a name. Thus, we added a second element to the object with the key “name” and the string “Test” as its value. This simply represents the display name for the contact, so we could set it to basically whatever we liked.
  7. As for spacing and newlines, we referred to the Gson User Guide, since that’s the library temi was using to perform JSON serialization/deserialization. The guide states:

“The default JSON output that is provided by Gson is a compact JSON format. This means that there will not be any whitespace in the output JSON structure. Therefore, there will be no whitespace between field names and its value, object fields, and objects within arrays in the JSON output.”

As a result, whitespace and newlines have been omitted.

Now that we understood where to publish the request and what the request should look like, the next step was figuring out how to alter the temi phone app to send this request. Since we were aiming for a simple proof-of-concept, we prioritized ease and speed of implementation over robustness or elegance when considering which code to change. To this end, it made sense to leverage the code already present in the app that was dedicated to publishing MQTT messages since it would minimize the amount of code we needed to change and thus reduce the risk of introducing unintended bugs into our altered app – an ever-present risk when directly modifying an app’s bytecode. While the phone app publishes many MQTT messages to various topics during its runtime, we decided to try using the phone app’s video calling functionality. This had the obvious advantage of giving us clear control over when and how often the MQTT message is published, since calls are always initiated by hitting the “Connect” or “Call” buttons. Ultimately, we decided to leverage InvitationManagerImpl.sendInviteMsg() for this purpose. If we refer back to Figure 39 and our section “How are MQTT Call Invite Messages Published?”, the reason for this becomes apparent: sendInviteMsg() has a direct interface to MqttManagerImpl.publish(), the underlying method temi uses to publish arbitrary MQTT messages, while still being specific to the call chain for outgoing video calls. This meant that it would only get executed unless when we manually initiated a call from the app.

Running our altered app resulted in our attack phone being added to temi’s contact list, as shown in the following video:

(embed)https://www.youtube.com/watch?v=cR0NkxBUisA(/embed)

Gaining OWNER Privileges

All that was left was for us to craft and publish a custom OwnersMessage in order to gain OWNER privileges on our temi. As before, we began by crafting the JSON for the message itself:

Figure 71: Our custom OwnersMessage in JSON format

This message was crafted using the following rationale:

  1. Since the entire message contents are being converted into a single OwnersMessage object, it follows that the contents should be contained within a pair of curly braces, representing the OwnersMessage object itself.
  2. An OwnersMessage contains a request, an object of the OwnersRequest class, and nothing else. Thus, we added an element with the key “request” and a new object as its value. The contents of this inner object will become our OwnersRequest.
  3. An OwnersRequest contains a type, an enum specifying the type of request. In our case we want to add an OWNER, so we added an element with the key “type” and the string “OWNERS_ADD” as its value. As for why we’re expressing this enum value as a string, it’s because this article shows that Gson’s default behavior is to express enum values as strings corresponding to the name of the enum value.
  4. An OwnersRequest also contains ownerIds, a list of strings enumerating the temi contacts the request should apply to. In our case, the list contains only a single ID – the ID of the “attack” phone, which is just the MD5 hash of its phone number.
  5. As before, spaces and newlines have been omitted, per the Gson User Guide.

Fortunately, we were able to leverage the code modifications we used to publish our SyncContactsMessage, since the only things changing were the MQTT topic we’re publishing to and the contents of the message.

Our full test consisted of running our first modified app in order to add ourselves to temi’s contact list, followed by our second modified app in order to perform privilege escalation, as shown in the following video:

(embed)https://www.youtube.com/watch?v=56JoHb4bioI(/embed)

The call appeared to be fully functional, and we were able to drive temi around, navigate to its saved locations, and receive its audio/video feeds. All an attacker needed to make this possible was the phone number of one of temi’s contacts.

This authentication bypass was the final and most impactful vulnerability we discovered in the temi robot, denoted by CVE-2020-16169 and having a CVSS score of 9.4. To better understand how this an auth bypass, let’s compare temi’s privilege management during normal operation:

Figure 72: temi’s privilege management during normal operation

to how it looks using our modified app:

Figure 73: temi’s privilege management with auth bypass

As you can see, although authentication es in place for adding OWNERs, it can be circumvented entirely by simply spoofing the MQTT message temi expects to receive from the REST server post-authentication.

Refining the Exploits

Once our exploits had the capabilities we initially set out to obtain, we got to work refining them. We created a single modified app that combined all of the MQTT attack vectors we previously outlined: it could intercept calls and send both MQTT messages necessary to obtain OWNER privileges, all with a single button press. Furthermore, we added some additional logic to automatically extract the client IDs required by our custom MQTT messages from the app’s contact list, meaning our app would work on any temi robot. To see all these features in action, please refer to the video below:

(embed)https://www.youtube.com/watch?v=_YKs31sotB0(/embed)

Impacto

At this point it would be prudent to take a step back, review the combined capabilities of the exploits we’ve outlined, and consider what impact these might have in a real-world setting.

At the time of discovery, the vulnerabilities in the temi robot meant that an attacker could join any ongoing temi call simply by using a custom Agora app initialized with temi’s hardcoded App ID and iterating over all 900,000 possible channel names – certainly feasible with modern computing power. This becomes more plausible if one considers that our testing revealed that joining a temi call this way does not notify either of the existing call participants that another user is present, since the apps were only designed with 1-on-1 calling in mind. The fact that the attacker needs no information on the victims makes this attack vector worrisome, but it also means that the attacker has no control over who he or she spies on using this method. Realistically, a malicious actor might use this exploit as a means of reconnaissance – by collecting data on arbitrary temi users, they could follow up on the more “promising” ones later with one of the targeted attack vectors. Given temi’s limited adoption by consumers but ramping adoption in industries such as healthcare, the odds are in the attacker’s favor that they stumble upon sensitive information.

Furthermore, if an attacker has a specific victim in mind, they could leverage the lack of per-topic authentication in temi’s MQTT implementation in order to intercept calls between a user and their temi. The plausibility of this particular attack vector is difficult to evaluate. On the one hand, the only information the attacker needs is the victim’s phone number, and telemarketers consistently remind us that this is a very low bar. On the other hand, this exploit’s behavior during our testing proved somewhat inconsistent. At times, the user running the exploit could listen in on the call as a third party with the call participants being none the wiser. At other times, the user running the exploit would disconnect the original caller and take their place. It is easy to imagine scenarios where both of these outcomes are desirable to a malicious actor, and neither bodes well for the privacy of the user being targeted. It is also important to note that although we focused on using this vulnerability to intercept calls, it can also be used to intercept all sorts of notifications intended for another user: contacts being added, when temi goes online/offline, its current location, etc. While less flashy than intercepting a call, this approach is more discreet and might allow a dedicated attacker to learn a user’s routine, only showing their hand to listen in when it would be most impactful.

Of the possible attack vectors, we believe that the ability to call and control a temi remotely, leveraging the authentication bypass present in temi’s privilege management mechanism, is the most impactful. The bar for this attack vector is arguably even lower than the previous, as the attacker would only need the phone number of alguna of temi’s contacts – it need not be its admin. In our testing, none of the steps involved in leveraging this exploit notify temi’s admin in any way that something is amiss; they are not notified that the attacker has added themselves to the robot’s contact list nor that they have gained raised privileges. Since this method does not cause temi to ring, an observer would have to see the attacker move temi or have a good look at its touchscreen during the attack to know something nefarious was going on. This level of control and subtlety becomes especially problematic when we consider some of temi’s applications in industry. In April of this year, Robotemi Global Ltd. stepped up production to 1,000 units per month in response to Israel’s Ministries of Defense and Health choosing temi to assist with patients in their COVID-19 wards. In South Korea, temi sees use in both public places and nursing homes, helping to facilitate social distancing. Besides the obvious impact of compromising patients’ sensitive medical information during remote doctor’s visits, the ability to have eyes and ears into a hospital is worrying in its own right. It isn’t difficult to imagine what a malicious agent might do with an overheard network password, access code to a sensitive area, or the location and condition of a person of interest.

Conclusion

The findings outlined in this paper highlight the importance of secure coding practices and security auditing for cutting edge technologies, particularly in the IoT space. When an IoT camera evolves into one that can also drive around your home or business and even assist medical patients, the need to properly secure who can access it only becomes more important. While history has demonstrated that any sufficiently complex software is bound to have vulnerabilities, there are many steps we can take to substantially raise the bar for bad actors. In our view, these responsibilities are shared between vendors, consumers, and the security industry as a whole.

First and foremost, vendors can ensure they are employing proper security hygiene when designing products. Often, best practices consist not of novel, out-of-the-box thinking, but rather adopting time-tested guidelines like the principle of least privilege. In the case of temi, application of this principle likely would have addressed CVE-2020-16167, which allows clients to subscribe/publish to topics they have no business accessing. Considerations for security should also extend beyond the development phase, with third-party security auditing being a powerful tool that allows vendors to discover and patch vulnerabilities before the bad guys get ahold of them. Taking vulnerability disclosure seriously and cooperating with the bodies that report them can often net similar benefits, and this is an area Robotemi excelled in.

Consumers of these technologies, at either the individual or enterprise level, also share some of the responsibility. Companies should ideally vet all technologies before widespread adoption, particularly if these technologies have access to customer information. It goes without saying that greater risk warrants greater scrutiny. Individuals, while typically having fewer resources, can also take certain precautions. Placing IoT devices on a VLAN, a virtually segregated subnetwork, reduces the risk that a vulnerability in one device will compromise your entire network. Staying up to date on important vulnerabilities can also help inform consumers’ purchasing decisions, although the presence of vulnerabilities is often less illuminating than if/how a company chooses to address them.

Finally, we in the security industry also have an obligation towards moving the needle in the realm of security, one that we hope research such as this embodies. One of our goals here at McAfee ATR is to identify and illuminate a broad spectrum of threats in today’s complex and constantly evolving landscape. While we take seriously our obligation to inform vendors of our findings in a timely and responsible fashion, it is only through cooperation that the best results are possible. Our partnership with Robotemi on addressing these vulnerabilities was a perfect example of this. They responded quickly to our private disclosure report, outlined to us their plans for mitigations and an associated timeline, and maintained a dialogue with us throughout the whole process. We even received feedback that they have further emphasized security in their products by approaching all development discussions with a security-first mindset as a result of this disclosure. The ultimate result is a product that is more secure for all who use it.





Enlace a la noticia original