Un buffer overflow para gobernar Chile

Claudio Salazar
alertot
Published in
16 min readApr 15, 2019

El año pasado hubo en Chile una charla titulada “Chile Exposed: un puerto para gobernarlos a todos” haciendo referencia al anillo del Señor de los anillos. La charla trataba sobre acceso a recursos compartidos de diversas organizaciones en el puerto 445, por lo que me cuestioné si ese era realmente el anillo para gobernarlos a todos. Creo que hay un mejor postulante a One Ring, vamos en su búsqueda!

Vamos a viajar en el tiempo: Chile en 2014. ¿Qué software estaba instalado en la mayoría de los principales sitios nacionales? Respuesta rápida: el kit de pago de Transbank (KCC). El KCC era una serie de ficheros que debían ser instalados en cada servidor que quería hacer comercio electrónico en Chile. Entre esos ficheros, habían unos binarios que debían ser corridos como CGI.

Vamos un poco mas atrás: Octubre de 2012. El cofundador de alertot, Guillermo, estaba haciendo el sitio de una maratón y necesitaba añadir pago electrónico. La única alternativa era el KCC 6.0.0 y me pidió analizar si su instalación suponía algún riesgo.

Cuento corto: hice reversing de los binarios y rápidamente encontré un buffer overflow remoto, no autenticado y fácilmente alcanzable. Hice una prueba de concepto (miles de A ), lo reporté responsablemente a Transbank, fui a las oficinas, mostré mi PoC, expliqué cual era el problema, cómo arreglarlo con una función adecuada, hasta que me preguntaron:

¿Cual es la probabilidad de explotarlo exitosamente?

A lo cual no tenía respuesta, ya que eso supondría mucho tiempo de investigación para llegar a un exploit completo. Al final, el bug nunca fue parchado. Es más, ya existía en la version 5.1 y no puedo confirmar cuanto tiempo estuvo presente, pero acorde a la versión 5.1serían al menos 8 años.

Ese era el anillo, un exploit funcional contra el KCC y ganar acceso a los servidores de los principales actores de la banca, retail y comercio de todo tamaño. En 2012 y 2014 intenté la explotación y fallé porque se realiza el overflow pero luego hay una serie de validaciones sobre información corrompida por el mismo overflow. Luego, en SPECT en 2015 escribí un post hablando sobre los riesgos de los CGIs del KCC. El año pasado, la charla antes mencionada hizo revivir el tema en mí, tiempo después llego una nueva idea a mi cabeza que podría simplificar el problema y ahora volvimos al ataque, la tercera es la vencida.

Este post trata sobre el descubrimiento de la vulnerabilidad y el proceso de explotación local, finalizando con un exploit que logra ejecución arbitraria de comandos usando una técnica conocida como ret2libc. Muestro todos los pasos realizados, algunos errores cometidos y como sortié un dead end con motivos educacionales.

En términos de notificación responsable, la notificación inicial fue en Octubre de 2012 y actualmente el KCC se encuentra obsoleto, por lo que no afectaría a terceros.

Como disclaimer, no soy experto en el área, de hecho es la primera vez que exploto un buffer overflow “real-world”, así que toda crítica, idea o corrección conceptual es bienvenida.

El ambiente

A continuación, se enumeran las condiciones del ambiente donde fueron realizadas las pruebas:

  • Sistema operativo: Ubuntu 16.04
  • Arquitectura: 32 bits
  • Version KCC: 5.1 (Mayo 2010)
  • ASLR: desactivado
  • NX bit: activado

tbk_bp_pago.cgi_51: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.4.1, with debug_info, not stripped

Las versiones afectadas son 5.1 , 6.0 y 6.0.2 .

En un inicio intenté con la versión 6.0.2 en 64 bits pero encontré problemas en la explotación debido a algunas características de la arquitectura (vamos a hablar más adelante sobre eso). Por lo tanto, decidí probar con 32 bits y la única versión que encontré fue 5.1 , que en términos prácticos presenta la misma lógica vulnerable.

Desactivé ASLR en el sistema operativo para hacer la explotación más fácil. Si bien puede parecer extraño, cuando el binario fue publicado (2010) ASLR no era tan común como lo es ahora.

La vulnerabilidad

Esta parte hace eco de mi proceso de búsqueda de vulnerabilidades en 2012. Lo bueno de estos CGIs es que proveen una interfaz en línea de comandos, por lo tanto pueden ser llamados como cualquier binario y pasarles input.

El input tiene la forma: parameter1=value1&parameter2=value2&... como si se tratase de una petición HTTP.

El enfoque fue simple: buscar el uso de funciones normalmente relacionadas con buffer overflows. Comencé con strcpy() y encontré un función interesante llamada filtraParametros que tiene una llamada a strcpy. Para estar a la moda, usaremos la decompilación provista por Ghidra.

Esta función es llamada al recibir una nueva petición. En breve, la función filtraParametros procesa todos los parámetros enviados por el usuario, idealmente acorde a la documentación. Sin embargo, el único requerimiento para que un parámetro sea válido es que el parámetro comience con el string TBK_.

La función lee ciertos ficheros al comienzo, luego en la línea 74 comienza a procesar los parámetros enviados por el usuario. Así, si enviamos como input TBK_OUR_PARAM=1234 , tenemos item.name=TBK_OUR_PARAM e item.value=1234 .

La línea 89 tiene una llamada a strcpy . El primer argumento varNameTemp se define en la línea 17 como un buffer de 512 bytes y el segundo argumentoitem.nameestá bajo nuestro control.

Como strcpy no limita el número de bytes copiados desde item.name a varNameTemp , si nuestro parámetro tiene una extensión mayor a 512 bytes podríamos sobreescribir datos en el stack y llegar a tomar el control del programa. Para alcanzar esta llamada a strcpy , es necesario cumplir con la condición de la línea 88, es decir que la función verificaTipoMall retorne un valor verdadero al ser llamada con nuestro parámetro como argumento. Con la ayuda de Ghidra, podemos saber que hace esta función.

Según la documentación (punto 8.2) hay un tipo de transacción llamada “Transacción Mall Virtual”, donde se añaden parámetros con nombres tales como TBK_CODIGO_TIENDA_MXXX y otros más.

La función verificaTipoMall busca el string _M00 en nuestro parámetro, entonces un input como TBK_CODIGO_TIENDA_M00=1234 cumpliría con la condición de la línea 88 y alcanzaríamos la llamada a strcpy deseada.

Ok, estamos listos para el overflow, debemos ver donde ponemos nuestro payload. En 2012, utilicé un parámetro como TBK_<payload>_M00=1234 para gatillar la vulnerabilidad y mi input contenía varios parámetros TBK_* tras mi parámetro malicioso en una idea de parecer una petición legítima.

Mi prueba de concepto en 2012 fue payload = "A" * 1000 , lo demostré en un servidor Apache sobre Windows que al recibir la petición maliciosa mostraba un diálogo que un proceso había terminado mal.

Esa fue la historia 7 años atrás, donde genere una corrupción de memoria. Ahora vamos por esa ejecución de comandos!

Primer análisis

Antes de comenzar, haremos unos cambios en nuestro input para asegurar mayor confort en el desarrollo del exploit.

  1. Viendo que para verificaTipoMall no importa donde se encuentra el string _M00 , usaremos un parámetro TBK_TIPO1_M00_<payload>=1234 que da más control sobre la escritura en el stack.
  2. En vez de tratar de simular una consulta legítima, usaremos como input solo nuestro parámetro malicioso, ya que es suficiente para gatillar la vulnerabilidad.
  3. Recordar que los bad characters son los que tienen incidencia en el término de un parámetro HTTP (= y &) y %00, ya que el cortaría el buffer pasado a strcpy .

El código del exploit inicial es el siguiente:

El código define una función que “genera” patrones, crea e imprime nuestro payload (para saber más sobre el uso de patrones). En este caso, comenzamos con un payload de 700 bytes.

Cargamos el binario en gdb (+ pwndbg) con gdb ./tbk_bp_pago.cgi_51 y luego corremos nuestro exploit con r <<< $(python exploit.py) . El programa termina con SIGSEGV y podemos ver que$EBP=8As9 , eso significa que sobreescribimos hasta el registro $EBP (Extended Base Pointer) y podríamos controlar la ejecución del programa. El patrón8As9 se encuentra en el offset 566, por lo tanto la distancia entre el inicio de nuestro buffer y $EBP es 566 bytes y con $RET=$EBP+4 es 570 bytes.

La importancia de $RET es que si logramos escribir en esa ubicación, tras la ejecución de la función filtraParametros , el $EIP (Extendend Instruction Pointer) tomará la siguiente instrucción a ejecutar desde $RET , lo que significa que controlamos la ejecución del programa.

Normalmente en los tutoriales de explotación, se crea una función que usa strcpy y luego la función termina, con lo cual la señal SIGSEGV normalmente corresponderá a que el$EIP apunta a una dirección inválida, se arregla y se logra ejecución arbitraria de código. En este caso, estamos en la línea 89 de un total de 138 líneas y nuestro overflow ha corrompido los datos presentes en el stack, que son usados por la función entre las líneas 90 y 138. El SIGSEGV recibido no es generado por un $EIP inválido sino por la línea 90, justo después de sobreescribir el stack.

Esto hace que debamos tener una mirada macro para alcanzar el final de la función y poder tomar control del programa. En orden de prioridades, nuestro overflow debería escribir valores específicos en el stack para:

  1. Pasar la línea 90.
  2. La línea 122 debería retornar NULL para salir del while.
  3. Pasar una argumento válido para registra_log en la línea 125 o 128.
  4. Cumplir con la condición de la línea 131.
  5. Cumplir con la condición de la línea 134.

Cuando uno dice “cumplir”, muchas veces es más fácil esquivar. Let’s go!

El código de la línea 90 no es tan claro, mejor revisemos las instrucciones de ensamblador asociadas.

En la línea 1, se recupera un valor númerico de 4 bytes desde el stack en la ubicación $EBP-0x30 , a este valor le llamaremos $variable90 y actualmente se encuentra sobreescrito por nuestro overflow. En la siguiente línea se le resta 3 y luego se calcula una ubicación con la siguiente fórmula:

$EBP + $variable90 - 0x244

En esa ubicación se escribirá el byte nulo 0x00. El SIGSEGV recibido se debe a que el valor de $variable90 hace que la fórmula calcule una ubicación donde no se puede escribir.

Lo primero, es saber donde se encuentra $EBP-0x30 en relación a nuestro payload. Tras el SIGSEGV, vemos que $EAX apunta a 0x3372412f ('/Ar3') . Si bien el patrón/Ar3 no es válido, es similar al patrón2Ar3 que se encuentra en el offset 518, y en este caso es el offset correcto.

Ahora se debe encontrar una dirección con permisos de escritura para apuntar la fórmula ahí. He elegido la sección de .dtors, que se encuentra en 0x0812c3c4 .

¿Qué valor debería tener $variable90 ? Tenemos que $EBP=0xbfff9010 , entonces si seteamos$variable90=0x48133383 daría como resultado la dirección de .dtors , considerando que a$variable90 se le resta 3.

¿Cómo pasamos este input? Ya que es un CGI, debemos usar codificación URL y tratar el valor como little endian, entonces nuestro exploit hasta el momento quedaría así.

Corremos nuevamente el exploit y esta vez recibimos SIGSEGV en la función list_next .

Se supone que la lista de parámetros enviados por el usuario son una lista enlazada de nodos (al estilo de curso de estructura de datos en C), por lo tanto list_next accede a node->next . Esta dereferencia a next añade 8 bytes a node , por lo tanto, si le pasamos la dirección de un node donde node+8 es igual a 0 , cumpliremos con terminar el gran while .

El patrón a9Ab está en el offset 28 a partir de la variable variable90 . Para node elegiremos una dirección cualquiera que cumpla con los requisitos.

Nuestro exploit luce así.

Corremos nuestro exploit nuevamente y recibimos lo siguiente.

En este caso, provocamos un SIGSEGV en la función copiarEn que es llamada por registra_log . A partir del patrón a5Aa , sabemos que el argumento pasado a registra_log está en nuestro payload a 16 bytes de variable90 . Después de probar una serie de argumentos, decidí poner una dirección cualquiera del heap porque registra_log realiza algunas escrituras en la dirección del argumento. En esta oportunidad la dirección será 0x08161818 y el exploit queda así.

Corremos el exploit y llegamos a la función fclose de la línea 132.

En este momento comenzaron los problemas. La lógica de fclose es bastante recursiva y no es fácil salir de ahí. Tuve problemas usando direcciones válidas de streams, streams propios (algo similar a lo expuesto en esta investigación), llegué a controlar algunas llamadas como call eax con eax bajo mi control pero habían un montón de bad characters, comportamientos raros y una confusión de contextos entre carácteres codificados para URL y no. También era un tema que el stack no era ejecutable.

Pasaron los días y no podía escapar de fclose . Como en alertot “we try harder” no porque nos lo dice una certificación, volví a las bases para ver si estaba obviando algo en el proceso. Leí este artículo sobre explotación de una vulnerabilidad off-by-one, si bien la conocía, no sabía como operaba exactamente. Me hizo click la idea de poder escribir de alguna forma un byte 0 en direcciones arbitrarias. Revisando nuevamente la lógica previo al fclose , hay una comparación:

La línea 1 toma una valor númerico de 4 bytes desde la ubicación$EBP-0x3C. Llamaremos a esta variable $fclose_condition. Debido a que nuestro overflow no puede contener caracteres nulos, modificamos los 4 bytes a algún valor i.e. 0x44440194 , por lo tanto es imposible cumplir con el valor requerido 0x00000194 . Sin embargo, en la inspiración off-by-one , volvemos un poco atrás a la línea 90.

Esta lógica es llevada a cabo por cada parámetro que cumple con el requisito de contener TBK_ y _M00 en el nombre. En la línea 3, el valor de$eax viene dado por el largo del nombre de nuestro parametro, entonces podríamos hipotéticamente escribir un valor 0 donde deseamos. Imaginan usar más parámetros para cambiar el valor escrito por nuestro overflow 0x44440194 a0x00000194 y esquivar la llamada afclose ?

Un nuevo comienzo

Antes del entusiasmo, hay que realizar una prueba de concepto para ver si la idea funciona en la práctica. Ya sabemos que la distancia entre el inicio de nuestro buffer y $EBP es 566 y si calculamos la distancia a$EBP-0x3C sería 566–0x3c=506 bytes. Por lo tanto, nuestra meta es el siguiente escenario para que $fclose_condition tenga el valor 0x149. (los bytes al revés son debido a little endian)

Nuestro parámetro actual, en su camino a $EBP y más allá, sobreescribe $fclose_condition con AAAA. El estado en que queda $fclose_condition tras el strcpy del primer parámetro es el siguiente:

Ahora entran los chicos buenos, la idea es que el segundo parámetro sobreescriba hasta el byte 508 y añada el byte 0x00 en la posición 509. Usando un parámetro con la misma forma del parámetro actual (para tener los mismos offsets), el parámetro resultaría:

TBK_TIPO2_M00_<payload>=AAAA, con payload="B" * 509

Tras elstrcpy de este nuevo parámetro, $fclose_condition queda así:

A pesar del extraño valor en la posición 506, logramos nuestro cometido de escribir el byte 0x00 en la posición 509. Ahora viene el tercer parámetro, que tiene como objetivo escribir un byte 0x00 en la posición 508, byte 0x01 en la posición 507 y byte 0x49 en la posición 506. El parámetro resulta:

TBK_TIPO3_M00_<payload>=BBBB, con payload="C"*506 + "%94%01"

Como resultado, tenemos este escenario para $fclose_condition :

El script usado para la prueba de concepto:

Luego en gdb, ponemos un breakpoint antes de la comparación y tenemos:

Bingo! Ahora debemos modificar nuestro exploit anterior para incorporar estos nuevos cambios.

En busca de EIP

Hay que realizar dos modificaciones en nuestro código:

  1. Cambiar nuestro node para que sea posible acceder a la lista de parámetros enviados por el usuario.
  2. Revisar si el stack cumple la condición de la línea 134, donde se libera un puntero.

En este momento, añadiremos un nuevo parámetro llamado TBK_TIPOX (un parámetro normal) que servirá para contener nuestros comandos a ejecutar en una posterior explotación. Lo agregamos ahora porque si lo hacemos después, puede modificar los offsets que encontremos.

Luego de la adaptación de nuestra prueba de concepto, nuestro exploit luce así:

Revisando el flujo normal del programa cuando recibe una serie de parámetros, node antes de la llamada a list_next debería apuntar a un puntero a nuestro primer parámetro, TBK_TIPO1_M00_....

Ponemos un breakpoint antes de la llamada a list_next y con la ayuda de pwndbg, podemos ver los siguientes valores en el stack.

Si revisamos 0x081432c8 , nos daremos cuenta que apunta a nuestro primer parámetro, y con la ayuda de pwndbg, encontraremos un puntero a esta ubicación de memoria.

Listo, el nuevo valor de node será 0x08141288 . Mantenemos el breakpoint, modificamos el exploit y lo corremos nuevamente, vemos como list_next procesa nuestros 4 parámetros y finalmente el programa finaliza con una señal SIGABRT provocada por la llamada a free de la línea 135.

Si revisamos el código de filtraParametros , vemos que el argumento de la función free es el mismo de que usamos en registra_log , es decir, a_heap_address . Nuestra primera elección de a_heap_address es una dirección cualquiera en el heap, sin embargo, debió haber sido una dirección previamente (m/c)alloc-eada. Hay varias llamadas a calloc , vamos a elegir la llamada de la línea 50, pondremos un breakpoint y obtendremos nuestro nueva dirección para a_heap_address=0x08142840 . Ahora la llamada free(a_heap_address) no emitirá una señal y continuará la ejecución de la función filtraParametros .

Con las últimas modificaciones, nuestro exploit se ve así.

Lo ejecutamos y recibimos lo que estuvimos buscando!

Vamos por esa ejecución arbitraria de comandos!

Pasando del SIGSEGV a ejecución de comandos

Ya pasó lo más complicado, ahora nos encontramos con control del $EIP . Como el stack no es ejecutable, no podemos almacenar una shellcode en nuestro buffer y redireccionar el programa hacia el buffer. Para estos casos, vamos a usar una técnica llamada ret2libc, haciendo uso de la función system de libc. En este tutorial pueden ver más detalles sobre la técnica.

El layout de la memoria se vería así:

Es un ret2libc normal, lo único adicional es un NOP sled al final, la idea fue extraída desde acá. arg_to_system apuntará a value de nuestro parámetro TBK_TIPOX=value .

La parte final de nuestro exploit, ejecutando id como comando, es la siguiente:

Sin embargo, estos nuevos valores añadidos a nuestro payload modifican el offset de node y también de value en TBK_TIPOX . Los reajustamos, cambiamos nuestro comando id fijo por un argumento que se le pasa al script y tenemos nuestro exploit final.

Ahora corremos nuestro exploit, ejecutando id y uname -r como comandos:

El camino a full RCE

Nuestro exploit no funciona si el CGI está corriendo sobre un servidor web como Apache o Nginx, que es el escenario donde uno encontraba el KCC. Citando la documentación de Apache:

Debugging CGI scripts has traditionally been difficult, mainly because it has not been possible to study the output (standard output and error) for scripts which are failing to run properly. These directives provide more detailed logging of errors when they occur.

Y sobre todo si se trata de un binario ya que la mayoría de las soluciones toman en cuenta el uso del codigo fuente , sumado a la complejidad de un entorno como Apache. ¿Se podría parchar el binario? Si, pero ya entra en el denominado “El camino a full RCE (Remote Command Execution)”, donde apunto algunas ideas para hacer el exploit más efectivo en el caso de lanzarlo contra un servidor web (y no localmente como mostramos en este post).

Primero que todo, el exploit tiene dependencias sobre direcciones de memoria como:

  • a_heap_address
  • node

Lo primero que pensé, como a_heap_address es usada en la condición final (línea 134), es evitar la llamada a free haciendo a_heap_address=0x00000000 usando la misma técnica que use en $fclose_condition . Sin embargo, a_heap_address también es usado en registra_log y lanzaría una señal al intentar escribir en 0x00000000 .

a_heap_address debe ser una dirección del heap que haya sido (m/c)-alloc-eada. Para aumentar las posibilidades de explotación, hay una relación directa entre el número de parámetros enviados y el tamaño del heap, ya que el procesamiento de parámetros incluye llamadas a calloc (línea 93).

Por ejemplo, en nginx el tamaño máximo por defecto del body es 1MB, eso significa que podemos enviar miles de parámetros cortos (i.e. TBK_1= ), todos crearán espacio en el heap en la línea 93 y sería más fácil que a_heap_address corresponda a una dirección válida.

La referencia a node es susceptible a todo tipo de cambios, por lo que lo ideal sería:

  • Tener las condiciones para que a_heap_address caiga en un espacio de heap alloc-eado.
  • Hacer fuerza bruta sobre el vecindario de node para dar con la referencia correcta.

Tener otros elementos a mano como un infoleak ayudaría a mejorar la confiabilidad del exploit y no olvidar, tener un claro entendimiento de lo que pasa dentro de los hilos de Apache o Nginx con el CGI.

La historia con 64bits

El problema con el que me encontré en 64 bits era para definir direcciones de memoria en el stack. Aprendí que si bien existe soporte para 64 bits (8 bytes), las direcciones de memoria solo usan 6 bytes, dejando los restantes 2 bytes nulos, es decir, una dirección 0x112233445566 es realmente 0x0000112233445566 . En vista que el overflow sobreescribía todo, era imposible conseguir los dos bytes nulos del comienzo.

Ahora que cuento con estos parámetros adicionales con los que puedo poner bytes 0x00 en ubicaciones arbitrarias, creo que podría explotar el KCC en su versión de 64 bits.

Conclusiones

Las conclusiones son claras, la vulnerabilidad existió por más de 8 años, 5 años con conocimiento de Transbank y no fue parchada, contando incluso con el parche. Hoy vemos que es explotable y seguramente un exploit writer más experimentado puede llegar al full RCE.

Espero que con las nuevas medidas en materia de ciberseguridad en Chile pongan un ojo también en los medios de pago y las vulnerabilidades en sus sistemas.

--

--