2
$\begingroup$

I am trying to solve another CTF challenge. The challenge consists in trying to exploit an oracle that decrypts any hex text we send (see code below). I am kind of stuck on this one as this is not a classical challenge, since the decrypted text is not displayed. My guess would be one should exploit the fact that exceptions are printed to find some detail regarding the key, and once the key is found, decrypt the flag sent at first, but this is just a random guess from my side. Do you have any idea on how can I approach the problem or some hints? Code:

#!/usr/bin/env python3

import signal
import os
from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES

TIMEOUT = 300
BLOCK_SIZE = 16

assert("FLAG" in os.environ)
flag = os.environ["FLAG"]
assert(flag.startswith("CCIT{"))
assert(flag.endswith("}"))

key = os.urandom(BLOCK_SIZE)
iv = os.urandom(BLOCK_SIZE)

print("Hello! Here's an encrypted flag")
cipher = AES.new(key, AES.MODE_CBC, iv)
print(iv.hex()+cipher.encrypt(pad(flag.encode(), BLOCK_SIZE)).hex())


def handle():
    while True:
        try:
            dec = bytes.fromhex(
                input("What do you want to decrypt (in hex)? ").strip())
            cipher = AES.new(key, AES.MODE_CBC, dec[:BLOCK_SIZE])
            decrypted = cipher.decrypt(dec[BLOCK_SIZE:])
            decrypted_and_unpadded = unpad(decrypted, BLOCK_SIZE)
            print("Wow you are so strong at decrypting!")
        except Exception as e:
            print(e)


if __name__ == "__main__":
    signal.alarm(TIMEOUT)
    handle()

My current approach (to bruteforce the first 2 bytes):

from pwn import *
from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES
import os
r = remote('padding.challs.cyberchallenge.it', 9033)
from Crypto.Util.Padding import pad, unpad
r.recvline()
ciphertext = bytes.fromhex(r.recvline().strip().decode())
BLOCK_SIZE = 16
BLOCK_N = len(ciphertext) // BLOCK_SIZE
iv = ciphertext[:BLOCK_SIZE]
encrypted_flag = ciphertext[BLOCK_SIZE:]
FIRST_BYTE = 47

def bruteforceByte(currentByte, ciphertext, lastDecryptedValue):
    if (currentByte == FIRST_BYTE): 
        lastDecryptedValue = 1
    for guess in range(256):
        craftedblock = ciphertext[:currentByte]
        craftedblock += chr(guess).encode()
        craftedblock += ciphertext[currentByte+1:]
        r.recvuntil(b'(in hex)?')
        r.sendline(craftedblock.hex())
        output = r.recvline().strip()
        if (output.startswith(b'Wow you')):
            paddingValue = FIRST_BYTE-currentByte+1
            d = bytes([guess ^ paddingValue])
            p = bytes([ciphertext[currentByte] ^ d[0]])
            newCipher = ciphertext[:currentByte]+bytes(ciphertext[currentByte] ^ lastDecryptedValue)+ciphertext[currentByte+1:]
            print(f"Plaintext: {p}")
            return newCipher, d
    return None, None
lastDecryptedValue = 1
for currentByte in range(FIRST_BYTE, -1, -1):
    print(f"Working on byte {currentByte}")
    ciphertext , lastDecryptedValue = bruteforceByte(currentByte, ciphertext, lastDecryptedValue)
```
$\endgroup$
13
  • $\begingroup$ You suspect that it's linked to triggering an exception, so try to pursue this further. Can you think of an input which is likely to produce an exception? Which line of code is likely to produce the exception? Can you then try to come up with another input which produces an exception at another place in the code? Does this allow you to learn something about the flag? $\endgroup$
    – Morrolan
    Aug 7 at 7:34
  • $\begingroup$ (If you're unable to answer the above, you might have to study some of the Crypto.Util methods which are used in the padding oracle. Try to figure out what those are actually doing.) $\endgroup$
    – Morrolan
    Aug 7 at 7:42
  • $\begingroup$ I'm focusing myself on the unpad method, which sometimes throws some exception (for instance if the data is not padded to 16 byte boundary, but how can I exploit this information to gain knowledge about the flag? $\endgroup$
    – Shark44
    Aug 7 at 20:13
  • 1
    $\begingroup$ Personally I'd recommend doing it with pen & paper first. That allows you to ignore many of the details you have to handle when programming. Illustrate the decryption operation of e.g. a two-block ciphertext + IV in CBC mode. Maybe draw each individual byte. Colour the things you can change on the input side, try to figure out what changes on the output side when doing so. Then consider the case where the padding operation is successful, try to figure out something about how the output looks like in that case. Only once you figured out how to perform the attack, try to actually implement it. $\endgroup$
    – Morrolan
    Aug 8 at 7:49
  • 1
    $\begingroup$ From a cursory glance at your code: 1) You iterate over only some of the values a byte can take: for i in range(33, 125) - ciphertext bytes can take any of the 256 values, so you will have to try them all to be guaranteed to produce a valid padding. 2) The way you proceed to guess the second byte seems to be missing something. Consider: How many bytes of the output do you want to force to a specific value? How many bytes of the input must you thus modify? $\endgroup$
    – Morrolan
    Aug 9 at 9:35

1 Answer 1

1
$\begingroup$

I wonder if u solve this problem or not, I can obtain my script to u, just replace some minor part to make sure this script can work properly to solve your problem.

If you'd like to know how this script work, you can let me know

from pwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.Cipher import AES
from random import randbytes


def unpad(c):
    length = c[-1]
    for char in c[-length:]:
        if char != length:
            raise ValueError
    return c[:-length]

def asymmetric_encryption(message, N, e):
    # encrypt message with RSA
    # message must be 16 bytes
    # padding 100 bytes random value
    padded_message = b'\x01' * 100 + message
    return pow(bytes_to_long(padded_message), e, N)

def symmetric_encryption(message, key, iv):
    # ecrypt message with AES + CBC Mode
    # message can be arbitrary length
    cipher = AES.new(key, AES.MODE_CBC, iv)
    ct = cipher.encrypt(message)
    iv = cipher.iv
    return iv, ct

def compute_know_part(padding_idx, known_key):
    if known_key == '':
        return b''
    known_part = b''
    for i in range(len(known_key)//2):
        known_part += bytes([int(known_key[i*2:i*2+2], 16) ^ padding_idx])
    
    return known_part

def construct_payload_and_verify(i, known_part, enc_self_key, encrypted_key):
    # candidate = []
    for byte in range(256): # Each byte guess 256 times at most
        # Control yourself
        self_pt = b'\x00' * (15-i) + bytes([byte]) + known_part
        log.info(f"self pt = {self_pt}")
        _, self_ct = symmetric_encryption(self_pt, self_key, self_iv)

        # Connect to oracle and verify
        r = remote("10.113.184.121", 10031)
        r.sendlineafter(b'key: ', str(enc_self_key).encode())
        r.sendlineafter(b'iv: ', str(encrypted_key).encode())
        r.sendlineafter(b'ciphertext: ', self_ct.hex().encode())#enc_png.hex().encode()
        res = r.recvline().decode().strip()
        # log.info(f'key = {enc_self_key}, iv = {encrypted_key}, ct = {self_ct.hex()}')
        print(res)

        if res == 'OK! Got it.':
            tmp = hex(byte ^ (i+1))[2:]
            if len(tmp) < 2:
                tmp = '0' + tmp
            return tmp
        r.close()


# The info the problem gave you
enc_png = open('./Crypto/HW/Oracle/encrypted_flag_d6fbfd5306695c4a.not_png', 'rb').read()
N = 69214008498642035761243756357619851816607540327248468473247478342523127723748756926949706235406640562827724567100157104972969498385528097714986614165867074449238186426536742677816881849038677123630836686152379963670139334109846133566156815333584764063197379180877984670843831985941733688575703811651087495223
e = 65537
encrypted_key = 65690013242775728459842109842683020587149462096059598501313133592635945234121561534622365974927219223034823754673718159579772056712404749324225325531206903216411508240699572153162745754564955215041783396329242482406426376133687186983187563217156659178000486342335478915053049498619169740534463504372971359692
encrypted_iv = 35154524936059729204581782839781987236407179504895959653768093617367549802652967862418906182387861924584809825831862791349195432705129622783580000716829283234184762744224095175044663151370869751957952842383581513986293064879608592662677541628813345923397286253057417592725291925603753086190402107943880261658

# The info you can control
self_iv = b'\x00' * 16
self_key = b'\x00' * 16
enc_self_key = asymmetric_encryption(self_key, N, e)


# Try to POA Key
real_key = ''
for i in range(16): # key has 16bytes
    known_part = compute_know_part(i+1, real_key)
    real_key = construct_payload_and_verify(i, known_part, enc_self_key, encrypted_key) + real_key

# Try to POA IV
real_iv = ''
for i in range(16): # iv has 16bytes
    known_part = compute_know_part(i+1, real_iv)
    real_iv = construct_payload_and_verify(i, known_part, enc_self_key, encrypted_iv) + real_iv

# Final Testing
test_key = pow(int(real_key, 16), e, N)
test_iv = pow(int(real_iv, 16), e, N)
r = remote("10.113.184.121", 10031)
r.sendlineafter(b'key: ', str(test_key).encode())
r.sendlineafter(b'iv: ', str(test_iv).encode())
r.sendlineafter(b'ciphertext: ', enc_png.hex().encode())
assert r.recvline().decode().strip() == 'OK! Got it.'

# Final Decrypt Flag Image
# real_key = '49276d5f345f357472306e395f6b3379'
# real_iv = '4ba3cb1c134651c3bb5cd6e381c2909b'
real_iv = bytes.fromhex(real_iv)
real_key = bytes.fromhex(real_key)
cipher = AES.new(real_key, AES.MODE_CBC, real_iv)
pt = unpad(cipher.decrypt(enc_png))
open("./decrypted_flag.png", "wb").write(pt)
$\endgroup$
1
  • 1
    $\begingroup$ Welcome to Cryptography.SE The question is already off-topic, I wonder why it was not closed. Your answer, too. Code only answers has no serve here. $\endgroup$
    – kelalaka
    Oct 5 at 9:33

Not the answer you're looking for? Browse other questions tagged or ask your own question.