Rating:
This is a fault injection attack.
The service allows us to encrypt any plaintext and recieve the ciphertext.
According to the description, every time we encrypt some input, a random bit in the key is zeroed.
Therefore, if we perform enough encryptions, eventually the ciphertext we'll get back will be one that was encrypted with a zeroed-out key.
So, we take a known input (e.g. "a"), and encrypt it until we get back a result that decrypts back to "a" using a zeroed key. We save all intermediate ciphertexts in a list.
Once we've arrived to a zero key, we start brute-forcing the key bit by bit. We go to the intermediate ciphertext we've received immediately before, and try to decrypt it with a key that has just one bit set. Once we get our known input, we continue to the one before that, and try all positions for the additional bit in the key until we get a match.
We continue until we've recovered the complete key.
```python
from pwn import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Util.number import bytes_to_long, long_to_bytes
import itertools
KEY_LEN = 16 # AES-128
KEY_LEN_BITS = KEY_LEN * 8
def decrypt(key, enc, iv):
try:
cipher = AES.new(key, AES.MODE_CBC, iv )
return unpad(cipher.decrypt( enc ), AES.block_size)
except ValueError:
return None
def set_key_bit(key, bit):
return long_to_bytes((bytes_to_long(key) | (1 << bit)), len(key))
p = remote("not_my_fault.ichsa.ctf.today", 8013)
p.recvline()
ciphertext = b64d(p.recvline())
p.recvline()
iv = b64d(p.recvline())
log.info(f"Ciphertext: {enhex(ciphertext)}")
log.info(f"IV: {enhex(iv)}")
dummy_text = b'a'
zero_key = b'\x00' * KEY_LEN
encrypted_history = []
with log.progress("Encrypting") as progress:
for i in itertools.count():
progress.status(f"Iteration #{i}")
p.sendlineafter("Enter base64-encoded input to be encrypted: ", b64e(dummy_text))
p.recvuntil("Encrypted result:\n")
encrypted = b64d(p.recvline(keepends = False))
decrypted = decrypt(zero_key, encrypted, iv)
if (decrypted == dummy_text):
log.info("Arrived to zero key")
break
encrypted_history.append(encrypted)
with log.progress("Brute forcing key") as progress:
key = zero_key
for phase_index, encrypted_phase in enumerate(encrypted_history[::-1]):
progress.status(f"Phase #{phase_index}/{len(encrypted_history)}")
for i in range(KEY_LEN_BITS):
test_key = set_key_bit(key, i)
if (decrypt(test_key, encrypted_phase, iv) == dummy_text):
progress.status(f"Found bit #{i} set in key")
key = test_key
break
# Since fault occurs before the encryption, one last bit might or might not be set in the key
decrypted = decrypt(key, ciphertext, iv)
if decrypted == None:
for i in range(KEY_LEN_BITS):
test_key = set_key_bit(key, i)
decrypted = decrypt(test_key, ciphertext, iv)
if (decrypted != None):
# TODO: Might rarely return false decryption
key = test_key
break
if decrypted != None:
log.info(f"Found key: {enhex(key)}")
log.success(f"Flag: {decrypt(key, ciphertext, iv)}")
else:
log.error(f"Can't find key, best guess: {enhex(key)}")
```