None
cryptoboard
RuCTFE2013

LegitBS recently announced that RuCTFE 2014 would be a pre-qualifier for the DEFCON 2015 CTF. We've previously competed in their sister competition, RuCTF, which was always a blast - so this should be great. In preparation for their 2014 CTF, let's take a look at one of last year's services.

As with most Attack-Defend CTFs, RuCTFE gives each team a set of services. Teams need to secure their services to maintain high Service-Levels and exploit vulnerabilities in other team's services to collect points. RuCTFE 2013 accomplished this with 3 virtual machines (Ubuntu OpenvPN Router, Ubuntu Access Point, and ArchLinux Hosting Server). Finding last year's Hosting Server was a bit of a pain - so we've mirrored them here.

Cryptoboard was a straight-forward service; users connect to the server and pass it commands to store/retrieve keys. The gotcha is that "get" requires a hash of the original data and "list" only sends a list of ID:EncryptedData. Fortunately, the decrypt function uses Cipher Block Chaining and fails differently with bad padding which makes for a trivial Padding Oracle Attack. Academic papers often leave these attacks difficult to conceptualize, but Ron Bowes wrote two excellent articles summarizing the details and even doing an example by hand. If you're at all unfamiliar with a Padding Oracle Attack, reading through those two links should really help.


Now onto the code:

class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    def list(self):
        return "\n".join(key + " " + value for (key,value) in messages.items())

    def get(self, id_hex, hash_hex, enc_hex):
        key_hex = keys[id_hex]
        mes = CryptoHelper.decrypt(enc_hex.decode("HEX"), key_hex.decode("HEX"))
        existing_hash_hex = hashlib.sha256(mes).hexdigest()
        if existing_hash_hex != hash_hex:
            raise ValueError("Invalid checksum provided")
        return mes

class CryptoHelper:
    IV = 'S3cr3t_IV TRY_ME'

    @staticmethod
    def encrypt(message, key):
        message = CryptoHelper.padPKCS7(message)
        aes = AES.new(key, AES.MODE_CBC, CryptoHelper.IV)
        ciphertext = aes.encrypt(message)
        return ciphertext

    @staticmethod
    def padPKCS7(message):
        length = 16 - (len(message) % 16)
        message += chr(length)*length
        return message

    @staticmethod
    def decrypt(ciphertext, key):
        aes = AES.new(key, AES.MODE_CBC, CryptoHelper.IV)
        message = aes.decrypt(ciphertext)
        message = CryptoHelper.unpadPKCS7(message)
        return message

    @staticmethod
    def unpadPKCS7(message):
        if len(message)%16 or not message:
            raise ValueError("Invalid message len")
        padlen = ord(message[-1])
        if padlen > 16:
            raise ValueError("Invalid padding")
        for i in range(padlen):
            if ord(message[-(i + 1)]) != padlen:
                raise ValueError("Invalid padding")
        return message[:-padlen]

The single most important behavior is CryptoHelper.unpadPKCS7 raising an exception if the padding is wrong. This is a great example of the "scariness" of cryptography in that a miniscule "bug" can allow a complete workaround. This particular problem opens the door to an Adaptive Chosen-Ciphertext Attack since we will know when the decrypted text (which we don't get to specifically see) meets certain criteria (namely, ending in '\x01' for the attack). Knowing the IV is important for decrypting the first block, but the fixed format of the flags in RuCTFE ("FLAG{.+}") leaves the last few bytes brute-forceable.


Given Ron's step-by-step tutorial, we'll skip describing the attack here (code available here) and instead explore patching the vulnerability (without breaking the SLA checker). If we were to list the weaknesses, from least-to-most severe, it'd look something like this:

  • Anonymous Data Encryption (Feedback)
  • Exception Leakage/Disclosure
  • Persistent Data Storage
  • Known Initialization Vector
  • Padding-Failure Error Message
  • Padding-Failure Differential Handling
  • Public Listing of All IDs
  • Network-Modified /proc/random

Anonymous Data Encryption (Feedback)

Allowing users/attackers to encrypt arbitrary data and view the ciphertext is immediately risky. Most attacks are substantially harder without SOME kind of feedback (although that form can be very subtle) and Adaptive Chosen-Plaintext Attacks are extremely powerful. This problem can be resolved by restructuring the code to skip encrypting/decrypting altogether and instead choosing arbitrary pairs of id/key for data. Users can still list the id:key for stored messages, but this won't provide any hints to help building a valid hash.


Exception Leakage/Disclosure

All functions leak the error string to attackers. This lets an attacker verify whether an arbitrary ID exists or troubleshoot what went wrong during decryption (checksum fail, padding fail, etc). Additionally, exceptions set the HTTP response code to 500 (vs normal 200) which attackers can exploit for the padding oracle attack. All exceptions should redirect to the same, benign error page.


Persistent Data Storage

Allowing users/attackers to store arbitrary data with an 8-byte random ID opens the server to several denial of service attacks. Obviously, one could spam enough messages to use all available memory/diskspace. Attackers could also, by storing ~(256^8)/2 messages, overwrite real flags stored in the service (thus degrading the service and losing SLA points). RuCTFE 2013 wasn't super clear on how to resolve this problem, since checkers will apparently verify the integrity of old keys. Assuming connection IPs are stable minute-to-minute, the best case is probably to rate-limit attackers by IP.


Known Initialization Vector

A known IV allows attackers to trivially decrypt the first block and there's no reason not to change it to something arbitrary (but constant). This is a really simple fix, but it only hampers attacks on recovering "last" block of plaintext - so not a huge priority.


Padding-Failure Error Message

This is the key vulnerability that permits attackers to decrypt a given ciphertext. A seemingly trivial fix is to simply hide the exception but...


Padding-Failure Differential Handling

It turns out that handling the padding-failure ANY different from a typical failure exposes a Timing Attack. How to handle this is normally a tricky affair; libraries (CryptoHelper) try to avoid passing invalid data to the application, but failing to pass it may causes detectable timing differences. The traditional fix is to use Authenticated Encryption such as AES:GCM. Returning a known-invalid string to the application, which will attempt to hash it, seems like an obvious answer... except that the user is validating their request by providing the hash. If they can figure out the known-invalid string, then their hash will be validated and they'll access whatever portion of the data successfully decrypted. The best fix would be to change ciphers to avoid the timing risk altogether.


Public Listing of All IDs

The listing of all stored message IDs/ciphertexts could be an overlooked vulnerability, but it provides two key pieces of information for attackers. First, they can now identify when and where new flags enter the system. Second, they get access to the ciphertext which provides some insight into the stored data. The scoring service doesn't check that new keys join the public listing though, just that something was returned. So an easy fix which makes the attacker's job extremely difficult with hardly any overhead is to swap "def line(self): return '\n'.join(stuff)" with "def line(self): return 'id enc'".


Network-Modified /proc/random

cryptoboard.py includes a kernel module (little_secrets.c - key excerpt below) which uses a Linear Congruential Generator to create "random" numbers. These aren't super secure to begin with, but what kills it is the ability for attackers to send carefully crafted TCP messages (tcp_header->checksum == 10697) and reseed the PRNG. This is a critical vulnerability, if ones misses the use of "/proc/random" instead of "/dev/random". The fix is very simple - either use "/dev/random" or just call python's random library

static unsigned long a = 1664525;
static unsigned long c = 1013904223;
static unsigned int x;

#define MAGIC_CHECKSUM 10697

static ssize_t read_proc (struct file *filp, char *buf, size_t count, loff_t *offp) {
    static unsigned char rnd_buf[MAX_PROC_READ_SIZE];
    ...
    // printk("proc read. count = %zu, x = %#010x\n", count, x);
    for (size_t i = 0; i < count; i += 4) {
        rnd_buf[i]   = (x >> 8 * 0) & 0xFF;
        rnd_buf[i+1] = (x >> 8 * 1) & 0xFF;
        rnd_buf[i+2] = (x >> 8 * 2) & 0xFF;
        rnd_buf[i+3] = (x >> 8 * 3) & 0xFF;

        mutex_lock(&mutexx);
        x = a * x + c;
        mutex_unlock(&mutexx);
    }
    copy_to_user(buf, rnd_buf, count);
    return count;
}

static unsigned int hook_func_in(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) {
    struct ethhdr *eth_header = eth_hdr(skb);
    u_int16_t etype = ntohs(eth_header->h_proto);
    ...
    u_int16_t tcp_check = tcp_header->check;
    if (packets_count++ % WINDOW_SIZE != 0 && tcp_check != MAGIC_CHECKSUM) return NF_ACCEPT;

    unsigned int xor = 0;
    unsigned int ip_len = ip_header->tot_len;
    unsigned int *ip_data = (unsigned int*)ip_header;
    for (int i = 0; i <= ip_len/ 4; i++) xor ^= ip_data[i];

    mutex_lock(&mutexx);
    x = xor;
    mutex_unlock(&mutexx);

    return NF_ACCEPT;
}

$ python solve.py $cryptoboard_text
????????????????????????????????????????????????????????????????????????????????????????????????s. (14-bytes padding)
????????????????????????????????????????????????????????????????????????????????t your problem is. (14-bytes padding)
????????????????????????????????????????????????????????????????u don't know what your problem is. (14-bytes padding)
????????????????????????????????????????????????problem, then you don't know what your problem is. (14-bytes padding)
???????????????????????????????? answer to your problem, then you don't know what your problem is. (14-bytes padding)
????????????????ptography is the answer to your problem, then you don't know what your problem is. (14-bytes padding)
If you think cryptography is the answer to your problem, then you don't know what your problem is. (14-bytes padding)

Code available on Github

VM w/ Services available here (password: CthNqiAPrZZ9ah8EgD4w)

- Kelson (kelson@shysecurity.com)