Skip to content

Instantly share code, notes, and snippets.

@MattMoony
Last active March 29, 2024 16:58
Show Gist options
  • Save MattMoony/031540e6d38505fa77b7458bd4aa1774 to your computer and use it in GitHub Desktop.
Save MattMoony/031540e6d38505fa77b7458bd4aa1774 to your computer and use it in GitHub Desktop.
Recover plaintext from encrypted AES/ECB ciphertext using an oracle.
#!/usr/bin/env python3
"""
Recover plaintext from encrypted AES/ECB ciphertext using an oracle.
Scenario:
You can send whatever you want to the server and you get it back
encrypted with an attached secret. (The secret was attached before the
encryption process). Since ECB is *stateless*, meaning each plaintext
block that is the same will result in the same ciphertext block (as long
as the same key is used), we can recover the plaintext by sending a
block-sized - 1 string of known gibberish and bruteforcing the last
character, until it is what we initially received from the server.
E.g.:
oracle(x) -> cbc_enc(x + 'mysecret')
oracle('aaaaaaa') -> cbc_enc('aaaaaaa' + 'mysecret') | = b00143de4d11c36f8c632341895783ff
oracle('aaaaaaaaaaaaaaaa') -> cbc_enc('aaaaaaaaaaaaaaaa' + 'mysecret') | = da030c17f299c08f5f63527b29ed520f 5f38d2da07d2354b5e215c91857d9303
oracle('aaaaaaaaaaaaaaa') -> cbc_enc('aaaaaaaaaaaaaaa' + 'mysecret') | = bcd0a89e64f9a900670c75c6e7730d17 9b252036f625dcada2d7082df6be3e41
oracle('aaaaaaaaaaaaaaa0') -> cbc_enc('aaaaaaaaaaaaaaa0' + 'mysecret') | = 727bcc09edf00a740885b39ef7e36f35 5f38d2da07d2354b5e215c91857d9303
oracle('aaaaaaaaaaaaaaa1') -> cbc_enc('aaaaaaaaaaaaaaa1' + 'mysecret') | = 25fdb4acee3191a64ed4818c02f5815a 5f38d2da07d2354b5e215c91857d9303
...
oracle('aaaaaaaaaaaaaaam') -> cbc_enc('aaaaaaaaaaaaaaam' + 'mysecret') | = bcd0a89e64f9a900670c75c6e7730d17 5f38d2da07d2354b5e215c91857d9303
--> as you can see, the first block of 'aaaaaaaaaaaaaaam' and 'aaaaaaaaaaaaaaa' are
are the same, suggesting that the secret coming after our plaintext starts with an 'm'
E.g.:
--> to use the example from above with the code below, just do the following:
---- oracle function
| ------ "target" block; block which secret will shift into
| | ------ block size
v v v
recover(oracle, 0, 16)
recover(oracle, 0, 16, align=0, only_printable=True, anim=True)
^ ^ ^
| | ---- show fancy recovery status while recovering?
| ----- try to restore only printable characters?
---- in case your plaintext isn't the first thing in the ciphertext
"""
import os
from math import ceil
from base64 import b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from string import printable
from typing import *
KEY: bytes = os.urandom(16)
SECRET: bytes = b'mysecret'
OFF: int = 0
def oracle(txt: bytes) -> bytes:
cipher: AES = AES.new(KEY, AES.MODE_ECB)
return cipher.encrypt(pad(b' '*OFF + txt + SECRET, len(KEY)))
def recover(f: Callable[[bytes], bytes], b: int, k: int, align: int = 0, only_printable: bool = True, anim: bool = False) -> bytes:
secret: bytes = b''
l: int = len(f(b'x'*align+b'a'*k)[(b+1)*k:])
_b: int = b
for i in range(l):
msg: bytes = b'x'*align + b'a'*(k-i%k-1)
goal: bytes = f(msg)[k*_b:k*(_b+1)]
msg = b'x'*align + (msg + secret)[-k+1:]
for x in (printable.encode() if only_printable else range(0x100)):
if anim:
print(f'[*] {secret+bytes([x,])} ... ', end='\r')
cu: bytes = f(msg+bytes([x,]))[k*b:k*(b+1)]
if cu == goal:
secret += bytes([x,])
break
if i%k == k-1:
_b += 1
return secret
def main():
rec: bytes = recover(oracle, max(ceil(OFF/len(KEY)), 1), len(KEY), align=len(KEY)-OFF) # align to fix cross-block known plaintext ...
print(f'[*] Recovered secret: {rec} (len={len(rec)})')
if __name__ == '__main__':
main()
#!/usr/bin/env python3
import os
from math import ceil
from base64 import b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from random import randbytes, randint
from string import printable
from typing import Callable, List, Optional
DBG: bool = False
KEY: bytes = os.urandom(16)
SECRET: bytes = b'whatever'
OFF: int = randint(1, 32) # use this to generate some stuff before str ...
RND: bytes = randbytes(OFF)
def oracle(txt: bytes) -> bytes:
cipher: AES = AES.new(KEY, AES.MODE_ECB)
p: bytes = pad(RND + txt + SECRET, len(KEY))
if DBG:
chnks: Callable[[bytes], List[bytes]] = lambda s: \
[ s[i:i+len(KEY)] for i in range(0, len(s), len(KEY)) ]
print(f'[*] ORC: ')
print('\n'.join(f' {c}' for c in chnks(p)))
return cipher.encrypt(p)
def recover(f: Callable[[bytes], bytes], k_sz: int,
block: Optional[int] = None,
align: Optional[int] = None,
only_printable: bool = True) -> bytes:
secret: bytes = b''
chnks: Callable[[bytes], List[bytes]] = lambda s: \
[ s[i:i+k_sz] for i in range(0, len(s), k_sz) ]
mtchc: Callable[[bytes, bytes], int] = lambda a, b: \
[ x==y for x, y in zip(chnks(a), chnks(b)) ].index(False)
# first, determine the first block we can use for leaking ...
if block is None:
# eliminate possibility of random match ...
ds: List[int] = []
js: List[int] = []
for x in (b'a', b'b',):
a: bytes = f(x*(2*k_sz))
c: int = mtchc(a, f(x*(2*k_sz-1)))
d: int = 0
j: int = 1
while (d := mtchc(a, f(x*(2*k_sz-j)))) == c:
j += 1
ds.append(d)
js.append(k_sz-j+1)
block = max(ds)
align = max(js)
# now, start recovery ...
l: int = len(f(b'x'*align+b'a'*k_sz)[(block+1)*k_sz:])
_b: int = block
for i in range(l):
msg: bytes = b'x'*align + b'a'*(k_sz-i%k_sz-1)
goal: bytes = f(msg)[k_sz*_b:k_sz*(_b+1)]
msg = b'x'*align + (msg + secret)[-k_sz+1:]
for x in (printable.encode() if only_printable else range(0x100)):
cu: bytes = f(msg+bytes([x,]))[k_sz*block:k_sz*(block+1)]
if cu == goal:
secret += bytes([x,])
break
if i%k_sz == k_sz-1:
_b += 1
return secret
def main():
rec: bytes = recover(oracle, len(KEY))
print(f'[*] Recovered secret: {rec} (len={len(rec)})')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment