Challenge 26: CTR bitflipping

There are people in the world that believe that CTR resists bit flipping attacks of the kind to which CBC mode is susceptible.

Re-implement the CBC bitflipping exercise from earlier to use CTR mode instead of CBC mode. Inject an admin=true token.

Flipping bits with CTR is extremely easy: flip a bit in the ciphertext, it flips the corresponding bit in the plaintext.

Remember that CTR mode builds a pseudo-random keystream that is XORed against the message. That is for each bit $ M_i $ of the message:

$$ C_i = M_i \oplus K_i $$

If you XOR ciphertext bit $ C_i $ with some bit $ X_i $ (noting result $ C'_i $) then the decryption procedure will result in:

$$ \begin{align} M'_i & = C'_i \oplus K_i \\ & = C_i \oplus X_i \oplus K_i \\ & = M_i \oplus K_i \oplus X_i \oplus K_i \\ & = M_i \oplus X_i \end{align} $$

Now in this challenge we have the same instructions as in challenge 16: we control some part of the plaintext but cannot insert characters like ";" and "=", and yet we want to get a ciphertext that decrypts to admin=true.

And just like in challenge 16, we are going to encrypt just any message like XXX... and to manipulate the ciphertext to flip bits in the underlying plaintext until we get this admin=true.

Only this time it will be much simpler: flipping plaintext bits in CBC mode required to flip bits in the previous block of ciphertext and we had to write a function to manage all that. With CTR however we simply have to apply our XOR on the ciphertext just like we would apply it to the plaintext itself.

In [1]:
import os
import urllib

from libmatasano import transform_aes_128_ctr, bxor, html_test

# taken from challenge 16 and adapted
class Oracle:
    def __init__(self):
        self.key = os.urandom(16)
        self.nonce = None
        
    def encrypt(self, msg):
        # using urllib to quote characters (a bit overkill)
        quoted_msg = urllib.parse.quote_from_bytes(msg).encode()
        
        full_msg = (
            b"comment1=cooking%20MCs;userdata="
            + quoted_msg
            + b";comment2=%20like%20a%20pound%20of%20bacon"
        )
        
        if self.nonce == None:
            self.nonce = 0
        else:
            self.nonce += 1
        
        ciphertext = transform_aes_128_ctr(full_msg, self.key, self.nonce)
        
        return self.nonce, ciphertext
    
    def decrypt_and_check_admin(self, ctxt, nonce):
        ptxt = transform_aes_128_ctr(ctxt, self.key, nonce)
        
        if b";admin=true;" in ptxt:
            return True
        else:
            return False
In [3]:
oracle = Oracle()

chosen_plaintext = b'X'*(len(';admin=true'))

nonce, ctxt = oracle.encrypt(chosen_plaintext)

to_xor = (b'\x00'*len(b"comment1=cooking%20MCs;userdata=")
         +bxor(b';admin=true', chosen_plaintext))

altered_ctxt = bxor(ctxt, to_xor)

print(transform_aes_128_ctr(altered_ctxt, oracle.key, nonce))
html_test(oracle.decrypt_and_check_admin(altered_ctxt, nonce))
b'comment1=cooking%20MCs;userdata=;admin=true;comment2=%20like%20a%20pound%20of%20bacon'
OK