Flash CTF – The Seer

Overview

The Seer challenge involves exploiting a cryptographic vulnerability known as the Padding Oracle Attack. This attack allows an adversary to decrypt data encrypted with block ciphers in Cipher Block Chaining (CBC) mode without knowing the encryption key. The attack takes advantage of the way padding is handled in CBC mode, specifically when using PKCS7 padding.

Challenge Description

In this challenge, we are given access to a remote service that encrypts data using AES in CBC mode. Our goal is to decrypt a given ciphertext by exploiting the padding oracle vulnerability.

Padding Oracle Attack Explanation

The Padding Oracle Attack is a type of side-channel attack that exploits the padding validation of a cryptographic message. When a message is encrypted using CBC mode, it is divided into blocks, and each block is XORed with the previous ciphertext block before being encrypted. The last block is padded to ensure it is the correct length.

If the decryption process reveals whether the padding is correct or not, an attacker can use this information to decrypt the message. By sending modified ciphertexts to the oracle and observing the responses, the attacker can deduce the plaintext.

Attack Steps

  1. Initialization: The ciphertext is divided into blocks of 16 bytes (the AES block size). The attack is performed block by block, starting from the last block.
  2. Brute Force: For each byte in the block, the attacker modifies the preceding block to guess the padding value. By iterating over all possible byte values (0-255), the attacker can determine the correct padding.
  3. Decryption: Once the correct padding is found, the attacker can deduce the plaintext byte by XORing the guessed byte with the corresponding byte in the previous ciphertext block and the padding value.
  4. Repeat: This process is repeated for each byte in the block, and then for each block in the ciphertext.
  5. Result: The attacker eventually recovers the entire plaintext, except for the first block, unless the Initialization Vector (IV) is known.

Implementation

The solution to this challenge is implemented in Python, as shown in the solve.py script. The script connects to the remote service and performs the padding oracle attack to recover the plaintext.

Key Functions

  • padding_oracle_attack(ciphertext): This function implements the attack logic. It iterates over each block and each byte within the block, using the oracle to determine the correct padding and recover the plaintext.
  • correctly_padded(p, ct): This function interacts with the remote service to check if a given ciphertext has valid padding.
  • main(): The main function initiates the attack by retrieving the encrypted token from the service and calling the padding_oracle_attack function.

Solve Script

from pwn import *
import binascii
from tqdm import tqdm

# Connect to the service
p = remote('kubenode.mctf.io', 30024)
#p = process('./chal.py', level="DEBUG")
#Based on the script provided here: https://github.com/flast101/padding-oracle-attack-explained

def padding_oracle_attack(ciphertext):
    BYTE_NB = 16  # AES block size
    block_number = len(ciphertext) // BYTE_NB
    decrypted = bytearray()

    total_bytes = block_number * BYTE_NB
    with tqdm(total=total_bytes, desc="Decrypting", unit="byte") as pbar:
        # Go through each block
        for i in range(block_number, 0, -1):
            current_encrypted_block = ciphertext[(i-1)*BYTE_NB:i*BYTE_NB]
            if i == 1:
                previous_encrypted_block = bytearray(16)
            else:
                previous_encrypted_block = ciphertext[(i-2)*BYTE_NB:(i-1)*BYTE_NB]

            bruteforce_block = bytearray(previous_encrypted_block)
            current_decrypted_block = bytearray(16)
            padding = 0

            # Go through each byte of the block
            for j in range(BYTE_NB, 0, -1):
                padding += 1
                # Bruteforce byte value
                for value in range(256):
                    bruteforce_block[j-1] = (bruteforce_block[j-1] + 1) % 256
                    joined_encrypted_block = bytes(bruteforce_block) + current_encrypted_block

                    # Ask the oracle
                    if correctly_padded(p, joined_encrypted_block):
                        current_decrypted_block[-padding] = bruteforce_block[-padding] ^ previous_encrypted_block[-padding] ^ padding
                        # Prepare newly found byte values
                        for k in range(1, padding+1):
                            bruteforce_block[-k] = padding+1 ^ current_decrypted_block[-k] ^ previous_encrypted_block[-k]
                        break

                pbar.update(1)  # Update the progress bar for each byte processed

            decrypted = current_decrypted_block + decrypted

    # Remove padding
    return decrypted[:-decrypted[-1]]

def correctly_padded(p,ct):
    p.sendlineafter(b'> ', b'2')
    p.sendline(binascii.hexlify(ct))
    response = p.recvline().decode()
    return("fortune is as follows" in response or "your fortune seems corrupted." in response)

def main():
    # Get the initial token from the service
    p.sendlineafter(b'> ', b'1')
    response = p.recvline().decode()
    token_hex = response.split(',')[0].split()[-1]
    token_bytes = binascii.unhexlify(token_hex)

    # Perform the padding oracle attack
    plaintext = padding_oracle_attack(token_bytes)[16:]
    print(f"Recovered plaintext: {plaintext.decode(errors='ignore')}")

if __name__ == "__main__":
    main()

Flag

MetaCTF{5t4r3_1nt0_7h3_ciph3r}

Credits

The explanation and implementation of the Padding Oracle Attack are based on the work by flast101. The solution script solve.py is adapted from this explanation to solve the challenge.