Last active
March 29, 2024 16:58
-
-
Save MattMoony/031540e6d38505fa77b7458bd4aa1774 to your computer and use it in GitHub Desktop.
Recover plaintext from encrypted AES/ECB ciphertext using an oracle.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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