As a beginner in the CTF world, I just skipped the ‘pwn’ and ‘rev’ categories. The challenges seemed too hard for me to solve. This year, however, I decided that I needed to improve and be able to at least solve some basic ones. With that in mind, I participated to Sin__’s beginner rev course, which included as much pwn concepts as reversing ones. When I saw the baby-rop challege during the ECSC 2020 Quals, I knew I needed to solve it.
EDIT: It is worth mentioning that one_gadget is not the ideal tool in this situation because the newer versions of libc introduce a lot of constraints. The easiest solution would be to use system() from LIBC with “/bin/sh” as an argument.
Mission Briefing
At the time of writing this article, the challenge files & description are available on CyberEDU. If you want to follow this article, you can get the binary from there (it’s free!). Running checksec returns the following output:
The program is pretty basic; it prints some text and then reads user input:
Crashing the Application
As the name of the challege suggests, the binary is vulnerable to a stack overflow vulnerability which can be triggered by simply inputting 1024 ‘A’s:
To make things easier, I made the following python script:
Besides crashing the app, the program above also attaches the GDB debugger to the application. This will be very helpful in the next steps. After running the script, a new window should pop up with the said debugger. For now, just input ‘c’ and press enter (it stands for ‘continue’ and tells the decompiler to continue the execution of the program that is being analyzed). The program will still crash, but the debugger will show the last assembly instruction that failed to execute:
Finding the Exact Offset for RIP
As you can see in the image above, the last instruction that is being executed is a return to 0x4141414141414141, which translates to 8 ‘A’ chars. Since the input consisted entirely of ‘A’s, we can assume that the input has overwritten the return instruction pointer (RIP) address on the stack.
To control the execution flow, we need to first find out the exact offset of the string that overwrites RIP. To do that, I used pwnlib’s cyclic() function:
As you can see in the example above, the function just generates a cyclic pattern. The cyclic_find function is able to find the position of a given substring in that pattern. The optional parameter ‘n’ basically tells pwnlib that we’re going to know at least 8 characters / bytes of the substring given to cyclic_find. Knowing this, we can easily modify crash.py to include cyclic():
After running it, we can see the return address changed to 0x6261616161616169:
The RIP offset can now be easily found using cyclic_find:
I used the following program to verify this offset:
The program above sends 264 ‘A’s (you could call that a padding) and then 8 ‘B’s. The RIP (which is 8 bytes long) will be overwritten with the ‘B’s only if the offset is correct.
It worked! This concludes our OSCP bof preparation :)
Okay, What Now?
NX is enabled, so we can’t just include some shellcode in the payload and then execute it. However, the eecutable loads the LIBC library, which contains a lot of functions that execute OS commands:
Another problem arises: LIBC is loaded dynamically, meaning that the base address changes every time the binary is executed. We can’t call a function like system without knowing its address.
To solve this problem, we need to leak the address of a function that was loaded (used) in the program. Knowing the LIBC version (more on that later), we can find the offset of that function online and use it to calculate the base of LIBC, which can then be used to calculate the address of any function in the library. I hope that makes sense :)
The functions that are loaded by the binary can be viewed using objdump:
Since puts is the only function that prints something on the screen, we can safely assume it was used to print the ‘black magic’ text (this theory can also be confirmed using a decompiler like IDA or Ghidra). This means that it was called before gets (the function vulnerable to bof) and it will already be loaded when we overwrite the RIP. We are basically going to call puts(&puts).
We will have to overflow the buffer a second time to execute the LIBC functions required for command execution after calculating their addresses. This can’t be done in one go: we first need to get the output of the puts function to build the second payload. To do this, we can simply call main() again.
Getting the Required Addresses
The binary is stripped, so objdump and grep alone won’t do the trick. I found the main() address by opening the binary in IDA and looking throught the loaded functions.
We also need to find the addresses of puts and puts_got (the GOT entry of puts, which contains the address of puts in LIBC). Fortunately, this can be achieved using objdump and grep:
If we want to call puts, we should call 0x401060, which calls the value stored at 0x404018 (puts@got). There is only one thing missing: a gadget. When a fnction like puts is called, it loads its parameters from registers mentioned in the system’s call convention (in this case, RDI, RSI, RDX, RCX, R8, R9, etc. ). To give puts@got as a parameter, we need to load the address into RDI. To find a ‘pop rdi; ret;’ gadget, I used rp++:
Okay, that was everything we needed for the first payload. Let’s see the script!
Leaking a LIBC Address
After plugging all the values obtained in the last step into the program, I got the following script:
Running the program outputs an address and prints the intro text a second time (thus letting me input another string):
It worked! How do I know that? Well, take a look at the command below:
In my LIBC version, the address of puts ends with 9c0. When the binary is loaded, the base address always ends in 000, so the address of puts will be something ending in 9c0 for my LIBC. However, there are multiple versions of LIBC, each with its different offsets. In order to find the one the remote program uses, we need to leak the remote program’s puts@GLIBC address. We can do that by simply chainging
to
I got the IP address and port from my CyberEDU dashboard. The program returns a completely different address:
I used this tool to find the potential versions of libc based on the last 3 nibbles of the address. The following results were shown:
In an ideal situation, there would be only one result. However, we can still find the good library using simple logic. The first 4 libraries are 32-bit, so they’re out. The last 3 have the same offsets, so any of them can be used. Just select one of them and click ‘download’.
Using the Server’s LIBC
After the LIBC library has been downloaded, we need to tell our computer to use that library instead of ours. To do that, we can simply modify the LD_PRELOAD environment variable by changing
to
After running the binary, we can see that the last 3 nibbles of the address are the same as the ones from the server, even though we run the binary on our machine:
Introducing one_gadget
It’s finally time to use one_gadget. The principle behind this program is simple: the LIBC library contains some addresses that, when called, will spawn a shell. However, like the others, these addresses have different offsets for every version, so the program also need the library or a build hash to perform its magic. Running it is very simple:
As you can see, there are 3 possible addresses which will do the job, each with different contraints. I chose to use the last one, but the others might also work. From the site listed above, we know puts’ offset is 0x0875a0, so we can calculate the address of this gadget. However, we still need to make sure the contraints are met.
Finding More Gadgets
First, RSI should be null or point to an address that has a value of 0. This gadget can be easily found using rp++:
The next constraint is that rdx should be null. However, the binary does not contain any gadget that assigns rdx to a value on the stack. Luckily for us, LIBC does:
I am going to use ‘pop rdx ; pop r12 ; ret ;’, but (again) the other gadgets might also work (I actually used ‘pop rdx ; pop rbx ; ret ;’ during the CTF). The following script uses the gadgets we’ve just found:
It Works! Oh, Wait, It Doesn’t
I tried to run the script above only to find out that the program crashes:
Please, don’t close this page just yet. This is a pretty common problem, so I decided to include it in this article. Let’s uncomment the line that attaches gdb to the process and re-run the program:
The instruction that crashed the program tried to access $rbp - 0x78. However, the value of the RBP register is 0x4141414141414141, which indicates that it has been overwritten by our payload. We need to set RBP to a writeable portion of the stack. gdb’s ‘vmmap’ command returns all memory blocks used by the program and their permissions, which is very helpful in this case:
Memory block 0x00404000-0x00405000 is readable and writeable, so it is a very good candidate, even though it is not exactly the stack. I chose to set RBP to 0x00404500, which is in the middle of that memory block. The string that overwirtes RBP is located exactly before the one that overwrites RIP, so we have to modify the payload accordingly.
The final script:
Running it on the remote address results in a shell:
Conclusion
Even though this challenge had the word ‘baby’ in its title, it took me about 5 hours to solve it. You can find the scripts used in this article on my GitHub. As always, you can message me on Twitter with any questions you might have.