HackTheBox CA 2022 - Space Pirate: Entrypoint

Within this post I’ll be doing a write up of the Space Pirate: Entrypoint challenge from the HackTheBox Cyber Apocalypse 2022 CTF competition (14/05/2022). This write up will be written according to my thought process whilst I was trying to complete the challenge.

Reconnaissance

First look

I started off by downloading the provided zip file from HackTheBox’s CTF platform and unzipping it. Within the zip file was a binary called ‘sp_entrypoint’ along with a glibc folder providing runtime .so libraries to allow the binary to run. Firstly, I decided to take a look at the binary file using the tool ‘strings’ to view all the printable strings present in the file.

The output of strings with sp_entrypoint as the input file

The ‘0nlyTh30r1g1n4lCr3wM3mb3r5C4nP455’ string seemed interesting, so I tried to run the program and enter it when prompted for a password!

A failed attempt to gain privileged access to the program

Damn, it seemed that it wouldn’t be that easy. I had to take a closer look at the program.

Analysis

After opening up the binary with radare2, analysing it, seeking to the ‘main’ function and printing the disassembled function, I began looking through the contents of the function. The following instructions in particular stood out to me:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
     ││    0x00000d66      ba1f000000     mov edx, 0x1f               ; size_t nbyte
     ││    0x00000d6b      4889c6         mov rsi, rax                ; void *buf
     ││    0x00000d6e      bf00000000     mov edi, 0                  ; int fildes
     ││    0x00000d73      e848fbffff     call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)
     ││    0x00000d78      488d3dd91800.  lea rdi, str._nYour_card_is:_ ; 0x2658 ; "\nYour card is: " ; const char *format
     ││    0x00000d7f      b800000000     mov eax, 0
     ││    0x00000d84      e817fbffff     call sym.imp.printf         ; int printf(const char *format)
     ││    0x00000d89      488d45d0       lea rax, [format]
     ││    0x00000d8d      4889c7         mov rdi, rax                ; const char *format
     ││    0x00000d90      b800000000     mov eax, 0
     ││    0x00000d95      e806fbffff     call sym.imp.printf         ; int printf(const char *format)
     ││    0x00000d9a      488b55c0       mov rdx, qword [var_40h]
     ││    0x00000d9e      b83713adde     mov eax, 0xdead1337
     ││    0x00000da3      4839c2         cmp rdx, rax
     ││┌─< 0x00000da6      752e           jne 0xdd6
    ┌────< 0x00000da8      eb46           jmp 0xdf0
				...
    └────> 0x00000df0      b800000000     mov eax, 0
          0x00000df5      e889fdffff     call sym.open_door

The ‘sym.open_door’ function reference looked interesting to me, so I decided to trace the jump instruction that lead to it. It seemed the function would be triggered if ‘[var_40h]’ variable was equal to the hex value ‘0xdead1337’. So perhaps it was possible to manipulate the value of that variable to get access?

[var_40h]

I looked further up in the function, namely near the start and saw that the ‘[var_40h]’ variable was being set there.

1
2
           0x00000d17      b8efbeadde     mov eax, 0xdeadbeef
           0x00000d1c      488945c0       mov qword [var_40h], rax

Unfortunately, it was being set to a hardcoded value of 0xdeadbeef and there doesn’t seem to be any way of modifying this variable myself, so this seemed like a dead end.

sym.check_pass

So I instead started looking around in other areas of the function and noticed the following function call.

1
    ││    0x00000daf      e895feffff     call sym.check_pass

Looking inside the ‘sym.check_pass’ function, the following instructions were of interest to me.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
           0x55e85ea00c81      488d45e0       lea rax, [var_20h]
           0x55e85ea00c85      ba0f000000     mov edx, 0xf            ; 15
           0x55e85ea00c8a      4889c6         mov rsi, rax
           0x55e85ea00c8d      bf00000000     mov edi, 0
           0x55e85ea00c92      e829fcffff     call sym.imp.read       ; ssize_t read(int fildes, void *buf, size_t nbyte)
           0x55e85ea00c97      488d45e0       lea rax, [var_20h]
           0x55e85ea00c9b      ba0f000000     mov edx, 0xf            ; 15
           0x55e85ea00ca0      4889c6         mov rsi, rax
           0x55e85ea00ca3      488d3dce1800.  lea rdi, str.0nlyTh30r1g1n4lCr3wM3mb3r5C4nP455 ; 0x55e85ea02578 ; "0nlyTh30r1g1n4lCr3wM3mb3r5C4nP455"
           0x55e85ea00caa      e8b1fbffff     call sym.imp.strncmp    ; int strncmp(const char *s1, const char *s2, size_t n)
           0x55e85ea00caf      85c0           test eax, eax
       ┌─< 0x55e85ea00cb1      7522           jne 0x55e85ea00cd5
          0x55e85ea00cb3      488d35e01800.  lea rsi, str.e_1_5_31m  ; 0x55e85ea0259a
          0x55e85ea00cba      488d3de71800.  lea rdi, str._n_s____Invalid_password__Intruder_detected____n ; 0x55e85ea025a8 ; "\n%s[-] Invalid password! Intruder detected! \U0001f6a8 \U0001f6a8\n"                                                
          0x55e85ea00cc1      b800000000     mov eax, 0
          0x55e85ea00cc6      e8d5fbffff     call sym.imp.printf     ; int printf(const char *format)
          0x55e85ea00ccb      bf391b0000     mov edi, 0x1b39         ; '9'
          0x55e85ea00cd0      e83bfcffff     call sym.imp.exit
       └─> 0x55e85ea00cd5      b800000000     mov eax, 0
           0x55e85ea00cda      e8a4feffff     call sym.open_door

Within this function I could see it was configured to do as the name implies, check the given input against a hardcoded password value. However, looking closer, I realised there was something wrong with the logic in the program. First, it was comparing my inputted string with a hardcoded string using strncmp, which returns 0 in ’eax’ if the strings are the same. Then the ’test’ instruction is checking if ’eax’ holds a value of zero, as it performs a bitwise AND and then sets the ‘ZF’ (zero flag) if the result is zero. This works because the only possible input values which result in zero with a bitwise AND is two zeroes. Then, instead of the expected ‘je’ (jump equal) instruction, which would jump if the ZF is set (meaning the strings are the same), there is a ‘jne’ (jump not equal) instruction. This means that it will trigger if the strings are not equal, instead of being equal, which is likely an error, as it’s the opposite of traditionally expected functionality. This jump instruction went right to a function call:

1
2
       └─> 0x55e85ea00cd5      b800000000     mov eax, 0
           0x55e85ea00cda      e8a4feffff     call sym.open_door

sym.open_door

Printing the contents of the sym.open_door function yielded the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
            ; CALL XREF from sym.check_pass @ 0x55e85ea00cda
            ; CALL XREF from main @ 0x55e85ea00df5
 82: sym.open_door ();
           ; var int64_t var_8h @ rbp-0x8
           0x55e85ea00b83      55             push rbp
           0x55e85ea00b84      4889e5         mov rbp, rsp
           0x55e85ea00b87      4883ec10       sub rsp, 0x10
           0x55e85ea00b8b      64488b042528.  mov rax, qword fs:[0x28]
           0x55e85ea00b94      488945f8       mov qword [var_8h], rax
           0x55e85ea00b98      31c0           xor eax, eax
           0x55e85ea00b9a      488d35170300.  lea rsi, str.e_1_32m    ; 0x55e85ea00eb8
           0x55e85ea00ba1      488d3d701900.  lea rdi, str._n_s___Door_opened__you_can_proceed_with_the_passphrase:_ ; 0x55e85ea02518 ; "\n%s[+] Door opened, you can proceed with the passphrase: "                                                 
           0x55e85ea00ba8      b800000000     mov eax, 0
           0x55e85ea00bad      e8eefcffff     call sym.imp.printf     ; int printf(const char *format)
           0x55e85ea00bb2      488d3d991900.  lea rdi, str.cat_flag   ; 0x55e85ea02552 ; "cat flag*"
           0x55e85ea00bb9      e8d2fcffff     call sym.imp.system     ; int system(const char *string)
           0x55e85ea00bbe      90             nop
           0x55e85ea00bbf      488b45f8       mov rax, qword [var_8h]
           0x55e85ea00bc3      644833042528.  xor rax, qword fs:[0x28]
       ┌─< 0x55e85ea00bcc      7405           je 0x55e85ea00bd3
          0x55e85ea00bce      e8adfcffff     call sym.imp.__stack_chk_fail
       └─> 0x55e85ea00bd3      c9             leave
           0x55e85ea00bd4      c3             ret

So it seems this function will provide us with the flag when called because of the system call on line 16! And all I needed to do to trigger this was enter a string not equal to the hardcoded one due to the inverted logic! Brilliant!

Exploitation

So now all I need to do is exploit the binary. I connected to the container running on the CTF platform and set about exploiting it.

A successful attempt in exploiting the challenge

Bingo! All I had to enter was 2 to choose to enter a password and any string I wanted after as the password to get access because of the inverted logic, great!

Mitigations

Inverted password check

The behaviour of the ‘check_pass’ function should be changed to only call ‘open_door’ if the entered password is equal to the “0nlyTh30r1g1n4lCr3wM3mb3r5C4nP455” string. This can be achieved by replacing the ‘jne’ (jump not equal) instruction with a ‘je’ (jump equal) instruction instead.

Monero

Monero

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy