Babytrace: Leaking flag through symbolic reg with angr

By Julio Vidal aka m12 - @_jcvg_

Este reto en la categoría de Reversing fue resuelto durante el Defcon Quals celebrado en el fin de semana del  11 de Mayo 2019.

El reto nos proporciona 2 archivos relevantes: headerquery y pitas.py . Así como una dirección y puerto donde se está ejecutando pitas.py babytrace.quals2019.oooverflow.io:5000 .


Al conectarnos al servidor con netcat nos presenta con el contenido de pitas.py lo cual en primera instancia nos pide que seleccionemos un binario, false o headerquery, una vez seleccionado el binario nos presenta una serie de opciones para comenzar, resumir o borrar un trace. Al optar por "start a trace" nos presenta otro menú donde nos pregunta cómo deseamos manejar el input del programa: unconstrained symbolic variable, constrained symbolic variable, concrete value. Como de entrada no sabemos qué ocupamos optamos por agregar una variable simbólica sin restricciones, en el siguiente menú podemos ver que tiene las opciones para ejecutar N pasos del programa, mostrar el input, mostrar output, mostrar error, concretar un registro, simbolizar un registro e imprimir las restricciones agregadas. Optamos por ejecutar 100 pasos para ver si marca algo y nos corta la conexión con el siguiente error:


Assertion violation: This is a tracing interface, not a general symbolic exploration client!!!


Lo cual al buscar en pitas.py vemos que tiene la siguiente restricción:


assert len(simgr.active) == 1, "This is a tracing interface, not a general symbolic exploration client!!!"


Dado que ejecutamos con angr y un input sin restricciones generará todas las ramas necesarias para explorar el programa por lo que debemos analizar el binario para ver de qué manera podemos hacer que se ejecute sin generar más de 1 estado activo.


Como podemos observar las operaciones que realiza es abrir el archivo del flag, leer el contenido del flag (buf) y leer 4 bytes desde el input (var_118). Si el input es menor o igual a 0xFF el programa continúa con la ejecución por la rama de la derecha , posterior a ello imprime "Checking input..." y vuelve a comparar nuestro input, si el mismo es mayor de 2 entonces se va por la rama de la derecha imprimiendo "Nope." por el contrario si es menor o igual a 2 salta por la rama de la izquierda imprimiendo el byte que corresponde al índice de nuestro input.

La cuestión es cómo obtener el flag si sólo nos permite leer los 3 bytes de acuerdo a la lógica del programa, la solución está en 0x400856 antes de que valide por segunda ocasión el  input.

mov eax,[rbp+var_118]
cdqe
movzx eax,[rbp+rax+buf] 

Como podemos observar antes de validar el input carga en eax el contenido del flag en el índice de nuestro input por lo que si podemos ejecutar hasta ese punto en rax tendríamos el byte de ese offset.

Con lo anterior en mente podemos proceder a conectarnos al servidor, cargar el binario headerquery, agregar un valor en concreto(p.e.: 03000000), ejecutar 12 pasos (Opc. 1), agregar un registro simbólico (Opc. 6, rax en este caso) e imprimir las restricciones (Opc. 7) para obtener el valor de rax con ese input como se puede observar al final de la siguiente
imagen.



Por lo que para obtener todo el flag podemos repetir la operación tantas veces sea necesario hasta que obtengamos el carácter de fin del flag '}'. Para automatizar lo anterior se diseñó
el siguiente script.


from pwn import *
import re

result = ""
for i in range(0x30):
 r = remote('babytrace.quals2019.oooverflow.io', 5000)
 for j in range(49):
  r.recvline()
 r.recvn(8)
 r.sendline("2")
 r.sendline("1")
 r.sendline("3")
 v = ""
 if i < 0x10:
  v += "0"
 v += "%x" % i
 v += "000000"
 r.sendline(v)
 r.sendline("0")
 r.sendline("1")
 r.sendline("12")
 r.sendline("6")
 r.sendline("rax")
 r.sendline("7")
 pattern = re.compile(r'CONSTRAINTS:')
 s = r.recvline()
 f = re.search(pattern, s)
 while f == None:
  s = r.recvline()
  f = re.search(pattern, s)
 result += chr(int(s[28:32],0))
 r.close()
 if chr(int(s[28:32],0)) == '}':
  print result
  break


P.D. ¿Cómo se obtuvo que se necesitaban 12 pasos para leer el flag? A prueba y error, ejecutando paso a paso y revisando el Output... en IDA observamos que la vulnerabilidad venia justo antes de que imprimiera "Checking input..." así que simplemente ejecuté paso por paso hasta antes de que imprimiera el mensaje.

Comentarios