home - links - about - /git/ - rss

Hack.lu 2020 - Pwnhub Collection [EN]

Pwnhub Collection

This was a crypto (with a bit of reverse at the start) challenge at Hack.lu 2020, solved by 24 teams.

We were given a binary, oracle and the connexion informations to interact with the same binary running on the server.

Taking a look at it in your favorite disassembler, we can notice that its compiled with AddressSanitizer, which makes the control-flow graph more complex.

In the main, we can see that it does the following :

  • It asks for a category and a link
  • It reads the content of a file src/key
  • Same with src/iv
  • Same with src/collection, but it tries to parse its content this time.
  • it does some formatting using sprintf
  • then some AES-CBC encryption, probably using the key and iv read before.
  • and finally prints the encrypted result

The parsing consist of repeating the same thing 6 times :

It starts by finding the next space in the data, then finding the next newline, and splitting the current line on the space. Then it stores the two elements resulting of the splits, and go to the next line. We can deduce that the src/collection file must have 6 lines, each composed of two words.

The program then sort and format the strings it just read, encrypt the result and prints it.

We can summarize what its doing by the following python scripts (without the sorting stuff, which does not really matters):

from Crypto.Cipher import AES

with open("src/key", "rb") as keyfd, open("src/iv", "rb") as ivfd, open("src/collection", "rb") as collfd:
    iv = ivfd.read(16)
    key = keyfd.read(16)

    print("category ?")
    cate = input()
    print("link ?")
    link = input()

    finalstr = cate + "{" + link + "}"
    for line in collfd.read().decode().strip().split("\n"):
        start,end = line.split(" ")
        finalstr += " " + start + "{" + end + "}"

    finalstr += "\x00" * (16 - (len(finalstr) % 16))

    print("Encrypted : {}".format(AES.new(key, AES.MODE_CBC, iv).encrypt(finalstr.encode()).hex()))

As our input is prepended to some string and then encrypted, that code is vulnerable to an "byte-at-a-time" attack, meaning we can take it as an encryption oracle (hence its name), and repeatedly call it with crafted categories and links to retrieve the content of the src/collection file on the server.

That type of attack is well described at many places on the net (its the same as the ECB-byte-at-a-time on cryptopals for instance) plus its not that hard to understand, so i wont describe it any further.

The only thing to watch out for is that our category and link are prepended as "category{link}", so we have to make sure that the link is aligned on a 16 bytes boundary in the final string, meaning we have to input a category 15 bytes long.

Here is my implementation of the attack :

from Crypto.Cipher import AES
from pwn import *

context.log_level = 'error'

# Stuff to test for the attack locally
LOCAL = False
finalstr = ""
with open("src/key", "rb") as keyfd, open("src/iv", "rb") as ivfd, open("src/collection", "rb") as collfd:
    iv = ivfd.read(16)
    key = keyfd.read(16)

    for line in collfd.read().decode().strip().split("\n"):
        start,end = line.split(" ")
        finalstr += " " + start + "{" + end + "}"

def local_oracle(cate, link):
    plain = cate + "{" + link + "}" + finalstr
    plain += "\x00" * (16 - (len(plain) % 16))

    enc = AES.new(key, AES.MODE_CBC, iv).encrypt(plain.encode()).hex()

    print("plain={}, len={}, enc={}".format(plain.encode().hex(), len(plain), enc))
    return enc


def remote_oracle(cate, payload):

    r = remote("flu.xxx", 2010)
    r.sendline(cate)
    r.sendline(payload)

    data = r.recvline().decode().lstrip("category? link? encrypted: ").strip()
    r.close()

    return data

def encryption_oracle(payload):

    category = "a"*15

    if LOCAL:
        return local_oracle(category, payload)[32:]
    else:
        return remote_oracle(category, payload)[32:]


def attack(blocksize, known):
    index = len(known) // blocksize
    prefix = 'a' * (blocksize - len(known) % blocksize - 1)
    lookup = {}

    substring = encryption_oracle(prefix)[32 * index:32 * (index + 1)]

    for char in range(0x20, 0x7f):
        char = chr(char)
        idx = encryption_oracle(prefix+known+char)[index * 32:(index + 1) * 32]

        if idx == substring:
            return char

plain = ''
while attack(16, plain) != None:
    print(plain)
    plain += attack(16, plain)

print("plain : ", plain)

It takes approximately ~45 minutes to retrieve the two first lines of the file (the flag is in the second line), and i didn't note it so i won't put it there :P.


Creative Commons License