Sunday, October 2, 2016

TUM CTF 2016 - totp writeup

Challenge Name: totp
Point Value: 100 Base + 100 Bonus
Description: It’s time to thank Google™ for making the Internet more secure for all of us.
Tags: web

The challenge is a website with a personal diary and user database.  We can create accounts and post messages to our diary.  A public Users page is listed, which shows the admin email address.  It's likely we need to login as an admin account and view their personal diary to find the flag.



The catch is that account logins use a Google Authenticator time-based one-time password (TOTP).  Usually, these are used in conjunction with a user selected password, but this site uses only the TOTP for login.


The Google Authenticator Wikipedia article, provided in the challenge description, explains how TOTP generates a secret key (displayed in the 2D barcode) at first creation, and that key is used along with the current time to generate 6 digit codes using HMAC every 30 seconds.  Looking at the source for the registration page, we see an example of one of these keys.

<div class="col-sm-9">
    <img src="https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=otpauth://totp/dairy?secret=6PP3WKGNATQYEV2V5ELFQPMD&choe=UTF-8">

    <input type="hidden" name="secret" value="6PP3WKGNATQYEV2V5ELFQPMD">
    <input type="password" id="password" placeholder="One Time Password" password="password" name="otp" required class="form-control">
</div>

Though a Google API is generating the barcode, the 120-bit (base32) secret key is being generated by the challenge site.  If we can guess what secret key was generated when the admin account was created, then we can create TOTP codes based off the current UTC time and log in as admin.  I believe this was the intent of the challenge designers, as they've conspicuously provided the time the admin account was created on the Users page.

Registered at: 2015-11-28 21:21

The key we eventually recover also gives a hint that a poor random number was used.  Without source code, there is still a lot of guessing involved in how the random numbers are generated by the site.  So while I continued to investigate that, I also setup a brute force password checker to run in the background, and that found the answer.

Brute Force


The TOTP number is 6 numeric digits, which gives 10^6 possible combinations, or 1,000,000.  Not a difficult brute force attack.  However, the correct number the server will accept changes ever 30 seconds.  But since that number is based off a SHA-1 HMAC, it has an even distribution. Each number in that 1,000,000 is just as likely to be correct for 30 seconds as any other number.  Let's say we can check 10,000 of the 1,000,000 keys within every 30 second period.  If we check the same 10,000 passwords every time (say 000 000 to 010 000) then after 50 attempts (1,000,000 / 10,000 / 2) we should have guessed the right password.  That's 50 attempts * 30 seconds, or 25 minutes, which is not long at all for an online brute force attack.

We could script this, but Burp Intruder has most of the tools we need already.  First, I benchmarked how fast Burp could send login requests from my connection to the web server.  I settled on 25 threads, which on my Internet connection, over 30 seconds could make about 3000 attempts.  Meaning an attack time closer to 84 minutes.  Next, it's not easy to script Burp to restart an Intruder attack every 30 seconds, exactly on the :00 and :30 marks.  So, I went the even lazier approach of trying all 1,000,000 passwords in a row.  But since the password the server picks is an even distribution, this shouldn't hurt our attack time too much in practice.

Lastly, playing with the site while using Burp Proxy showed no difference in the response from the "POST /?target=login.php" message on an unsuccessful login vs a successful login. Instead, the PHPSESSID returned in the cookie, has server side state, which marks the login as successful.  So we'd need to do an extra "GET /" request after every POST to check for success.  Well that would slow down the attack another 2x, so instead we'll check manually every 30 seconds or so.

So the simplified steps for the brute force attack are:

1) Generate a file of every possible TOTP possibility:

$ for i in {0..999999}; do printf "%06d\n" $i >> totp_999999.txt; done

2) Start Burp Intruder with POST login requests against the web server using the generated file:



3) Periodically refresh your web browser using the same PHPSESSID cookie as Burp:

I got lucky after about 75,000 request / 13 minutes.

4) Collect the flag:




Sunday, August 7, 2016

OpenCTF 2016 - diskomatic writeup

Challenge Name: diskomatic
Point Value: 200
Description: It slices, it dices, it juliennes fries!
Tags: forensics

The challenge is a binary file with a forensics tags, so I assumed it would be pulling some data out of some other file format.

Step 1:  Unpack

 

$ file diskomatic-98757b0043b8aea77e9014cff8ead7b1
diskomatic-98757b0043b8aea77e9014cff8ead7b1: gzip compressed data, last modified: Mon Jun 22 00:08:37 2015, max compression, from Unix

Simple enough.

$ zcat diskomatic-98757b0043b8aea77e9014cff8ead7b1 > diskomatic.dat

$ file diskomatic.dat diskomatic.dat: DOS/MBR boot sector, code offset 0x58+2, OEM-ID "mkdosfs", FAT  1, Media descriptor 0xf8, sectors/track 32, heads 64, sectors 250368 (volumes > 32 MB) , FAT (32 bit), sectors/FAT 1941, serial number 0x20550574, label: "DISKOMATIC "

 

Step 2:  Mount Filesystem

 

So we have a FAT filesystem image.  Let's try mounting it and seeing what's inside.

$ sudo mount -t vfat ./diskomatic.dat ~/mnt/dos/
$ ls -la ~/mnt/dos
total 5
drwxr-xr-x 2 root  root   512 Dec 31  1969 .
drwxr-xr-x 3 grind grind 4096 Aug  6 12:41 ..
$ df -h | grep dos
/dev/loop0      122M   512  122M   1% /home/grind/mnt/dos
$ sudo umount ~/mnt/dos

Interesting so from the filesystem's perspective there are no valid files, but we have 122MB of something.

 

Step 3: Analyze the Binary

 

$ binwalk -e diskomatic.dat

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
17787904      0x10F6C00       PNG image, 1600 x 900, 8-bit/color RGB, non-interlaced
34088942      0x20827EE       Minix filesystem, V1, little endian, 30 char names, 23429 zones

binwalk found a PNG file, but was unable to extract it.  Interesting... perhaps it is corrupted somehow. I don't know where the Minix FS detection came from, but it's a false positive.

So let's open up the file in a hex editor and take a look at location 0x10F6C00.

 

Step 4:  Hex Editor



The beginning of the file is as `file` advertised, a FAT32 partition table.  Let's go to location 0x10F6C00.


Here's the PNG header, with the first IDAT section but it is noticeably quite short.  Searching for IDAT we find many more of these sections.  Each one is exactly 512B, which I believe is the block size for FAT32.  So it seems we have a PNG file which has been fragmented and we need to piece it back together.  Well let's try the naieve approach, and just concatenate all the chunks.  We can do this from within the hex editor by cropping the file from location 0x10F6C00 onward and then doing a find and replace on all the \x00\x00\x00\x00 words with nothing.

We'll save this file as diskomatic.png and open it.


Well that didn't work. But the CRC error is a great hint.  PNG is a simple format with chunks split into just 4 fields.

Length Chunk type Chunk data CRC
4 bytes 4 bytes Length bytes 4 bytes

But you don't have to take my word for it, you can read the Wikipedia article.

Looking at the 512B blocks in the hex editor again, we see that the PNG chunks are split in the middle of the data section, leaving some data and the CRC for that data at the front of the next block.  So we have a fragmented file, but we have a method of determining in which order the pieces are supposed to fit back together by combining two blocks and then checking whether the CRC for the PNG chunk is correct.  So now we have an idea of the problem and a solution, but first we'll need to extract each block of the PNG file.

 

Step 5: Programmatically Extracting FAT32 Blocks

 

At first I just selected a few blocks in the hex editor and saved them as new files.  But after about 6 of these, with the location not moving much, I realized I'd probably need to automate this.

$ grep -abc IDAT diskomatic.dat
4976

Yep, I definitely don't want to copy'n'paste 5000 times by hand.  So I wrote some Python:
FILE = "diskomatic.dat"

def get_parts():
    fh = open(FILE, 'rb')

    parts = []

    # we know the location of the first part from `binwalk`
    fh.seek(0x010F6C00)
    p_idx = 0
    part = ""

    try:
        while True:
            block = fh.read(512)
            if len(block) == 0:
                break
            if block[0:4] != "\x00\x00\x00\x00":
                fh2 = open("parts/p"+str(p_idx)+".png.part", "wb")
                fh2.write(block)
                fh2.close()
                p_idx += 1
            offset = fh.tell()
            if offset % 0x100000 == 0:
                print "0x%x" % offset
    except EOFError:
        fh.close()

This writes all the blocks/parts out, 1 per file, and puts them in a directory called parts. After running this code we have 6238 files.

$ cd parts
$ ls | wc
   6238   56144  354468

 

Step 6:  Reassemble the Blocks

 

First I tested my hypothesis that all the blocks we needed were present, by programmatically concatenating every block with the first block, and was successful finding one block.

From that code, I moved on to checking every block against every other block, and constructing the correct ordering of blocks in a dictionary.  Then writing that string of blocks out into a single PNG file.  The full code, excluding the get_parts() function is:
#!/usr/bin/env python

import binascii
import struct

FILE = "diskomatic.dat"
NUM_CHUNKS = 6238
FIRST_PART = 0

def tokenize_chunk(data):
    length = struct.unpack("!I", data[0:4])[0]
    chunk = data[0:4+4+length+4]

    return chunk


def png_chunk_verify_crc(chunk):
    length = struct.unpack("!I", chunk[0:4])[0]
    ctype = chunk[4:8]
    data = chunk[8:8+length]
    crc = struct.unpack("!I", chunk[8+length:8+length+4])[0]

    # The CRC is a network-byte-order CRC-32 computed over the chunk type and
    # chunk data, but not the length.
    crc_c = (binascii.crc32(chunk[4:-4]) &amp; 0xFFFFFFFF)

    valid = (crc == crc_c)

    return valid


def read_parts():
    parts = []

    # XXX: magic number: that's how many chunks were in the original file
    for i in xrange(0, NUM_CHUNKS):
        fh = open("parts/p"+str(i)+".png.part", "rb")
        data = bytearray(fh.read())
        parts.append(data)
        fh.close()

    return parts


def compare_parts(parts):
    parts_dict = {}

    for i in xrange(1, NUM_CHUNKS):
        first_two = parts[0] + parts[i]
        # XXX: magic number, for first one, because there's the PNG header
        chunk = tokenize_chunk(first_two[0x2E:])
        matched = png_chunk_verify_crc(chunk)
        if matched:
            print "Chunk 0 and %d matched!" % i
            parts_dict[0] = i
            break

    for i in xrange(1, NUM_CHUNKS):
        matched = False
        for j in xrange(1, NUM_CHUNKS):
            if i == j:
                continue
            two_parts = parts[i] + parts[j]
            # XXX: magic number, most other parts seem to have the same offset
            offset_to_chunk = 0x2E
            chunk = tokenize_chunk(two_parts[offset_to_chunk:]) 
            matched = png_chunk_verify_crc(chunk)
            if matched:
                if i % 100 == 0:
                    print "Chunk %d and %d matched!" % (i, j)
                parts_dict[i] = j
                break

        if not matched:
            print "Chunk %d had no match!" % (i)

    return parts_dict


def write_parts(parts, parts_dict):
    fh = open("final.png", "wb")

    next_key = 0
    for i in parts_dict.iterkeys():
        fh.write(parts[next_key])
        try:
            next_key = parts_dict[next_key]
        except KeyError:
            print "Hit KeyError"

    fh.close()


def main():
    parts = read_parts()
    parts_dict = compare_parts(parts)
    write_parts(parts, parts_dict)


if __name__ == "__main__":
    main()

The nested for loops do some unnecessary work, but the whole thing runs in under 5 minutes on my laptop.

 

Step 7:  Collect the Flag

 

Now we have a (hopefully) valid PNG. Let's open it.


Friday, June 3, 2016

Running IDA Evaluation Version on Kali Linux 2016.1 64-bit

The IDA Evaluation Version previously came installed with Kali Linux 1.0, but since the upgrade to 2.0 and now Rolling Edition, IDA is no longer present.  Since the evaluation version is available as 32-bit binaries only, getting it running requires figuring out the rather large set of dependent 32-bit libraries that must be installed on Kali 64-bit.

For a quick fix, run the following commands:

$ sudo dpkg --add-architecture i386
$ sudo apt-get update
$ sudo apt-get install libglib2.0-0:i386 libx11-xcb1:i386 libxi6:i386 libsm6:i386 libfontconfig1:i386 libqt5gui5:i386

Now IDA should successfully execute from the CLI and give you a graphical window to accept the IDA License Agreement.

~/Downloads/idademo69$ ./idaq

How to determine what libraries are missing and which packages provide them involves the iterative process of:
  1. Check for missing shared objects
  2. Check which package provides them
  3. Install that package
For example:

~/Downloads/idademo69$ ldd idaq | grep "not found"
    libgobject-2.0.so.0 => not found
    libgthread-2.0.so.0 => not found
    libglib-2.0.so.0 => not found
    libXext.so.6 => not found
    libX11.so.6 => not found
    libgthread-2.0.so.0 => not found
    libglib-2.0.so.0 => not found
~/Downloads/idademo69$ dpkg -S libXext.so.6
libxext6:amd64: /usr/lib/x86_64-linux-gnu/libXext.so.6.4.0
libxext6:amd64: /usr/lib/x86_64-linux-gnu/libXext.so.6
~/Downloads/idademo69$ sudo apt-get install libxext6:i386

 

References