Control de aplicaciones de Windows Defender (WDAC) es una característica de seguridad de Windows que ayuda a evitar que se ejecute código no autorizado (como malware o ejecutables y scripts no confiables) en un sistema. Es un mecanismo de lista blanca de aplicaciones que aplica políticas que solo permiten ejecutar en un sistema los ejecutables, scripts y controladores explícitamente confiables. Se utiliza con frecuencia en entornos de alta seguridad o estrictamente controlados donde la seguridad y la integridad del sistema son crítico, como los que el equipo de X-Force Red Adversary Simulation está comprometido a probar.
Hace unas semanas, mi colega Bobby Cooke publicó una entrada en el blog que detalla un método para eludir incluso las políticas más estrictas de WDAC mediante la puerta trasera de aplicaciones confiables de Electron. Recomiendo encarecidamente leer su entrada en el blog para tener una idea de cómo las aplicaciones Electron usan Node.js y cómo se pueden hacer backdoors.
Como parte de esa investigación, también hizo código abierto Loki C2, un programa basado en Node.js. marco de mando y control Gracias al excelente trabajo de Bobby y Dylan Tran en el desarrollo de Loki C2, el equipo de X-Force Adversary Simulation ha logrado obtener la ejecución de código en interacciones en entornos reforzados que emplean WDAC.
Entonces, ¿dónde entra esta investigación? La técnica mencionada tiene una deficiencia: está limitada a ejecutar solo código JavaScript y no puede ejecutar código nativo, como cargar archivos DLL o ejecutar EXE. Tampoco puede ejecutar shellcode para lanzar una carga útil C2 de etapa 2. Esta entrada en el blog cubre una técnica que utilizamos para sortear esas restricciones.
Para empezar, Bobby y yo comenzamos a realizar ingeniería inversa de módulos Node.js firmados cargados por aplicaciones Electron, en busca de vulnerabilidades que pudieran garantizar la ejecución de código de bajo nivel y nivel de instrucción. Después de una exploración inicial y por sugerencia de jeffssh, mi atención se centró en el motor V8 utilizado por Node.js y Chrome.
En lugar de encontrar una vulnerabilidad en un módulo Node.js, ¿qué tal explotar el motor V8 con un día N?
El escenario de ataque es familiar: traer un binario vulnerable pero confiable, y abusar del hecho de que es confiable para afianzarse en el sistema. En este caso, usamos una aplicación confiable de Electron con una versión vulnerable de V8, reemplazando main.js con una explotación V8 que ejecuta la etapa 2 como carga útil, y voilà, tenemos la ejecución nativa de shellcode. Si la aplicación explotada está incluida en la lista blanca/firmada por una entidad confiable (como Microsoft) y normalmente se le permitiría ejecutarse bajo la política WDAC empleada, puede usarse como un recipiente para la carga útil maliciosa.
Además de poder ejecutar libremente shellcode, este enfoque también tiene el beneficio de ejecutar shellcode en el contexto de un proceso similar a un navegador, lo cual tiene sus ventajas. Un comportamiento que, de otro modo, la ejecución de la detección y respuesta de endpoints (EDR) podría marcar como sospechoso, parece normal para un navegador, como tener asignada la memoria RWX para el código Just-In-Time (JIT).
Este enfoque parecía bastante sencillo, pero tenía algunas preguntas abiertas. ¿Funcionaría realmente una explotación pública de Chrome V8 N-day dentro de una aplicación Electron? ¿En qué se diferencia el motor V8 que se usa en Chrome respecto al de Node.js? ¿Qué modificaciones necesitará el exploit? ¿Cómo puedo depurar esto?
Resulta que existe un trabajo público sobre la explotación de exploits V8 en las aplicaciones Electron, que, lamentablemente para mí, no encontré hasta después de haber terminado. Turb0 hace un excelente trabajo al cubrir el (algo agonizante) proceso de adaptar un exploit v8 público y sus correspondientes primitivas de lectura/escritura para explotar dentro de una aplicación Electron. La entrada en el blog de Turb0 ya cubre muchos de los detalles técnicos en profundidad de lo que tuve que lidiar, que recomiendo consultar. El resto de esta entrada en el blog se centrará en las etapas restantes del ciclo de desarrollo de la explotación en lo que respecta a apuntar a Windows con el objetivo específico de crear un bypass de WDAC y los problemas que encontré al poner en funcionamiento la explotación para su uso en el mundo real.
Lo primero que tenía que hacer era averiguar los objetivos exactos. Necesitaba elegir una aplicación Electron confiable y elegir una vulnerabilidad para explotarla. Antes de esto, tenía muy poca experiencia en explotación de navegadores, así que la vulnerabilidad elegida debería tener un exploit público como punto de partida.
No estaba seguro de cómo se asignaban las versiones de V8 a la versión de V8 Electron o cómo saber si era realmente vulnerable. La versión de Electron de V8 a menudo va a la zaga de la última versión de Chrome V8. Los mantenedores de Electron trasladan parches de seguridad importantes de versiones más recientes a la versión que congelaron para una versión concreta de Electron. Eso significa que incluso si Electron usa una versión anterior de V8, no significa necesariamente que sea vulnerable a un error, ya que un arreglo podría haber sido retroadaptado. Los parches seleccionados que se aplican están almacenados aquí.
Decidí que lo más fácil sería aprovechar una vulnerabilidad que se corrigió después de lanzar la versión de la aplicación. Así, no habría ninguna posibilidad de que esa versión de la app estuviera ya parchada. Después de investigar un poco, encontré descargas de los últimos casi 2 años de versiones de VSCode. Tenía una buena variedad de aplicaciones vulnerables firmadas por Microsoft entre las que elegir 😊.
Para empezar, simplemente tomé un exploit PoC público reciente de V8 y retrodoté la vulnerable aplicación Electron con él, reemplazando main.js con el exploit, y crucé los dedos. Quizá sería así de fácil, ¿no? Esperaba al menos un accidente. Para sorpresa de nadie, no sucedió nada cuando inicié la aplicación. A regañadientes, sabía que iba a necesitar construir V8 para entender lo que estaba pasando en un nivel más profundo. Al construir V8 yo mismo, podría construir la versión de depuración (d8), entrar en las profundidades del ataque y luego ajustarlo para la versión específica a la que estaba apuntando.
Mi primer objetivo fue establecer una “verdad sobre el terreno”: replicar el entorno exacto donde se sabe que funciona el exploit. Después, podía examinar las diferencias entre esa versión y la versión a la que me dirigía para entender qué estaba fallando.
La mayoría de los exploits públicos de V8 que encontré se dirigieron a Linux. Así que comencé por compilar V8 en Linux, verificando el compromiso exacto al que se dirigía el exploit público que elegí. Luego ejecuté el exploit para asegurarme de que funcionaba. Afortunadamente, así fue. Ahora tenía mi verdad fundamental.
A partir de ahí, compilé la versión de V8 a la que me dirigía (la misma que utiliza la aplicación Electron) pero en Linux. La explotación no funcionó desde el principio. El beneficio de crear un proyecto usted mismo es que puede tener tanta introspección en el código como necesite. En particular, V8 tiene d8, el shell independiente para el motor JavaScript V8, que se utiliza principalmente para probar, depurar y ejecutar código JavaScript y WebAssembly fuera de un navegador o entorno Node.js. d8 tiene funciones de depuración interna habilitadas con el
Con esto, pude imprimir las direcciones de los objetos de interés y ajustar los desplazamientos codificados del exploit público. Ahora estaba llegando a alguna parte. Solo necesitaba trasladar mi exploit a Windows.
Compilar una versión anterior de V8 en Windows me dio muchos dolores de cabeza. Necesitaba solucionar un montón de problemas de dependencias, así que hice algunas modificaciones internas dudosas en el código. Los detalles se me escapan ahora: mi cerebro los ha bloqueado para mi propia protección. Después de horas de lucha, ¡finalmente pude compilar la versión que necesitaba! Para mi sorpresa, el exploit de Linux funcionó en Windows sin ajustes.
Ahora, todo lo que quedaba era probar el exploit en la aplicación y aguantar la respiración... ¡Vaya, no funcionó! ¿Pero por qué?
Al principio, tenía esperanza porque el objetivo sí se estrelló. Después de todo, no había adaptado la carga útil de Linux para Windows, así que no podía esperar que pasara nada interesante. Para confirmar el comportamiento, cambié la carga útil del exploit para ejecutar en la dirección 0x4141414141. Esta es una técnica común que los escritores de exploits usan para poder ver/probar que han obtenido el control del programa controlando la dirección del puntero de instrucción. Sin embargo, después de ver el bloqueo en WinDbg, no estaba viendo lo que quería. Me daba un error de segmentación al sobrescribir el puntero de la función objetivo.
¿Recuerdas que Electron selecciona los commits de V8 de los que hablaba antes? Resulta que a pesar de que la aplicación era vulnerable al error que estaba usando para explotar, el método de escape de sandbox que usaba el exploit público ya estaba parcheado vía cherry pick. Si no está familiarizado con el sandbox/memory Cage de V8, puede leer al respecto aquí. Esencialmente, es una forma de dificultar la explotación de V8 en el caso de una vulnerabilidad.
Para darme cuenta de lo que estaba sucediendo, necesitaba volver a construir la versión específica de V8, esta vez aplicando los parches seleccionados. Además de los parches de seguridad, Node.js también aplica parches específicos de Node.js a la versión de V8 que utiliza Electron. Me tomó mucho tiempo darme cuenta de que incluso necesitaba hacer esto, ya que no estaba claro de inmediato cómo Electron y Node.js lidian con sus diversas dependencias.
Después de un día o dos de tratar de asegurarme de que la versión de V8 que estaba compilando era *idéntica* a mi objetivo y también de leer sobre técnicas recientes de escape de sandbox, hice progresos. Pude encontrar una técnica de escape que funcionaría para mi objetivo. Después de ajustar la explotación, finalmente pude bloquear la aplicación con el control del puntero de instrucciones. Una dulce victoria, vi el final a la vista...
En este punto, todo lo que quedaba por hacer era modificar la carga útil para explotar el público para ejecutar nuestra carga útil C2 en su lugar. Este cambio aparentemente sencillo resultó ser más molesto de lo que pensaba. La carga útil de Linux del exploit público era simple para hacer estallar un shell, que tenía solo unos pocos bytes de tamaño. La carga útil del C2 era... mucho mayor que eso.
Si conoce la programación en shellcode, sabrá que escribir shellcode de Windows es más molesto que shellcode en Linux, principalmente porque no existe una forma sencilla de realizar llamadas al sistema directas de forma independiente de la posición como en Linux. La carga útil también debía ser "contrabandeada por JOP" dentro de una matriz de punto flotante:
Obviamente, toda la carga útil C2 de la etapa (que tenía varios miles de bytes) no podía ejecutarse así. Así que necesitaba escribir un payload de bootstrapping que mapeara una página ejecutable, copiara la carga útil final y luego saltara a ella.
El problema con la carga útil de bootstrap es que, aunque tenía control del programa, no tenía forma de pasar argumentos a la carga útil que se ejecutaba. Por lo tanto, mi código shell contrabandeado no sabría la dirección de la carga útil final desde la que copiar. Lo solucioné con algo que llamé “contrabando de argumentos”.
Sabía que la dirección del objeto JSFunction sobrescrito se almacenaría en el registro rcx. Así que, utilizando la primitiva de escritura arbitraria, guardé la página mapeada en uno de los campos del objeto que no se necesitaría. Esto requirió un poco de prueba y error, ya que sobrescribir algunas compensaciones provocó fallas. Hice lo mismo para el valor que se copiará y el desplazamiento donde copiarlo. El desplazamiento del campo podría codificarse de forma fija en el código shell, de modo que este supiera desde dónde copiar la carga útil. Llamé a la carga útil n número de veces, donde n es el número de bytes para copiar.
TurboFan, el compilador de optimización de V8, arruinó mis planes. Debido a las optimizaciones de TurboFan, el contrabando de secuencias de instrucciones que se traducían en múltiples puntos flotantes del mismo valor daría como resultado solo una instancia de ese valor en la memoria. Esto impuso limitaciones sobre la frecuencia con la que se podían repetir las instrucciones. Lo solucioné haciendo mi shellcode lo más compacta posible, y también variando la posición de las instrucciones de contrabando si era absolutamente necesario repetir una instrucción, de modo que el valor de punto flotante fuera diferente y no hubiera entradas repetidas.
También me encontré con problemas para copiar shellcode si la carga útil de la etapa 2 era demasiado grande, probablemente debido a la cantidad de veces que necesitaba llamar a la misma JSFunction y TurboFan pisoteados, tratando de optimizar esto. Eventualmente solucioné esto copiando y pegando múltiples bucles en "WriteShellcode" en lugar de un gran bucle. Horriblemente feo, ¡pero funcionó! Más tarde, Bobby y Dylan intercambiaron la carga útil de C2 por un stager que recuperaba la carga útil más grande del almacenamiento de blobs, por lo que no era necesario almacenar la carga útil final en el disco. Esto también ayudó a mantener el tamaño del archivo de main.js en un nivel razonable.
La preparación para el uso operativo real de los exploits siempre debe incluir pruebas en diferentes entornos. En el contexto del compromiso, no sabíamos en qué entorno se ejecutaría la carga útil, solo que se trataba de un sistema Windows que probablemente tenía WDAC habilitado. Por lo tanto, el exploit tenía que funcionar independientemente del sistema operativo. Estaba seguro de que, dado que la versión V8 de la aplicación y todas las dependencias estaban contenidas dentro de la aplicación, no se encontraría mucha variabilidad. Me equivoqué en esa suposición.
Por razones que no entiendo, la compensación del puntero de función vulnerable para sobrescribir cambió en las diferentes versiones de Windows. Esto no tenía sentido porque, según tengo entendido, la distancia de compensación está determinada por el motor V8 JIT, cuyas bibliotecas se cargan directamente desde el paquete de la aplicación. Esto significa que se cargan exactamente las mismas bibliotecas V8 independientemente del sistema operativo. Para complicar aún más las cosas, la variación no parecía seguir ningún patrón. El desplazamiento a veces era de 4 bytes en algunas versiones de Windows (tanto antiguas como nuevas). Esto era especialmente molesto porque no había forma (por lo que pude ver) de obtener el offset adecuado dentro del exploit de JavaScript. La única forma de calcularlo era utilizar el shell de depuración para leer la dirección de memoria y hacer los cálculos, lo que obviamente no era una opción desde la aplicación Electron de producción. TLDR: la variación de las compensaciones no se puede calcular en tiempo de ejecución de explotar.
Para solucionar el problema de la compensación inconsistente, Bobby y Dylan rediseñaron el exploit para que main.js lo lanzara varias veces, probando los diferentes offsets posibles hasta que tuviera éxito. Esto se hizo haciendo que el proceso de código inicial realizara un bucle. Este bucle generaba procesos hijos que intentaban el exploit con un desplazamiento único. Si la explotación fallaba, el proceso secundario se terminaría. Si la explotación fue un éxito, entonces el shellcode se ejecutaría y escribiría un archivo Mutex antes de desplegar el C2 de la etapa 2. Una vez que la explotación fuera un éxito, el proceso inicial saldría del bucle y dormiría para siempre.
Aunque esto significaba que un intento de desplazamiento incorrecto causaba un fallo, nuestras pruebas revelaron que no había errores visibles para el usuario y que la funcionalidad de la aplicación seguía funcionando perfectamente. Aunque no era la solución más limpia y era algo ruidosa por los accidentes, el tiempo era fundamental. Esto es lo que llamamos en el negocio "JIT xdev", y funcionó perfectamente para nuestras necesidades.
Obviamente no queríamos que el exploit fuera evidente si nos pillaban y alguien analizaba el punto de entrada main.js de la aplicación. Para evitar eso, aplicamos un ofuscador de JavaScript en el código de explotar, lo que lo hizo prácticamente incomprensible para el ojo humano. Gracias al talento y la dedicación de Chris Spehn, que mantiene el pipeline de CI/CD de carga útil del equipo, pudimos optimizar la entrega de esta carga útil y reofuscar el código cada vez que se generaba la carga útil, por lo que pudimos reutilizar la aplicación indefinidamente con código de explotar diferente cada vez. Esto evitó que la carga útil quedara registrada. Esto resultó especialmente útil, ya que, lamentablemente, la primera vez que intentamos usar la capacidad, nos atraparon porque el usuario marcó el correo electrónico de phishing 🙁. Curiosamente, si bien el equipo azul del cliente analizó la aplicación a partir del correo electrónico de phishing, no detuvieron el propósito de la aplicación, ni identificaron el exploit V8 incrustado.
Todavía no entiendo por qué las compensaciones de funciones JITted dependían del sistema operativo, ya que se supone que todas las bibliotecas V8 relevantes están agrupadas dentro de la aplicación Electron. Si alguien tiene alguna idea de por qué esto es así, ¡hágamelo saber!
Electron ha implementado una característica experimental para la integridad que verifica la integridad de todos los archivos de la aplicación en tiempo de ejecución. Ha estado disponible para macOS desde la versión 16 y Windows desde la versión 30. Los desarrolladores de aplicaciones pueden habilitar este fusible Electron para asegurarse de que ninguno de los archivos de la aplicación sea manipulado. Si lo son, el proceso se cerrará automáticamente y no se ejecutará nada.
Esta característica evita la modificación de cualquiera de los archivos empaquetados de la aplicación Electron, incluido main.js, y frustra las técnicas discutidas. Sin embargo, aún no se ha implementado en las aplicaciones más populares. Si esta característica ve un uso más generalizado, aún debe tenerse en cuenta que las versiones anteriores de la aplicación, antes del fusible de integridad, seguirán siendo vulnerables y utilizables para este ataque.
Bobby Cooke & Dylan Tran — Ayudando a operacionalizar la explotación
Dylan Tran – Creación de diagramas
Chris Spehn:Integración de esta carga útil en nuestro pipeline de CI/CD (y todo el otro trabajo de DevOps ingrato que ha realizado para el equipo)
jeffssh – Inspiración
jj - Ser un hacker maestro de V8 cuyos prolíficos PoC de V8 ayudaron inmensamente