Continuing the previous two ROP Emporium blog posts this will be the challenge 0x03, named Callme. If you don't know what ROP Emporium, Binary Ninja, Ghidra, or Pwntools are you might want to take a look at some previous blog posts here in order to catch up. The notes for this challenge say:
Well, this is a new topic that I need to learn about. Prepare for a wall of text. Let's do some digging into the Procedure Linkage Table (PLT) and the Global Offset Table (GOT). We need to take a moment to discuss the differences in dynamically linked and statically linked ELF binaries. Statically, as the name might indicate, contain all code necessary to run within the binary themselves. This means during execution the binary will not import any external libraries for use. On the other hand, dynamically linked binaries will import external libraries for use. In terms of common use, dynamically is far more prevalent than it's counterpart. Why is this? Let us take the example of a common function used in C programming, the
printf statement. On the majority of Linux systems this function is held within the library file
libc.so.6. So how does the C program know where to find that function? It needs the address in order to call it. Well that could be easy in the example of the statically linked. Why not just provide the address hardcoded into our C program? Well as soon as the
libc.so.6 library is updated, modified, moved, etc it will break our hardcoding. Also, we have to assume that in modern systems that Address Space Layout Randomization (ASLR) would be changing the base address on every reboot. Alright, but how does the dynamically linked program even know where the address is? The
gcc compiler provides a handy mechanism to call these functions. The mechanism is known as relocation, and has the hard work of doing this at runtime which is performed by the linker. The linker is found in
ld-linux.so. Every dynamically linked program will be linked against the linker, which is set in a specific section of the ELF known as the
Whew... that was a wall of text. Let's just jump into the actual code and see what all of that text actually means. I will be using Binary Ninja to investigate the ELF binary. The first screenshot shown below is the top of the compiled binary which has our magic bytes and defines the ELF type, the platform, and the architecture.
Directly below the magic bytes is the different sections and their respective addresses. As mentioned above, the first section
.interp is used to link functions to external sources using the linker. There are many sections here, while important we will not be focusing on all of the sections. The few sections we will be discussing in this blog post will be the
.got.plt, and the
.got file, also known as the Global Offset Table (GOT) is the actual table of offsets as filled by the linker for external symbols.
The next section is the
.plt or the Procedure Linkage Table (PLT), these are stubs that look up the addresses in the
.got.plt section, and either jump to the right address, or trigger the code in the linker to look up the address. The
.got.plt is the GOT for the PLT. It contains the target addresses (after they have been looked up) or an address back in the
.plt to trigger the lookup.
.plt.got contains code to jump to the first entry of the
.got. As mentioned in one of my sources, not exactly sure if anything uses the
.plt.got or if it is positioned to hold information for a later time.
In order to visualize the linkage we will need to step through this binary using GDB and the extension Python Exploit Development Assistance (PEDA) for GDB. In case you are following along and notice that the output looks a bit different, that is why. I will use the command
disassemble main to see the main function. As you can see in the screenshot below we have functions that are local to the binary such as
pwnme, but other functions are being called by the address contained within the PLT, such as
We can set a breakpoint at the first function that calls a function from PLT
setvbuf to see what is actually happening. The screenshot below might look a bit messy, but it can be broken down as the registers, followed by a short disassembly snippet. At the bottom of the screenshot we see in green the current line where our breakpoint has been hit. We are set to call the
setvbuf@plt function. The address that is being called is at
0x401860 which is contained within our PLT.
If we take another step the assembly will step into the PLT to call the actual function. The
RIP register shows that our next instruction will be inside the PLT. Cool, so we made to our function.
The screenshot above shows the jump that is about to be taken, but this is not a normal jump within our binary base address. As we can see in the comment section off to the side the pointer will get dereferenced and will jump to the address outside of our binary to
0x602050. My expectation is to jump into the address containing the
setvbuf@plt function. Let us test that theory.
Wait... what? we just stepped into the function, but it just went to the next instruction instead of jumping to the function. Why does this function like this? This is known as lazy loading. Since we haven't called the
setvbuf function before it first needs to trigger the lookup. We could continue to step through the linker to eventually get to the address, or we for the brevity of this blog post we can assume that the linker will in fact get the address for the function
Alright, we just went through that whole explanation of PLT and GOT, what the heck does this have to do with ROP Emporium? Well if we look at the files included in our Callme challenge, we have several additional files that are included. As shown in the screenshot below.
Let's investigate what these extra files are doing by opening them in Binary Ninja and Ghidra. When opening the Callme binary in Ghidra it shows an import for the
libcallme.so file. Which can be seen inside the Symbol Tree, under Imports.
libcallme.so file it shows the functions that are being exported for use in the screenshot below.
So what are these functions actually doing? In order to figure out how they are being used, we can start by investigating the primary binary and stepping through the program execution. Once we click through the
__libc_start_main and get to the
main function we can see that it calls a function called
Following the link to the new
pwnme function we see a few more lines, and then the
fgets function which is where the overflow will happen.
As mentioned above, much like the other ROP Emporium challenges the
fgets will be our entry point for exploitation. As we can see in the screenshot above on line 5, the binary creates a buffer of 32 bytes, and then reads in 0x100 or 256 bytes. We can test that this is actually our entry point by running the binary and sending more than 32 characters, which should result in a segmentation fault.
Thus far, we have not found a way to print out the contents of our flag. Let's look at some of the other functions provided to us within the binary. The following three screenshots document the functions
callme_three. Within these functions we see that they take in 3 values, and then will open an encrypted flag. In order to decrypt the flag we need to use the next two functions to open subsequent data files that will then decrypt the encrypted flag.
In previous ROP Emporium challenges the binary provided us with an easy way to call the needed resources, typically from a function called
usefulFunction. The screenshot below demonstrates that this binary does in fact have the same function.
However, if you look closely at the screenshot above you will see that the
usefulFunction is calling the three decryption functions with the wrong values. In our Ghidra screenshot above we can note that the first action taken is to check and make sure the parameters are 1,2,3. In order to decrypt this flag we just need to call the functions ourselves. As I have learned recently Pwntools can automate the majority of this work for you. The following code snippet will produce the output of the following screenshot.
#!/usr/bin/env python from pwn import * #setup pwntools to work with binary elf = context.binary = ELF('callme') io = process(elf.path) # Pwntools can do most of the heavy lifting for us rop = ROP(elf) rop.callme_one(1,2,3) rop.callme_two(1,2,3) rop.callme_three(1,2,3) print(rop.dump())
Pwntools allows us to create a ROP object, and call functions by their name. Which will then convert everything to assembly instructions for us and setup the necessary arguments to pass to those functions. Now we just need to build our payload and send it to the binary.
If we open the binary in Binary Ninja while looking at the linear disassembly as well as the medium level output it will give us an understanding of how much space was allocated for the
fgets function. The screenshot below shows that the Frame offset for
-28 and the total size requested with
Let's wrap up this lengthy blog post and solve this challenge. Our final exploit code looks like this:
#!/usr/bin/env python from pwn import * #setup pwntools to work with binary elf = context.binary = ELF('callme') io = process(elf.path) # Pwntools can do most of the heavy lifting for us rop = ROP(elf) rop.callme_one(1,2,3) rop.callme_two(1,2,3) rop.callme_three(1,2,3) print(rop.dump()) # need to find the size of our buffer to crash. # build the payload stage1 = 'A'*0x28 + rop.chain() stage1 += 'B' * (0x100 - len(stage1)) # just gotta send it io.send(stage1) io.interactive()
Well we got through another ROP Emporium challenge, until next time keep on digging!