diff --git a/ctf-solutions/bean_counter.py b/ctf-solutions/bean_counter.py new file mode 100644 index 0000000..31a6219 --- /dev/null +++ b/ctf-solutions/bean_counter.py @@ -0,0 +1,92 @@ +''' +Solution for https://cryptohack.org/courses/symmetric/bean_counter/ +''' + +import os +import requests +from math import ceil + +from Crypto.Cipher import AES + +from imp.math.util import xor_bytes + +class StepUpCounter(object): + def __init__(self, step_up=False): + self.value = os.urandom(16).hex() + self.step = 1 + self.stup = step_up + + def increment(self): + if self.stup: + self.newIV = hex(int(self.value, 16) + self.step) + else: + self.newIV = hex(int(self.value, 16) - self.stup) + self.value = self.newIV[2:len(self.newIV)] + return bytes.fromhex(self.value.zfill(32)) + + def __repr__(self): + self.increment() + return self.value + +''' +NOTE: Since step_up is used ONLY as false, we can simplify: +class StepUpCounter(object): + def __init__(self): + self.value = os.urandom(16).hex() + + # SAME AS DOING NOTHING + def increment(self): + return self.value() + + def __repr__(self): + return self.value +''' + +URL = 'https://aes.cryptohack.org/bean_counter' + +def get_encrypted() -> bytes: + resp = requests.get(f'{URL}/encrypt/') + encrypted = bytes.fromhex(resp.json()['encrypted']) + return encrypted + +PNG_HEADER = [ + '89', '50', '4e', '47', + '0d', '0a', '1a', '0a', + '00', '00', '00', '0d', + '49', '48', '44', '52' +] + +def main() -> None: + # NOTE: the counter is constant! + ctr = StepUpCounter() + # init = ctr.increment() + # init_val = ctr.value + # for i in range(2560000): + # if ctr.increment() != init: + # print('Counter Changed') + # print(i) + # break + # elif ctr.value != init_val: + # print('valued changed') + # print(i) + # break + + # PNGs *should* have a constant header, we will use the first 16 bytes + # to determine what the counter value must be set to + encrypted = get_encrypted() + png_header = bytes.fromhex(''.join(PNG_HEADER)) + ctr_value = xor_bytes(encrypted[:16], png_header) + # apply the counter value to the entire encrypted image (in blocks of 16 bytes) + BLOCK_SIZE = 16 + with open('bean_flag.png', 'wb') as f: + for i in range(ceil(len(encrypted) / BLOCK_SIZE)): + block = encrypted[i*BLOCK_SIZE : (i+1)*BLOCK_SIZE] + # NOTE: the block we get is not guaranteed to be block size (ie if at end) + plaintext_block = xor_bytes(block, ctr_value[:len(block)]) + f.write(plaintext_block) + +if __name__ == '__main__': + try: + main() + except (KeyboardInterrupt, EOFError): + print('\n[!] Interrupt') diff --git a/ctf-solutions/flipping_cookie.py b/ctf-solutions/flipping_cookie.py new file mode 100644 index 0000000..4e6e8e2 --- /dev/null +++ b/ctf-solutions/flipping_cookie.py @@ -0,0 +1,55 @@ +''' +Solution to https://cryptohack.org/courses/symmetric/flipping_cookie/ +''' + +import requests +from datetime import datetime, timedelta + +URL = 'https://aes.cryptohack.org/flipping_cookie' + +# NOTE: assumes A and B are equal length +def xor_bytes(A: bytes, B: bytes) -> bytes: + return b''.join([(a ^ b).to_bytes() for (a, b) in zip(A, B)]) +def xor_str(A: str, B: str) -> str: + return ''.join([chr(ord(a) ^ ord(b)) for (a, b) in zip(A, B)]) + +def gen_expiry() -> str: + return (datetime.today() + timedelta(days=1)).strftime("%s") + +def get_cookie() -> tuple[bytes, bytes]: + resp = requests.get(f'{URL}/get_cookie/') + cookie = resp.json()['cookie'] + iv = bytes.fromhex(cookie[:32]) + ciphertext = bytes.fromhex(cookie[32:]) + return iv, ciphertext + + +def main() -> None: + # cookie flipping preprocessing step + admin_len = len('admin=') + expiry_len = len(';expiry=') + len(gen_expiry()) + admin_mask = '\x00' * admin_len + expiry_mask = '\x00' * expiry_len + # we aim to replace "admin=False;" with "admin=True;;" + # NOTE: double semicolon ("True;;") is intentional + # NOTE: and the server won't ever realise it happened! + deletion = admin_mask + 'False' + expiry_mask + insertion = admin_mask + 'True;' + expiry_mask + # determine the value that replaces deletion with insertion + cookie_flip = xor_str(deletion, insertion) + + # get our new cookie and apply the cookie flip! + iv, ciphertext = get_cookie() + flipped_iv = xor_bytes(cookie_flip.encode(), iv) + + print('Flipped Cookie:') + print('IV:', flipped_iv.hex()) + print('Body:', ciphertext.hex()) + + + + + + +if __name__ == '__main__': + main() diff --git a/ctf-solutions/symmetry.py b/ctf-solutions/symmetry.py new file mode 100644 index 0000000..d12ad08 --- /dev/null +++ b/ctf-solutions/symmetry.py @@ -0,0 +1,39 @@ +''' +Solution to https://cryptohack.org/courses/symmetric/symmetry/ +''' + +import os +import requests + +from imp.math.util import xor_bytes, xor_str + +URL = 'https://aes.cryptohack.org/symmetry' + +def get_encrypted_flag() -> tuple[bytes, bytes]: + resp = requests.get(f'{URL}/encrypt_flag/') + ciphertext = resp.json()['ciphertext'] + iv = bytes.fromhex(ciphertext[:32]) + flag = bytes.fromhex(ciphertext[32:]) + return iv, flag + +def encrypt(plaintext: bytes, iv: bytes) -> bytes: + plaintext, iv = plaintext.hex(), iv.hex() + resp = requests.get(f'{URL}/encrypt/{plaintext}/{iv}') + return bytes.fromhex(resp.json()['ciphertext']) + +def main() -> None: + iv, flag = get_encrypted_flag() + # generate a random plaintext of equal length to the flag + random_plaintext = os.urandom(len(flag)) + random_ciphertext = encrypt(random_plaintext, iv) + # XOR random plaintext with corresponding ciphertext to + # get the set generated by IV under the keyed AES permutation + aes_orbit = xor_bytes(random_plaintext, random_ciphertext) + # XOR this orbit with the flag to get the hexed flag plaintext + flag_plaintext = xor_bytes(flag, aes_orbit) + print('Flag:', flag_plaintext.decode()) + + print('IV:', iv.hex()) + +if __name__ == '__main__': + main() diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..d637d64 --- /dev/null +++ b/default.nix @@ -0,0 +1,10 @@ +let + sources = import ./nix/sources.nix; + pkgs = import sources.nixpkgs {}; + # Let all API attributes like "poetry2nix.mkPoetryApplication" + # use the packages and versions (python3, poetry etc.) from our pinned nixpkgs above + # under the hood: + poetry2nix = import sources.poetry2nix {inherit pkgs;}; + myPythonApp = poetry2nix.mkPoetryApplication {projectDir = ./.;}; +in + myPythonApp diff --git a/crack.py b/examples/cryptohack-ecboracle.py similarity index 92% rename from crack.py rename to examples/cryptohack-ecboracle.py index c749cdf..d4ce7aa 100644 --- a/crack.py +++ b/examples/cryptohack-ecboracle.py @@ -34,7 +34,7 @@ def encrypt(b: bytes) -> bytes: return bytes.fromhex(resp['ciphertext']) def main() -> None: - paddingoracle.crack(encrypt, pad, CHARSET, 64, batch_size=2, debug=True) + paddingoracle.crack(encrypt, pad, CHARSET, 16, batch_size=20, debug=True) diff --git a/test.py b/examples/local-ecboracle.py similarity index 100% rename from test.py rename to examples/local-ecboracle.py