LighTender / March 17, 2024

Wolv CTF 2024

Cryptographie
AES_ECB
AES_CBC
HashExtended
Test alt

Sommaire

Cryptographie

Limited1

Enoncé :

It's pretty easy to find random integers if you know the seed, but what if every second has a different seed?

Code :

import time
import random
import sys



if __name__ == '__main__':
    flag = input("Flag? > ").encode('utf-8')
    correct = [189, 24, 103, 164, 36, 233, 227, 172, 244, 213, 61, 62, 84, 124, 242, 100, 22, 94, 108, 230, 24, 190, 23, 228, 24]
    time_cycle = int(time.time()) % 256
    if len(flag) != len(correct):
        print('Nope :(')
        sys.exit(1)
    for i in range(len(flag)):
        random.seed(i+time_cycle)
        if correct[i] != flag[i] ^ random.getrandbits(8):
            print('Nope :(')
            sys.exit(1)
    print(flag)

Découverte

On découvre ce chall, il y a plusieurs choses qu'on sait.

La longueur du flag, ici 25 caractères.

Le fait que chaque caractère soit chiffré avec une clé nommé correct.

On voit un random.seed on sait donc que le random.getrandbits(8) est prédictible si on connaît la seed, en l'occurrance ici, il est assez facile de retrouver la seed, car time_cycle est comprise entre 0 et 255 puisqu'on à cette ligne : time_cycle = int(time.time()) % 256

Ainsi, en faisant du brute force de la seed, on retrouvera le flag.

Il ne faut pas oublier une chose,

correct[i]=flag[i]bytescorrect[i] = flag[i] \oplus bytes

donc

flag[i]=correct[i]bytesflag[i] = correct[i] \oplus bytes

Résolution

import random

correct = [189, 24, 103, 164, 36, 233, 227, 172, 244, 213, 61, 62, 84, 124, 242, 100, 22, 94, 108, 230, 24, 190, 23, 228, 24]

for time_cycle in range(256):
    flag = []
    for i in range(len(correct)):
        random.seed(i+time_cycle)
        flag.append(correct[i] ^ random.getrandbits(8))
    try:
        print(bytes(flag).decode('utf-8'))
    except:
        pass

Flag

wctf{f34R_0f_m1ss1ng_0ut}

Limited2

Enoncé :

I was AFK when the flag was being encrypted, can you help me get it back?

Code :

import time
import random
import sys

if __name__ == '__main__':
    flag = input("Flag? > ").encode('utf-8')
    correct = [192, 123, 40, 205, 152, 229, 188, 64, 42, 166, 126, 125, 13, 187, 91]
    if len(flag) != len(correct):
        print('Nope :(')
        sys.exit(1)
    if time.gmtime().tm_year >= 2024 or time.gmtime().tm_year < 2023:
        print('Nope :(')
        sys.exit(1)
    if time.gmtime().tm_yday != 365 and time.gmtime().tm_yday != 366:
        print('Nope :(')
        sys.exit(1)
    for i in range(len(flag)):
        # Totally not right now
        time_current = int(time.time())
        random.seed(i+time_current)
        if correct[i] != flag[i] ^ random.getrandbits(8):
            print('Nope :(')
            sys.exit(1)
        time.sleep(random.randint(1, 60))
    print(flag)

Découverte

Même challenge que celui d'avant, seul différence, c'est sur le time, il faut que ce soit le 31 décembre 2023.

Résolution

J'ai juste pris le 30 décembre 2023 et le 1 janvier 2024 pour être sûr.

import time
import random
import sys

correct = [192, 123, 40, 205, 152, 229, 188, 64, 42, 166, 126, 125, 13, 187, 91]

year = 2023
day = 365 #ou 366

if year >= 2024 or year < 2023:
    print('Nope :(')
    sys.exit(1)


if day != 365 and day != 366:
        print('Nope :(')
        sys.exit(1)

for time_current in range(1703894400,1704153600):
    flag = []
    for i in range(len(correct)):
        random.seed(i+time_current)
        flag.append(correct[i] ^ random.getrandbits(8))

        time_current += random.randint(1, 60)
    try:
        print(bytes(flag).decode('utf-8'))
    except:
        pass

Flag

wctf{b4ll_dr0p}

Blocked1

Enoncé :

The WOLPHV group (yes, this is an actual article) group encrypted our files, but then blocked us for some reason. I think they might have lost the key. Let's log into their accounts and find it...

nc blocked1.wolvctf.io 1337

Code du serveur :

"""
----------------------------------------------------------------------------
NOTE: any websites linked in this challenge are linked **purely for fun**
They do not contain real flags for WolvCTF.
----------------------------------------------------------------------------
"""

import random
import secrets
import sys
import time

from Crypto.Cipher import AES


MASTER_KEY = secrets.token_bytes(16)


def generate(username):
    iv = secrets.token_bytes(16)
    msg = f'password reset: {username}'.encode()
    print(iv + msg)
    if len(msg) % 16 != 0:
        msg += b'\0' * (16 - len(msg) % 16)
    cipher = AES.new(MASTER_KEY, AES.MODE_CBC, iv=iv)
    return iv + cipher.encrypt(msg)


def verify(token):
    iv = token[0:16]
    msg = token[16:]
    cipher = AES.new(MASTER_KEY, AES.MODE_CBC, iv=iv)
    pt = cipher.decrypt(msg)
    username = pt[16:].decode(errors='ignore')
    print(username)
    return username.rstrip('\x00')


def main():
    username = f'guest_{random.randint(100000, 999999)}'

    print("""                 __      __
 _      ______  / /___  / /_ _   __
| | /| / / __ \\/ / __ \\/ __ \\ | / /
| |/ |/ / /_/ / / /_/ / / / / |/ /
|__/|__/\\____/_/ .___/_/ /_/|___/
              /_/""")
    print("[      password reset portal      ]")
    print("you are logged in as:", username)
    print("")
    while True:
        print(" to enter a password reset token, please press 1")
        print(" if you forgot your password, please press 2")
        print(" to speak to our agents, please press 3")
        s = input(" > ")
        if s == '1':
            token = input(" token > ")
            if verify(bytes.fromhex(token)) == 'doubledelete':
                print(open('flag.txt').read())
                sys.exit(0)
            else:
                print(f'hello, {username}')
        elif s == '2':
            print(generate(username).hex())
        elif s == '3':
            print('please hold...')
            time.sleep(2)
            # thanks chatgpt
            print("Thank you for reaching out to WOLPHV customer support. We appreciate your call. Currently, all our agents are assisting other customers. We apologize for any inconvenience this may cause. Your satisfaction is important to us, and we want to ensure that you receive the attention you deserve. Please leave your name, contact number, and a brief message, and one of our representatives will get back to you as soon as possible. Alternatively, you may also visit our website at https://wolphv.chal.wolvsec.org/ for self-service options. Thank you for your understanding, and we look forward to assisting you shortly.")
            print("<beep>")


main()

Découverte

Un classique, AES bit flipping

Résolution

Avec 2 on récupère le token de l'utilisateur en cours.

Avec 1 on déchiffre le token et on récupère l'utilisateur : Hello, guest_540874.

Puis on bitflip de password reset: guest_540874 vers password reset: doubledelete.

Et on l'envoie à 1 le nouveau token.

Utilisation du repo de Vozec pour craft le nouveau token : AES-Flipper

from AES_flipper import Aesflipper

enc = b'021632c825d0e35e0a019cfb54ee0706a4b6c61b020919953d89b373d40999c824eea5246ebf05f6f714037bc55b2141'


plain = b'password reset: guest_540874'
target = b'password reset: doubledelete'

flipper = Aesflipper(
    plain=plain,
    ciphertext=enc,
    add_iv=True,
    debug=True
)
token = flipper.full_flip(target=target)
print(token)

Flag

wctf{th3y_l0st_th3_f1rst_16_byt35_0f_th3_m3ss4g3_t00}

Blocked2

Enoncé :

We managed to log into doubledelete's email server. Hopefully this should give us some leads...

nc blocked2.wolvctf.io 1337

Code :


import random
import secrets
import sys
import time

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.strxor import strxor


MASTER_KEY = secrets.token_bytes(16)


def encrypt(message):
    if len(message) % 16 != 0:
        print("message must be a multiple of 16 bytes long! don't forget to use the WOLPHV propietary padding scheme")
        return None

    iv = secrets.token_bytes(16)
    cipher = AES.new(MASTER_KEY, AES.MODE_ECB)
    blocks = [message[i:i+16] for i in range(0, len(message), 16)]

    # encrypt all the blocks
    encrypted = [cipher.encrypt(b) for b in [iv, *blocks]]

    # xor with the next bloc of plaintext
    for i in range(len(encrypted) - 1):
        encrypted[i] = strxor(encrypted[i], blocks[i])

    return iv + b''.join(encrypted)


def main():
    message = open('message.txt', 'rb').read()

    print("""                 __      __
 _      ______  / /___  / /_ _   __
| | /| / / __ \\/ / __ \\/ __ \\ | / /
| |/ |/ / /_/ / / /_/ / / / / |/ /
|__/|__/\\____/_/ .___/_/ /_/|___/
              /_/""")
    print("[          email portal          ]")
    print("you are logged in as doubledelete@wolp.hv")
    print("")
    print("you have one new encrypted message:")
    print(encrypt(message).hex())

    while True:
        print(" enter a message to send to dree@wolp.hv, in hex")
        s = input(" > ")
        message = bytes.fromhex(s)
        print(encrypt(message).hex())


main()

Découverte

Ici, il faut bien comprendre comment fonctionne le chiffrement et comment ECB fonctionne.

On commence par récupérer un message chiffré avec une clé MASTER_KEY.

Ensuite, on peut chiffrer autant de messages qu'on veut avec la même clé.

Comment un message est chiffré

On a un IV qui est initialisé.

Ensuite, on découpe notre message en bloc de 16.

blocks = [message[i:i+16] for i in range(0, len(message), 16)]

Ensuite on chiffre ces blocs avec une petite particularité, on chiffre aussi l'IV.

encrypted = [cipher.encrypt(b) for b in [iv, *blocks]]

Si F est notre fonction de chiffrement, on a pour un message de longueur 32 :

encrypted=[F(IV),F(blocks[0]),F(blocks[1])]encrypted = [F(IV), F(blocks[0]), F(blocks[1])]

Ensuite, on boucle sur tous les éléments de encrypted sauf le dernier

On a donc :

encrypted=[F(IV)blocks[0],F(blocks[0])blocks[1],F(blocks[1])]encrypted = [F(IV) \oplus blocks[0], F(blocks[0]) \oplus blocks[1], F(blocks[1])]

Ensuite, la fonction retourne :

iv + b''.join(encrypted)

ainsi on connaît l'IV généré

L'idée

C'est de récupérer chaque bloc du texte chiffré qui contient le flag. (C'est l'IV en clair au début)

Ensuite, on chiffre ce bloc : con.sendline(block_i)

On récupère le dernier bloc. Mais pourquoi ?

Souvenez-vous comment sont chiffré les données, en chiffrant juste le bloc de 16 bytes, on obtient ça :

encrypted=[F(IV)blocks[0],F(blocks[0])]encrypted = [F(IV) \oplus blocks[0], F(blocks[0])]

et donc, les 16 derniers bytes, contienne notre message passé par la fonction de chiffrage.

Ainsi sur le premier tour, on obtient: F(IV)F(IV)

Et il suffit de xorer ça avec le deuxième bloc du message du début pour récupérer le premier bloc en clair du message chiffré.

Pour rappel, le deuxième bloc de message :

F(IV)blocks[0]F(IV) \oplus blocks[0]

et ensuite, on réutilise se nouveau bloc connue, pour récupérer celui d'après, et ainsi de suite.

Résolution

from pwn import *
import time
con = remote('blocked2.wolvctf.io', 1337)


con.recvuntil(b'message:\n')

message_encrypt = con.recvline().strip().decode()
block_i = message_encrypt[:32].encode()

msg_found = b''

for i in range(20):
    print("Tour : ",i)
    print("bloc : ",block_i)
    con.recvuntil(b'> ')

    con.sendline(block_i)

    time.sleep(0.5)
    test_encrypt = con.recvline().strip().decode()
    last_encrypt = test_encrypt[-32:]


    block_i = xor(bytes.fromhex(last_encrypt), bytes.fromhex(message_encrypt[32*(i+1):32*(i+2)]))
    msg_found += block_i
    block_i = block_i.hex().encode()
print(msg_found)

Flag

wctf{s0m3_g00d_s3cur1ty_y0u_h4v3_r0lling_y0ur_0wn_crypt0_huh}

TagSeries1

Enoncé :

Don't worry, the interns wrote this one.

nc tagseries1.wolvctf.io 1337

Code du serveur :

import sys
import os
from Crypto.Cipher import AES

MESSAGE = b"GET FILE: flag.txt"
QUERIES = []
BLOCK_SIZE = 16
KEY = os.urandom(BLOCK_SIZE)


def oracle(message: bytes) -> bytes:
    aes_ecb = AES.new(KEY, AES.MODE_ECB)
    return aes_ecb.encrypt(message)[-BLOCK_SIZE:]


def main():
    for _ in range(3):
        command = sys.stdin.buffer.readline().strip()
        tag = sys.stdin.buffer.readline().strip()
        if command in QUERIES:
            print(b"Already queried")
            continue

        if len(command) % BLOCK_SIZE != 0:
            print(b"Invalid length")
            continue

        result = oracle(command)
        if command.startswith(MESSAGE) and result == tag and command not in QUERIES:
            with open("flag.txt", "rb") as f:
                sys.stdout.buffer.write(f.read())
                sys.stdout.flush()
        else:
            QUERIES.append(command)
            assert len(result) == BLOCK_SIZE
            sys.stdout.buffer.write(result + b"\n")
            sys.stdout.flush()


if __name__ == "__main__":
    main()

Découverte

Ici, on utilise une propriété désastreuse du mode ECB

Le but est d'envoyer une command ainsi que le message chiffré. (le tag)

Et si ce n'est pas la command attendu, le serveur nous donne le tag.

Subtilité, le serveur ne renvoie que les 16 derniers bytes du texte chiffré, mais du coup, il ne demande aussi que les 16 derniers.

Résolution

Ainsi on découpe notre flag en 2 blocs de 16 bytes, GET FILE: flag.t et xtaaaaaaaaaaaaaa

On envoie xtaaaaaaaaaaaaaa au serveur, il chiffre le bloc et nous donne son tag.

Ensuite, on concatène les deux bloc on l'envoie et on donne le tag récupéré avec xtaaaaaaaaaaaaaa

et hop, on a le flag.

from pwn import *


conn = remote('tagseries1.wolvctf.io', 1337)

conn.recvuntil(b'== proof-of-work: disabled ==\n')

conn.sendline(b'xtaaaaaaaaaaaaaa')
conn.sendline(b'a' * 16)

r2 = conn.recvline()
print(r2)

conn.sendline(b'GET FILE: flag.txtaaaaaaaaaaaaaa')
conn.sendline(r2)
conn.interactive()

Flag

wctf{C0nGr4ts_0n_g3tt1ng_p4st_A3S}

TagSeries3

Enoncé :

Surely they got it right this time.

nc tagseries3.wolvctf.io 1337

Code serveur :

import sys
import os
from hashlib import sha1

MESSAGE = b"GET FILE: "
SECRET = os.urandom(1200)


def main():
    _sha1 = sha1()
    _sha1.update(SECRET)
    _sha1.update(MESSAGE)
    sys.stdout.write(_sha1.hexdigest() + '\n')
    sys.stdout.flush()
    _sha1 = sha1()
    command = sys.stdin.buffer.readline().strip()
    hash = sys.stdin.buffer.readline().strip()
    _sha1.update(SECRET)
    _sha1.update(command)
    if command.startswith(MESSAGE) and b"flag.txt" in command:
        if _sha1.hexdigest() == hash.decode():
            with open("flag.txt", "rb") as f:
                sys.stdout.buffer.write(f.read())


if __name__ == "__main__":
    main()

Découverte

Classique Hash length extension attack.

Comprendre l'attaque : Hash length attack

Résolution

J'ai changé le fonctionnement d'un repository github pour récupérer la réponse directement en python.

Le repo : Hash_Extender (Il va vraiment falloir que je fasse ma propre lib ^^)

from pwn import *
import os
from Length_Extender import main

conn = remote('tagseries3.wolvctf.io', 1337)

conn.recvuntil(b'== proof-of-work: disabled ==\n')

hash1 = conn.recvline()
print(hash1)
hash_name = "SHA1"
sign = hash1.decode().strip()
data_ = "GET FILE: "
append = "flag.txt"
key_length = 1200
message, hash2 = main(hash_name, sign, data_, append, key_length)
conn.sendline(message)
conn.sendline(hash2.encode())
conn.interactive()

Flag

wctf{M4n_t4er3_mu5t_b3_4_bett3r_w4y}