How do I even pwn anything? Part 9— Getting RCE by Returning to LIBC

Daryl 🥖
4 min readJun 24, 2024

--

Explore how to perform Linux Binary Exploitation from Capture-the-Flag (CTF) competitions.

Click here for part 8!

Course Link

You can find all the files required for the exercises on GitHub.

Leaking Contents From GOT

Now that we understand how to bypass ASLR, and what the PLT and GOT is used for, we will now learn how to leak the contents of the global offset table.

Do you remember what puts does? It takes in a single parameter, an address and writes the contents of that location in memory as ASCII to STDOUT (until it hits a null-byte). This means that if the address we send to puts is the location of the function at the global offset table, we will be able to get the address of where the function is in LIBC! This can be done by generating a ROP chain to call puts and set the GOT as the parameter!

However, we will likely only have one gets function to abuse. What is the point of leaking an address if we cannot use it to spawn ourselves a shell? Is there a way to overflow the buffer another time?

That’s right! We can chain the vulnerable function into the ROP chain. This means that it will first call puts to leak the function address in LIBC, then jump back to the start of the function and run gets again!

ROP Chain to call gets() again

Since puts will write the address to STDOUT in ASCII, we can use the u32 function from Pwntools to convert it back into a number. We can then calculate the base address of LIBC.

>>> PUTS_LIBC = u32(b"\x44\x43\x42\x41")
>>> hex(PUTS_LIBC)
'0x41424344'

>>> libc = ELF("/usr/lib32/libc.so.6")
>>> libc.address = PUTS_LIBC - libc.sym["puts"]

Spawning A Shell

Since we know the version of LIBC, and the location of a function in LIBC, we can calculate the location of system . But how can we pass /bin/sh to the system function?

Well, LIBC contains the string /bin/sh in the binary itself. This is rather convenient as it allows us find and pass the address as the first parameter.

libc = ELF("./libc.so")
next(libc.search(b"/bin/sh\x00"))

Building the ROP chain again allows us to pop a shell.

ROP Chain to call system()

Exercise

Return To LIBC — Bypassing ASLR and executing system

You can find the binary and source code in at ret2libc32. If you deployed the services locally, this exercise can be accessed using nc localhost 30006

#include <stdio.h>
#include <stdlib.h>

void vuln() {
char buffer[64];
gets(buffer);
}

int main() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);

puts("Guess my name");
vuln();
puts("Wrong!");

return 0;
}

Solution

If you didn’t manage to pwn it, do not worry, look at the solution and see if you can replicate it for yourself.

You should be able to build the ROP Chain and leak the contents in the GOT address of 2 functions to retrieve the version of LIBC.

Leaking address of gets() and puts() from LIBC

When leaking the address, you might have notice that there are way more than 4 bytes.

Leaked addresses

Since puts keeps printing until it hits the a null-byte, we can just ignore any bytes after the first 4 bytes.

>>> u32(p.recvline().strip()[:4])

We can then use the LIBC Database to get the LIBC version. After some trial an error, the bottom 3 LIBCs can be used.

LIBC Database to get LIBC versions

Calculate the base address, and create a second ROP Chain that calls system with the address of /bin/sh to pop a shell!

Final solve script

--

--