Misfortune CTF: x86-64 Binary Exploitation ft. Ret2libc, ROP, pwntools

The binary I will be going over here is ‘Misfortune’, an x86-64 (64-bit) binary exploitation challenge, by John Hammond who has a video going over his challenge and this topic in depth here. I am documenting this to reinforce my own learning and share my notes for anyone else interested in learning about Return Oriented Programming (ROP) in binary exploitation scenarios concerning a typical return2libc attack- including the use of pwntools and Python.

You can obtain the binary for this challenge from John’s GitHub repository here and we can clone the repository with git clone https://github.com/JohnHammond/misfortune-ctf-challenge.git.

Installation of PWNTOOLs for Python Integrated Exploit Development:

You should be able to install pwntools with pip by sending: sudo pip install pwntools (if it fails add —break-system-packages).

Source to the documentation for pwntools: https://docs.pwntools.com/en/stable/

Setting up the LOCAL instance of this challenge:

We are going to setup the docker container image in the background to run the binary exploitation challenge so we can remotely interact with it, and be able to specify local as an argument to interact with the challenge that way as well. You can follow the Docker installation instructions here as we are using a Dockerfile to build out the CTF for us and it will build the binaries for us in the ‘play’ folder.

We now have the misfortune binary and the libc.so.6 file. The latter file is the shared library for the GNU C Library (glibc) in Linux. It provides essential functions for C programs such as system calls, memory management, input / output operations, and string handling. Nearly every program on Linux relies on these for functionality.

Setup PWNINIT to Automate Starting Binary Exploitation Challenges:

This is a tool for the automation of beginning binary exploitation challenges that will will automatically determine the binary, the libc that is present there, acquire the linker (LD) that is associated and automatically run Patch Elf and create a misfortuned_patched binary that will work when crafting exploits and is more cooperative for a remote service (like docker) and builds out a solve.py script where you can start filling in the blanks with your challenge context and it has most of the Python written for you. This skeleton script saves a lot of time as you’re going to be building it out anyways. You can find the release here: https://github.com/io12/pwninit

It makes a lot of sense to place this into your /opt/ directory and call that as /opt/pwninit in the directory where the binary is present.

See everything we have after running pwninit in the current working directory.

This is what the generated solve.py script looks like. Notice it has all of the binary contexts set up for us and it starts to stage different connections we may want to use whether it is a remote or local binary. This is helpful when you’re connecting to a CTF competition endpoint with a tool like Netcat or something where you need to setup a “connection”.

Now that we’re good to go and can interact with the challenge, we can move forward with the binary exploitation locally.

What Kind of Binary is This?

We can see information about the file type and about the binary with the file command. We notice that misfortune is not stripped whereas the other binary is stripped, this is important for allowing us to see more information contextually when debugging a program.

If we run a utility like dmesg against the binary to obtian information from the kernel about error messages and other information that may get logged about the nature of the binary we’ve just ran we can do so and view that with dmesg | tail. We do not get any information that is directly helpful here.

We can use CHECKSEC on the binary to tell us more about the security details pertaining to a binary. We will notice that we have NX on. This means we will not be able to use shellcode that utilizes a return instruction like in our previous buffer overflow post. I used BASH to make the output better looking.

We are going to have to use Return Oriented Programming (ROP) and figure out what functions in lib.c can be used to stage to obtain a shell. We can find these ROPGADETS on the command line by doing: ROPgadget —binary misfortune (this is available due to the pwntools installation).

In x64 bit calling conventions when we want to end up calling another function we want to pass in parameters to these such as /bin/sh or bash to obtain a shell. In these type of calling conventions, we can use RDI as the first argument to pass those through.

Because we’re using Return Oriented Programming (ROP) we can add another ret (return) instruction without our payload falling apart due to memory addresses and such changing due to NX. We can automate the workflows of the mitigations to these concerns with PWNTOOLS. We will revisit this idea later as we’re going to have to take advantage of ROP due to the NX mitigation.

I changed this to a locally running version- you will notice I removed the optional arguments that would interact with the Docker container components of this challenge. Adding in the GDB Debug gdb.debug() to the executable will have a whole new window pop up that lets us interact with the program more manually.

When we run the challenge on its own without the script you will see that it closes out quickly. It is expecting an input but it is timed to close out.

Starting with Binary Exploitation.

Our goal is going to be to send 90 ASCII A characters into the program in order to attempt to crash the program and overflow some memory locations, registers, like RSP (x86-64 version) or ESP (x86 version). We will receive data until we hit the prompt of > with r.recvuntil() and use r.send() to send the As 90 times with Python.

If we enter continue once we will make changes where the register values will change. You will see that we filled the RBP register with 0x4141414141414141 over and over. Those are our A characters in hexadecimal. When we enter continue again (c) we will reach a segmentation fault.

Now we want to generate a unique pattern to see where exactly the “sweet spot” is that breaks out program by overflowing the instruction program- using cyclic patterns we can achieve this. We will build a variable holding the cyclic pattern ‘payload’ and then send the payload to the program. We will continue on the program and call an info registers command to see what happened.

We can see the pattern appearing in the registers. We can now copy the whole thing from 0x to the last 9 on the first column and we will put it into a script on the side (a small one) that will print out the real number of the hexadecimal pattern- if you have pwntools installed correctly (unlike me) you can run cyclic -l 0x61616161a for example.

Then you can run this in the terminal and it will output a number like 36. That number will be incorrect because we’re dealing with pairing of two for the hexadecimal number and ‘a’ is inherently not a part of the pattern. So we will change it to 0x61616169 and that will show as 32. We’re taking that 9 from how the pattern ends in the registers photo we have.

Now we will define an offset variable (where our program starts to break and allow us to reach other areas of memory) of 32. Then a length of 90 (what we were sending previously). Then we will amend our payload to reflect that and add B’s as well to validate where we’re hitting. We will also add some more padding to the original length of the data we sent out to begin with.

We can see that we have this instruction pointer controlled that will allow us to bounce to other locations. The way we can move around this program is through Return Oriented Programming (ROP) that will chain locations in code that will use a return instruction because NX is on. When we ran checksec we were not able to run code off the stack. Then we need to figure out what functions exist in LIBC that we can take advantage of.

Implementing Return Oriented Programming (ROP / ROPGadgets)

At this point is it probably worth deferring to the ROPGadget discussion above. Within PWNTOOLs we’re able to setup a ROP object. We can point to the RDI and RET where we can print the value and names of these variables.

Enter ‘q’ into GDB in the terminal that opens. We can look at the terminal holding the script being ran itself and we see that we have the name and value of the variables we requested from the program. Those are the same spots we saw in the ROPGadget output that we showed earlier near the beginning but its important to see how PWNTOOLs makes this less confusing and faster.

ROP lets us move around the program. We can demonstrate this concept by creating a situation where we can call the main function of the program twice.

We can call the main function in the program because the symbols are not stripped in this binary. We can try to bounce around and call the main function multiple times. We can change the payload to no longer use BBBB and call the main_function and because we’re extracting the numeric values for ROP Gadgets so we need to pack them into the binary format with a convenience function like p64() to pack it into a 64-but representation.

The prompt will also be saved into its own variable and we’ll decode it using utf-8 because its bytes. So when we go interactive again it will ask us the same question because we’re jumping into the ROP functionality.

You can see that we’re going to call that main function again in the terminal below the one that pops up for debugging, after we run ‘c’ to continue.

Finding Where a Vulnerable Function is to Stage an Attack (return-to-libc):

The binary can pull out worthwhile functions like “puts”, that could be a function inside the exe in symbols. Our goal will be to use the puts function from the PLT (Procedural Linkage Table) to find an address in the GOT (Global Offset Table) where we may be able to get a new function address loaded in LIBC. There are some other functions we may be able to use like ‘alarm’, where we saw it used earlier where it was timing out for the program if we did not feed information fast enough.

This is a classic approach in ret2libc (return-to-libc) attacks, where you use known functions to resolve the base address of libc, allowing you to call other libc functions (like system). Deep Dive video on this: https://www.youtube.com/watch?v=tMN5N5oid2c

We can then acquire the addresses we were looking for once we run the code that is modified above. Now we can force the program to use PUTS the function to display the address of the alarm function in the GOT during its runtime. We need to pass in an argument now.

We can use our pop_rdi instruction that lets us add something into an argument here, where we can use the alarm_got as argument we want to give to the next puts function we’re calling. After that function we’re going to want to return to a safe location after its executed like the main function. After that function runs we still want to return to a safe function like the main function.

We’re able to get the application to spit out the alarm GOT function memory address- in bytes that represent the new address we want to call. We are using alarm as an example because it is loaded in LIBC. PWNTOOLs only knows offsets to differnet locations and how it may reach anything else in the program- it does not keep track of the ‘you are here’ location in the map of things. So now that we have that location we can retrieve this when our payload returns.

Our goal now is to leak the during runtime address of the alarm function. We can display the address using the same method we’ve been using for ‘success’. There is stripping to remove newlines, left justify (ljust()) 8 bytes with null bytes at the end to carve out the unpacked memory address (now we’re using u64() instead of p64() address). Then we can enter ‘c’ or continue in order to execute through our intended exploitation after our code is modified and we execute the script.

Now we have the address for real that could potentially have a function there. We can go to GDB and use disas to dissassemble that function and see what it is.

When we disassemble on that memory address we call in GDB we are able to confirm that is in fact the alarm function. Now we can figure out where the base of LIBC is because from our object in PWNTOOLs we know the offset of the function but not where the address base will be.

Now we know the offset of that function but we don’t know where the address base will be. So we can say libc_base is equal to the one we just retrived but subtract the value of the libc.symbols.alarm object (what pwntools finds), and set it to the libc objects address to stage all the other calculations we want to do after properly aligning libc and all the function mappings.

We were able to find the LBICBASE address. Trailing zeroes too, almost perfectly aligned, is a libcbase.

Now we can try to find what the real system address is during runtime by taking the same symbol syntax but since we set the anchor for where we are we can grab the system function. LIBC already has the /bin/sh present in the file contents itself. So we can pass that as a string to the libc function.

Here is the modified code for that purpose. When you run it send a ‘c’ or continue as we’ve been doing. Now we have the memory addresses of each.

We are able to receive both the SYSTEM address and the address of our string.

We can confirm that is what we’ve received by disassembling on the SYSTEM address and try to display the string with x/s 0x7f8b3b3b3d88. Now we are able to pop that into RDI for our first argument to call SYSTEM and spawn a shell.

Now we’re going to modify our payload. We are going to find the offset sweet spot again. We need to keep the stack alignment and use pop_rdi to add the /bin/sh syntax into the first argument then run the system function, for a trampoline like effect add another return, and remove our main function at the end. We will send that next payload too and keep the C buffer. We are putting everything underneath our success statement of bin_sh.

Now when we execute our Python script and enter a ‘c’ or continue statement we will notice that we ‘detach from process XYZ’ and we now have a shell spawn where we have command execution as the user you’re running as. If we ran this with the container running you’d spawn as the ‘challenge’ user and be able to run commands on the container. Here is the solution script: https://github.com/CommodoreAlex/Python/blob/master/misfortune.py

And that’s a wrap on my journey into the 'Misfortune' binary! This x86-64 challenge by John Hammond dives deep into the world of Return Oriented Programming (ROP) and the classic return2libc attack. John’s video on this topic is solid gold, and I highly recommend checking it out for anyone serious about binary exploitation. For me, documenting this process has been a way to lock in these techniques and share some hands-on insights for anyone looking to get into ROP attacks with tools like pwntools and Python. Hope you found this useful, and stay tuned for more notes from the trenches!

Previous
Previous

Huntress 2024 CTF: StackIT XOR Operation Challenge

Next
Next

Starting with Large Language Models (LLMs) using Hugging Face and PyTorch