Score:2

CTF - AES Padding Oracle

bv flag

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)
```
Morrolan avatar
ng flag
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?
Morrolan avatar
ng flag
(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.)
Shark44 avatar
bv flag
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?
Shark44 avatar
bv flag
I have read some articles online and they say that I can try to alter the ciphertext I get with single bytes and check whether the unpadding operation is still successful, in particular it will be successful just for 1 byte, which is then the correct one. So basically I should iterate through all possible bytes and bruteforce all bytes in such a way. I added the piece of code to find 1 byte. Do you think this is a winning strategy? I am still trying to make it work properly
Maarten Bodewes avatar
in flag
There are quite a few good youtube vids on padding oracles; why don't you watch one and see if you can replicate? Trust me, it's tricky enough to program it after seeing a video once. If you're afraid of cheating, you can watch it without audio and an eye closed.
Morrolan avatar
ng flag
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.
Shark44 avatar
bv flag
I have studied a bit a video (https://www.youtube.com/watch?v=O5SeQxErXA4&ab_channel=SidSawhney for reference) and played a bit with symbols, trying to reproduce it as code. I came up with a simple and naive script to get the last 2 bytes of the plaintext, but I am not quite sure it is doing what it is supposed to do. I used the same notation used in the video. Furthermore, may I ask (if my code is correct) why the script sometimes fails to find a value for c (all messages "Padding is incorrect"), and sometimes not?
Morrolan avatar
ng flag
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?
Shark44 avatar
bv flag
Any additional hint? I know I should probably edit 2 bytes instead of just 1 and so on, but how should I edit them in order to have 0x02 at the end? Sorry for the late response anyway.
Morrolan avatar
ng flag
@Shark44 Indeed you'll have to edit two bytes. Now you could brute-force both, but that's not sustainable for anything more than a few bytes. Instead try to come up with a smart way so that you only need to brute-force a single of the two. Maybe write down equations for both of those bytes, expressing which other parts of the ciphertext they depend on. Maybe, having found the first byte of the plaintext, you possess sufficient information to come up with a shortcut for one of the two when it comes to guessing the second byte.
Shark44 avatar
bv flag
The smart way you are referring to may be to xor the D[63] obtained with 0x02 in order to get the correct byte to place in C[47]? I have tried implementing it but I never get both numbers (I updated the code above). I sometimes get c1 and p63 printed (not every time), and I cannot figure out why.
Morrolan avatar
ng flag
@Shark44 yes, this approach sounds correct. As it doesn't work the cause is likely an error in the code. I'd advise to refactor your code, I find it very hard to read --- and thus debug --- in its current state. Some ideas: 1) Work with raw bytes wherever possible, get rid of all the `chr()` and `encode()` stuff. 2) Extract the logic of finding a single byte into its own method, which takes as input among other things the index of which byte to brute-force. Then you can get rid of the nested loops. 3) Maybe add a few unit tests. But this is all kind of out of scope of this site.
Shark44 avatar
bv flag
I think I am still missing something, if I remove those encodings I get a bunch of warnings and the code won't work at all. With the current version it works sometimes for the first byte but it won't work for the second. Would you mind giving it another look, since I tried to make it more clear? Sorry for bothering you
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.