R2wars, una competición diferente

(Esta entrada ha sido elaborada por Erik Martínez y Vicente Lahoz)

INTRODUCCIÓN

La semana pasada asistimos a la r2con, una conferencia con una comunidad muy comprometida, centrada en el análisis de malware, ingeniería inversa, explotación de binarios y mucho más. La conferencia está organizada por los desarrolladores de la herramienta radare2, un framework de reversing de código abierto muy potente que ofrece multitud de posibilidades, como el ensamblado y desensamblado de archivos, debugging de ejecutables o explotación de software.

Todo esto ejecutado desde la línea de comandos (también tiene una GUI llamada Cutter), así que puedes automatizar tareas y gestionar las salidas del terminal como desees. También tiene una API disponible para la mayoría de lenguajes de programación llamada r2pipe, por lo que puedes integrarla en tus propios scripts y herramientas de una forma muy sencilla. ¡Bastante chulo!

La r2con se compone de charlas, talleres y competiciones (entre varias pausas para café) [Véase github.com/radareorg/r2con2019]. En este post, vamos a contar nuestra experiencia en r2wars, una de las competiciones que se celebran durante la conferencia.

¿EN QUÉ CONSISTE ESTE TIPO DE COMPETICIÓN?

Esta competición consiste en una serie de batallas entre dos programas (llamados bots o warriors), que se ejecutan en el mismo espacio de memoria. El objetivo es hacer crashear al oponente (o sobrevivir hasta que el oponente crashee por si mismo).

¿Pero cómo hacer esto para derrotar al oponente? Hay varias formas de hacer que un proceso crashee, o deje de funcionar. Por ejemplo, acceder a una posición de memoria inválida (fuera del espacio definido para el combate, de 0 a 1024 bytes) o ejecutar una instrucción no válida para el procesador. Como ya sabréis (y si no, lo sabres después de leer este post), el lenguaje ensamblador depende de la arquitectura del procesador. Cada instrucción tiene asignada un determinado opcode en lo que se denomina código máquina, que es el conjunto de bytes que codifican una instrucción, y que ejecuta el procesador.

Si por ejemplo tenemos un bot escrito en arquitectura x86 de 32 bits y este escribe los bytes 89d0 (mov eax, edx) en una dirección de memoria determinada, cuando un bot de x86 intente ejecutar el contenido de dicha dirección, moverá el contenido del registro edx al registro eax y seguirá ejecutando la siguiente dirección de memoria. ¿Pero y si es un bot escrito en ARM el que intenta ejecutar el contenido de esta dirección de memoria? Los opcodes varían dependiendo de la arquitectura del procesador, por lo que este escenario resultaría en un crash del bot ARM por ejecución de instrucciones inválidas, ya que es incapaz de interpretar los bytes 89d0 como una instrucción del procesador.

Los bots son pequeños programas escritos en ensamblador. Puedes escoger tu arquitectura favorita (x86, arm o mips) y prepararte para la batalla. Pero espera un momento, has dicho que los bots se ejecutan en el mismo espacio de memoria, ¿cómo puede un bot en ARM competir contra un bot en x86 si son arquitecturas diferentes? La respuesta a esta pregunta es ESIL (Evaluated Strings Intermediate Language). ESIL traduce los opcodes de las arquitecturas soportadas a un lenguaje intermedio que es emulado en una máquina virtual del propio lenguaje. Las R2wars se ejecutan en un espacio de memoria emulado que es controlado por ESIL, por lo que todas las arquitecturas pueden competir con las demás.

Durante la batalla, los bots se ejecutan por turnos. En cada turno se ejecuta un ciclo de una instrucción. Hay instrucciones que ejecutan más ciclos que otras y se ejecutan más lentas. La longitud de la instrucción también es importante. Hay instrucciones que ocupan más direcciones de memoria que otras, lo que hace que el bot sea más fácil de encontrar durante la batalla. Controlar el tamaño del bot y la cantidad de espacio que ocupará en memoria es clave para crear un buen bot, aunque el éxito depende en gran parte de la posición en la que empiece el bot (es aleatoria para cada ronda) y en la táctica de los oponentes.

El espacio virtual de memoria, donde se ejecutan los bots, tiene un tamaño de 1024 bytes (desde la posición 0 a la 1023). Si un bot intenta acceder a una dirección inválida crasheará, y por lo tanto perderá la ronda. Los combates tienen un límite de 4000 ciclos, con el fin de evitar rondas que no acaban nunca.

Sabemos que puede ser difícil de entender, pero será mucho más fácil después de una explicación visual. Aquí se puede ver el servidor con r2wars, en el cual se está llevando a cabo un combate:

Combate r2wars

Combate r2wars ¿Puedes adivinar el ganador de este combate? Pista: No es tan fácil como parece

Veamos algunos detalles:

  • Hay dos bots, uno de color azul y otro de color rojo.
  • Cada bot se ubica en una dirección de memoria aleatoria al principio de cada ronda.
  • Se puede observar el código de cada bot a los lados del tablero.
  • La instrucción actual y su ciclo se muestran y se marcan en el código del bot.
  • Los valores contenidos en cada registro se muestran bajo el nombre de cada bot.
  • El tablero muestra el espacio de memoria compartido, desde la dirección 0x0 a la 0x1023. Cada cuadrado representa un byte. (El 0 se encuentra en la esquina superior izquierda)
  • Cada cuadrado azul/rojo oscuro representa una dirección de memoria que contiene código de uno de los bots. La X se añade al cuadrado cuando un bot ejecuta dicha dirección de memoria.
  • Cada cuadrado azul/rojo claro representa una dirección de memoria que ha sido accedida por un bot. La letra muestra el tipo de acceso (Read or Write).

Ahora que conocemos las reglas, estamos listos para crear muestro primer bot. Nosotros elegimos la arquitectura x86 de 32 bits ya que es la que más conocemos. El primer bot fue muy simple, el objetivo era asimilar los conceptos de la competición, como en el tutorial de un videojuego. La idea del bot era escribir 4 bytes de opcodes inválidos cada bloque de 18 bytes. El bot escribe 4 bytes y amenta el stack pointer 22 bytes (18 + 4) en bucle hasta que alcanza el límite de la arena y crashea al tratar de acceder a una posición inválida (es el de color rojo en el GIF). Fue un gran comienzo, ya que nos permitió asimilar bien las normas y algunas técnicas de la competición.

call tag
tag:
pop esp
loop:
add esp, 16
push 0xffffffff
jmp loop

Este bot simple se compone de dos fases:

  • Reconocimiento: Puesto que en cada combate los bots se ubican en direcciones de memoria aleatorias, el bot necesita saber donde está para no sobrescribirse a sí mismo por error. Al ejecutar la instrucción call, la siguiente dirección de memoria a continuación del call es pusheada al stack para saber donde volver cuando la función llamada por el call finalice. Después del call usamos pop para obtener la dirección de memoria donde se encuentra el bot. Usamos esta dirección para que los opcodes inválidos pusheados en la siguiente fase no sobrescriban el propio bot. Este es un método muy conocido para obtener el puntero de instrucción en la arquitectura x86.
  • Bucle: Ahora que sabemos donde se ejecutará el bot independientemente de la posición donde se coloque inicialmente, podemos empezar a escribir opcodes inválidos. Si un oponente intenta ejecutarlos, crasheará. En cada iteración se suman 22 bytes al stack pointer (4 de los opcodes inválidos y 18 del bloque) y se pushea el valor 0xffffffff a la dirección de memoria que marque el stack pointer (que hemos modificado para que no colisione con el bot y recorra parte del tablero en bloques de 18 bytes). El bot crashea por sí mismo cuando alcanza el límite de la arena.

Ahora que de verdad sabemos lo que está ocurriendo en la pantalla, es el momento de buscar los bots presentados a otras ediciones de r2wars para conocer las posibles capacidades de nuestros oponentes. Tras unas horas de leer artículos de compañeros, decidimos crear 2 bots con estrategias diferentes.

La primera idea fue un bot con las siguientes características:

  • Moverse por toda la arena sin llegar a crashear por sí mismo.
  • Escribir la mayor cantidad de datos en la arena usando la menor cantidad posible de bytes de código.

De esta forma maximizamos las posibilidades de cazar al oponente y reducimos las de ser cazado.

Cuando revisamos los bot de competiciones anteriores, nos llamó la atención una técnica común que consiste en guardar el payload del bot en los registros, para después pushear todos los registros y saltar a ejecutar al último pusheado. Esta técnico nos gustó mucho, ya que podíamos mantener una copia del código en los registros, donde los oponentes no pueden acceder en ningún momento. Además, mediante esta técnica es posible escribir muchos bytes en el stack con muy pocos bytes de código.

X86 BOT

Nuestro primer bot fue MacacoBOT (es el de color azul en el GIF):

mov eax, 0xffffffff
mov ebx, 1024
mov edx, 0x84
mov ebp, eax
mov esi, 0xe4ff5160
mov edi, 0x606060e3
mov ecx, 0x420fd439
call pwn
pwn:
pop esp
sub esp,4
pusha
mov esp, 0xa8
pusha
push ecx
jmp esp

Se compone de 3 fases:

  • Inicialización: Este bot se basa en el uso de la instrucción pusha (en la fase del bucle). Esta instrucción pushea todos los registros a la dirección de memoria que marca el stack pointer. La instrucción pusha es realmente buena, ya que permite escribir 32 bytes de memoria haciendo uso de un solo byte de código. Durante esta fase el bot rellena los registros con el código de la fase del bucle, algunos opcodes inválidos y algunas variables útiles, como el tamaño máximo de la arena o una dirección de memoria aleatoria usada para hacer la primera iteración del bucle.
mov eax, 0xffffffff ; instrucciones inválidas
mov ebx, 1024 ; tamaño de la arena
mov edx, 0x84 ; dirección de memoria aleatoria
mov ebp, eax ; instrucciones inválidas
mov esi, 0xe4ff5160
mov edi, 0x606060e3 ; código del bucle
mov ecx, 0x420fd439
  • Preparación y lanzado de ejecución: Durante esta fase el bot pushea todos los registros una vez y empieza a ejecutar el código del bucle por primera vez. Simplemente ajuste el stack pointer con una dirección de memoria hardcodeada y salta a ejecutar al último valor pusheado al stack. La dirección de memoria hardcodeada la cambiábamos entre cada ronda para evitar que nuestros rivales nos cazasen y no pudiesen predecir donde empieza a ejecutarse nuestro bot. Siempre elegíamos direcciones de memoria altas o bajas ya que a muchos oponentes les gusta empezar en esos rangos.
mov esp, 0x340
pushad
jmp esp
  • Bucle: La fase del bucle es más simple de lo que parece. Pushea todos los registros varias veces para escribir la mayor cantidad de datos posible en la arena. Antes de escribir, comprueba que no se va a salir de la arena y si es así, salta hasta el final, donde sigue iterando esta fase. El código de esta fase se guarda en los registros durante la fase de inicialización, concretamente en los registros ecx, edi y esi.
cmp esp, edx ;Comprueba si va a salirse de la arena
cmovb esp, ebx ; Si es así, se coloca en el final de la arena
pushal
pushal
pushal ; Escribe una gran cantidad de bytes en la arena
pushal
push ecx
jmp esp ;Vuelve a ejecutar la fase del bucle

ARM BOT

Para mostrar que también pueden participar bots en otras arquitecturas, nos hemos animado con Aarch64. Para aquellos que no lo conozcan, Aarch64 es un estado de ejecución de los procesadores ARMv8 que utiliza un juego de instrucciones de 64 bits.

Este bot se mueve entre las primeras y las últimas posiciones de la arena, con el fin de esconderse de los rivales. Además escribe basura (opcodes inválidos) en una única posición de todo el espacio de memoria.

Este bot tiene una particularidad, y es que carga las instrucciones que va a ejecutar en bucle desde la propia memoria. Estas instrucciones son las que se encuentran en la etiqueta loop.

; Fase de inicialización
adr x10, loop ; Referencia a las instrucciones del bucle
mov x0, 1008 ; Posición alta donde esconderse
eor x1, x1, x1 ; Posición baja donde esconderse
mov x4, 512 ; Posición donde escribir basura
sub x5, x5, 1 ; x5 = 0xffffffffffffffff (opcode inválido)
mov x6, x5

; Preparar y lanzar la ejecución
ldp x2, x3, [x10] ; cargar en x2 y x3 las instrucciones del bucle
stp x2, x3, [x0] ; cargar en memoria instrucciones del bucle
br x0 ; saltar a las instrucciones recién escritas

; Código que se ejecutará en bucle
loop:
eor x1, x1, x0 ; alternar entre 0 y 1008 para moverse
stp x5, x6, [x4] ; escribir basura
stp x2, x3, [x1] ; cargar las instrucciones en la otra parte de la memoria
br x1 ; saltar para volver a ejecutar el bucle

Durante la competición no llegamos a presentar ningún bot en ARM, sin embargo nos quedó el gusanillo por hacerlo. Por eso, creamos este bot para practicar con otras arquitecturas, para muchos, menos conocidas, y que nos sirven como punto de partida para seguir explorándolas.

CONCLUSIÓN

Para concluir nos gustaría decir que la experiencia de la r2con nos encantó, y que pudimos disfrutar de muchas charlas y actividades de gran calidad. Además, la competición a la que hemos querido acercaros se vivió con mucho entusiasmo entre los participantes, y sirve para ampliar nuestros horizontes de múltiples maneras.

Al fin y al cabo, este es el objetivo que debemos perseguir siempre.