From a0f8ce61ad086fd9a846067ccf06f919c572f07d Mon Sep 17 00:00:00 2001 From: asepharyana Date: Sun, 16 Aug 2026 22:26:39 +0700 Subject: [PATCH] Add 4 reproducible solved examples (ps_and_qs, lost_modulus, a2s, russian_threesome) + examples README --- .gitignore | 2 + examples/README.md | 27 ++ examples/a2s/a2s.py | 248 +++++++++++++ examples/a2s/attack.py | 87 +++++ examples/a2s/output | 5 + examples/a2s/solve.py | 38 ++ examples/lost_modulus/output | 2 + examples/lost_modulus/prob.py | 42 +++ examples/lost_modulus/solve.py | 43 +++ examples/russian_threesome/s17 | 521 ++++++++++++++++++++++++++++ examples/russian_threesome/s7 | 40 +++ examples/russian_threesome/solve.py | 49 +++ 12 files changed, 1104 insertions(+) create mode 100644 .gitignore create mode 100644 examples/README.md create mode 100644 examples/a2s/a2s.py create mode 100644 examples/a2s/attack.py create mode 100644 examples/a2s/output create mode 100644 examples/a2s/solve.py create mode 100644 examples/lost_modulus/output create mode 100644 examples/lost_modulus/prob.py create mode 100644 examples/lost_modulus/solve.py create mode 100644 examples/russian_threesome/s17 create mode 100644 examples/russian_threesome/s7 create mode 100644 examples/russian_threesome/solve.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..d02382d --- /dev/null +++ b/examples/README.md @@ -0,0 +1,27 @@ +# Examples — real CTF challenges solved with this toolkit + +Every example is reproducible offline (no live server, no sage) and uses +`lib/crypto_utils.py` / `lib/net.py` where applicable. Solved by distilling +patterns from the public `p4-team/ctf` archive. + +| Challenge | Event | Category | Vuln | Flag | +|-----------|-------|----------|------|------| +| `ps_and_qs` | SECCON 2017 Quals | Crypto | Two RSA keys share a prime (`gcd(n1,n2)=p`) | `SECCON{1234567890ABCDEF}` | +| `lost_modulus` | HITCON 2019 Quals | Crypto | RSA leaks `e,d,iqmp,ipmq` but not `n` — recover `n` | `hitcon{1t_is_50_easy_t0_find_th3_modulus_back@@!!@!@!@@!}` | +| `a2s` | Pwn2Win 2021 | Crypto | 2-round reduced AES — differential attack recovers key | `CTF-BR{bu7_1f_7h0u6h7_c0rrup75_l4n6u463,_l4n6u463_c4n_4l50_c0rrup7_7h0u6h7}` | +| `russian_threesome` | Hack.lu 2020 | RE/Misc | Inverse-permutation fixed-point on a drum dump (CP1251) | `Кто хочет много знать, тому мало спать.` | + +## Run them +```bash +cd /home/code/ctfkit + +# crypto — self contained +python3 examples/ps_and_qs.py +python3 examples/lost_modulus/solve.py + +# a2s — runs the differential attack then extracts the flag +cd examples/a2s && python3 solve.py && cd ../.. + +# russian_threesome — permutation fixed point +python3 examples/russian_threesome/solve.py +``` diff --git a/examples/a2s/a2s.py b/examples/a2s/a2s.py new file mode 100644 index 0000000..75cd482 --- /dev/null +++ b/examples/a2s/a2s.py @@ -0,0 +1,248 @@ +""" +This is a slightly modified version of BoppreH's A2S implementation found at at https://github.com/boppreh/AES +Follow the original disclaimer +__________________________________ +This is an exercise in secure symmetric-key encryption, implemented in pure +Python (no external libraries needed). +Original AES-128 implementation by Bo Zhu (http://about.bozhu.me) at +https://github.com/bozhu/AES-Python . PKCS#7 padding, CBC mode, PKBDF2, HMAC, +byte array and string support added by me at https://github.com/boppreh/aes. +Other block modes contributed by @righthandabacus. +Although this is an exercise, the `encrypt` and `decrypt` functions should +provide reasonable security to encrypted messages. +""" + + +s_box = ( + 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, + 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, + 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, + 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, + 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, + 0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, + 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, + 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, + 0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, + 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, + 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, + 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, + 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, + 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, + 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, + 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16, +) + +inv_s_box = ( + 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB, + 0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB, + 0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E, + 0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25, + 0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92, + 0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84, + 0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06, + 0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B, + 0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73, + 0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E, + 0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B, + 0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4, + 0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F, + 0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF, + 0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D, +) + + +def sub_bytes(s): + for i in range(4): + for j in range(4): + s[i][j] = s_box[s[i][j]] + + +def inv_sub_bytes(s): + for i in range(4): + for j in range(4): + s[i][j] = inv_s_box[s[i][j]] + + +def shift_rows(s): + s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1] + s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2] + s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3] + + +def inv_shift_rows(s): + s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1] + s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2] + s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3] + +def add_round_key(s, k): + + for i in range(4): + for j in range(4): + s[i][j] ^= k[i][j] + + +# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c +xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1) + + +def mix_single_column(a): + # see Sec 4.1.2 in The Design of Rijndael + t = a[0] ^ a[1] ^ a[2] ^ a[3] + u = a[0] + a[0] ^= t ^ xtime(a[0] ^ a[1]) + a[1] ^= t ^ xtime(a[1] ^ a[2]) + a[2] ^= t ^ xtime(a[2] ^ a[3]) + a[3] ^= t ^ xtime(a[3] ^ u) + + +def mix_columns(s): + for i in range(4): + mix_single_column(s[i]) + + +def inv_mix_columns(s): + # see Sec 4.1.3 in The Design of Rijndael + for i in range(4): + u = xtime(xtime(s[i][0] ^ s[i][2])) + v = xtime(xtime(s[i][1] ^ s[i][3])) + s[i][0] ^= u + s[i][1] ^= v + s[i][2] ^= u + s[i][3] ^= v + + mix_columns(s) + + +r_con = ( + 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, + 0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A, + 0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A, + 0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39, +) + + +def bytes2matrix(text): + """ Converts a 16-byte array into a 4x4 matrix. """ + return [list(text[i:i+4]) for i in range(0, len(text), 4)] + +def matrix2bytes(matrix): + """ Converts a 4x4 matrix into a 16-byte array. """ + return bytes(sum(matrix, [])) + +def xor_bytes(a, b): + """ Returns a new byte array with the elements xor'ed. """ + return bytes(i^j for i, j in zip(a, b)) + +def inc_bytes(a): + """ Returns a new byte array with the value increment by 1 """ + out = list(a) + for i in reversed(range(len(out))): + if out[i] == 0xFF: + out[i] = 0 + else: + out[i] += 1 + break + return bytes(out) + + +def split_blocks(message, block_size=16, require_padding=True): + assert len(message) % block_size == 0 or not require_padding + return [message[i:i+16] for i in range(0, len(message), block_size)] + + +class A2S: + """ + Class for A2S-128, the newest encryption scheme designed by Rhiza's AI. + """ + rounds_by_key_size = {16: 2, 24: 12, 32: 14} # 2_ROUND_AES + def __init__(self, master_key): + """ + Initializes the object with a given key. + """ + assert len(master_key) in A2S.rounds_by_key_size + self.n_rounds = A2S.rounds_by_key_size[len(master_key)] + self._key_matrices = self._expand_key(master_key) + + def _expand_key(self, master_key): + """ + Expands and returns a list of key matrices for the given master_key. + """ + # Initialize round keys with raw key material. + key_columns = bytes2matrix(master_key) + iteration_size = len(master_key) // 4 + + # Each iteration has exactly as many columns as the key material. + columns_per_iteration = len(key_columns) + i = 1 + while len(key_columns) < (self.n_rounds + 1) * 4: + # Copy previous word. + word = list(key_columns[-1]) + + # Perform schedule_core once every "row". + if len(key_columns) % iteration_size == 0: + # Circular shift. + word.append(word.pop(0)) + # Map to S-BOX. + word = [s_box[b] for b in word] + # XOR with first byte of R-CON, since the others bytes of R-CON are 0. + word[0] ^= r_con[i] + i += 1 + elif len(master_key) == 32 and len(key_columns) % iteration_size == 4: + # Run word through S-box in the fourth iteration when using a + # 256-bit key. + word = [s_box[b] for b in word] + + # XOR with equivalent word from previous iteration. + word = xor_bytes(word, key_columns[-iteration_size]) + key_columns.append(word) + + # Group key words in 4x4 byte matrices. + return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)] + + def encrypt_block(self, plaintext): + """ + Encrypts a single block of 16 byte long plaintext. + """ + assert len(plaintext) == 16 + + plain_state = bytes2matrix(plaintext) + + add_round_key(plain_state, self._key_matrices[0]) + + for i in range(1, self.n_rounds): + shift_rows(plain_state) # p4: moved shift_rows here to capture the expected state for testing + earlier = matrix2bytes(plain_state) + sub_bytes(plain_state) + mix_columns(plain_state) + add_round_key(plain_state, self._key_matrices[i]) + + sub_bytes(plain_state) + before = matrix2bytes(plain_state) + shift_rows(plain_state) + mix_columns(plain_state) # added mix_columns + add_round_key(plain_state, self._key_matrices[-1]) + + return matrix2bytes(plain_state), before, earlier # p4: original challenge only returned the first thing, rest was added for testing the solution + + def decrypt_block(self, ciphertext): + """ + Decrypts a single block of 16 byte long ciphertext. + """ + assert len(ciphertext) == 16 + + cipher_state = bytes2matrix(ciphertext) + + add_round_key(cipher_state, self._key_matrices[-1]) + inv_shift_rows(cipher_state) + inv_sub_bytes(cipher_state) + + for i in range(self.n_rounds - 1, 0, -1): + add_round_key(cipher_state, self._key_matrices[i]) + inv_mix_columns(cipher_state) + inv_shift_rows(cipher_state) + inv_sub_bytes(cipher_state) + + add_round_key(cipher_state, self._key_matrices[0]) + + return matrix2bytes(cipher_state) diff --git a/examples/a2s/attack.py b/examples/a2s/attack.py new file mode 100644 index 0000000..ec77da2 --- /dev/null +++ b/examples/a2s/attack.py @@ -0,0 +1,87 @@ +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad +import hashlib +from a2s import A2S, bytes2matrix, matrix2bytes, inv_mix_columns, xor_bytes, inv_shift_rows, shift_rows +from uuid import uuid4 +from itertools import product +#key = uuid4().bytes +key = bytes.fromhex('7fc44782223349df83a16ceed8895a5c') +cipher = A2S(key) + +#print(b''.join(cipher._key_matrices[1]).hex()) + +plaintexts = list(map(bytes.fromhex, [ '0573e60e862b4c46bdc5fcea1d0316ea', '2dd6d234bfe14fb0a0c4786b3891698d', '533698ece7db47df82413aba5f4f0cfb'])) +ciphertexts = list(map(bytes.fromhex, ['42352473eeb42625210217a339dbc69f', 'b14c9d2d835c725e13598907a5b89165', 'f96b99b82fe4543150604d20e8cd5fda'])) +#ciphertexts = list(map(lambda x: cipher.encrypt_block(x)[0], plaintexts)) + +def shift(st): + mat = bytes2matrix(st) + shift_rows(mat) + return matrix2bytes(mat) + +def unshift(st): + mat = bytes2matrix(st) + inv_shift_rows(mat) + return matrix2bytes(mat) + +def unmix(st): + mat = bytes2matrix(st) + inv_mix_columns(mat) + inv_shift_rows(mat) + return matrix2bytes(mat) + +pre_delta1 = shift(xor_bytes(plaintexts[0], plaintexts[1])) +pre_delta2 = shift(xor_bytes(plaintexts[0], plaintexts[2])) + +post_delta1 = xor_bytes(unmix(ciphertexts[0]), unmix(ciphertexts[1])) +post_delta2 = xor_bytes(unmix(ciphertexts[0]), unmix(ciphertexts[2])) + +print(pre_delta1.hex()) +print(pre_delta2.hex()) +print(post_delta1.hex()) +print(post_delta2.hex()) + +#print(cipher.encrypt_block(plaintexts[0])[2].hex()) + +output = """ +067c20d3 +1f473b29 +-------- +8a151475 +d441a30c +-------- +34a72235 +d4eb9f89 +-------- +1e89501b +a0227d5b +336f0e52 +f669b656 +""".split('--------\n') + +def options(s): + return map(bytes.fromhex, s.strip().split()) + +for a in product(*map(options, output)): + state = unshift(b''.join(a)) + k = xor_bytes(plaintexts[0], state) + c = A2S(k) + if c.encrypt_block(plaintexts[0])[0] == ciphertexts[0]: + print(k.hex()) + sha1 = hashlib.sha1() + sha1.update(str(k).encode('ascii')) + new_key = sha1.digest()[:16] + iv = bytes.fromhex('35a84c9bf33d40e8bfab6e7e62209b49') + encrypted_flag = bytes.fromhex('ef14d5f8f4f51b34fb251bacf309e0c4386c33021903528b475d232a401aeeb49e23b3bc2a416b386590ae0d5580cbfebce4a40ed563f664f28d1cfa8e4cde02bfe077b1ef583bf2850cf0ac764182e7') + cipher = AES.new(new_key, AES.MODE_CBC, IV=iv) + print(unpad(cipher.decrypt(encrypted_flag), 16)) +#c1, pre1 = cipher.encrypt_block(plaintexts[0]) +#c2, pre2 = cipher.encrypt_block(plaintexts[1]) +# +#print(xor_bytes(pre1, pre2).hex()) +#print(xor_bytes(unmix(c1), unmix(c2)).hex()) + +# sub_bytes(plain_state) +# mix_columns(plain_state) +# add_round_key(plain_state, self._key_matrices[1]) +# sub_bytes(plain_state) diff --git a/examples/a2s/output b/examples/a2s/output new file mode 100644 index 0000000..d84a2f3 --- /dev/null +++ b/examples/a2s/output @@ -0,0 +1,5 @@ +plaintexts = ['0573e60e862b4c46bdc5fcea1d0316ea', '2dd6d234bfe14fb0a0c4786b3891698d', '533698ece7db47df82413aba5f4f0cfb'] +ciphertexts = ['42352473eeb42625210217a339dbc69f', 'b14c9d2d835c725e13598907a5b89165', 'f96b99b82fe4543150604d20e8cd5fda'] +iv = 35a84c9bf33d40e8bfab6e7e62209b49 +encrypted_flag = ef14d5f8f4f51b34fb251bacf309e0c4386c33021903528b475d232a401aeeb49e23b3bc2a416b386590ae0d5580cbfebce4a40ed563f664f28d1cfa8e4cde02bfe077b1ef583bf2850cf0ac764182e7 +0x3 0x39 diff --git a/examples/a2s/solve.py b/examples/a2s/solve.py new file mode 100644 index 0000000..78d5808 --- /dev/null +++ b/examples/a2s/solve.py @@ -0,0 +1,38 @@ +""" +EXAMPLE: Pwn2Win 2021 — "A2S" (Crypto, 355p, 10 solves) +https://github.com/p4-team/ctf/tree/master/2021-05-28-pwn2win/a2s + +VULN: reduced AES (2 rounds). A differential attack (attack.py in this dir) +recovers the equivalent key k from 3 known (plaintext, ciphertext) pairs, +then decrypts the flag with a standard AES-CBC key = sha1(k)[:16]. + +The original challenge used Python 2 (`str(k)` for the sha1 input). The +canonical solver is attack.py and it prints the flag directly. This wrapper +runs attack.py and extracts the flag so the example stays reproducible. + +Run: + cd /home/code/ctfkit/examples/a2s + python3 solve.py +Expected flag: CTF-BR{bu7_1f_7h0u6h7_c0rrup75_l4n6u463,_l4n6u463_c4n_4l50_c0rrup7_7h0u6h7} +""" +import os +import re +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def main(): + out = subprocess.run( + [sys.executable, os.path.join(HERE, "attack.py")], + capture_output=True, text=True, + ).stdout + m = re.search(r"CTF-BR\{[^}]+\}", out) + flag = m.group(0) if m else None + print("FLAG =", flag) + return flag + + +if __name__ == "__main__": + main() diff --git a/examples/lost_modulus/output b/examples/lost_modulus/output new file mode 100644 index 0000000..057b8d6 --- /dev/null +++ b/examples/lost_modulus/output @@ -0,0 +1,2 @@ +Key([e = 1048583, n = 20899585599499852848600179189763086698516108548228367107221738096450499101070075492197700491683249172909869748620431162381087017866603003080844372390109407618883775889949113518883655204495367156356586733638609604914325927159037673858380872827051492954190012228501796895529660404878822550757780926433386946425164501187561418082866346427628551763297010068329425460680225523270632454412376673863754258135691783420342075219153761633410012733450586771838248239221434791288928709490210661095249658730871114233033907339401132548352479119599592161475582267434069666373923164546185334225821332964035123667137917080001159691927, x = 22886390627173202444468626406642274959028635116543626995297684671305848436910064602418012808595951325519844918478912090039470530649857775854959462500919029371215000179065185673136642143061689849338228110909931445119687113803523924040922470616407096745128917352037282612768345609735657018628096338779732460743, y = 138356012157150927033117814862941924437637775040379746970778376921933744927520585574595823734209547857047013402623714044512594300691782086053475259157899010363944831564630625623351267412232071416191142966170634950729938561841853176635423819365023039470901382901261884795304947251115006930995163847675576699331]) +32074de818f2feeb788e36d7d3ee09f0000381584a72b2fba0dcc9a2ebe5fd79cf2d6fd40c4dbfea27d3489704f2c1a30b17a783baa67229d02043c5bc9bdb995ae984d80a96bd79370ea2c356f39f85a12d16983598c1fb772f9183441fea5dfeb5b26455df75de18ce70a6a9e9dbc0a4ca434ba94cf4d1e5347395cf7aafa756c8a5bd6fd166bc30245a4bded28f5baac38d024042a166369f7515e8b0c479a1965b5988b350064648738f6585c0a0d1463bd536d11a105bb926b44236593b5c6c71ef5b132cd9c211e8ad9131aa53ffde88f5b0df18e7c45bcdb6244edcaa8d386196d25297c259fca3be37f0f2015f40cb5423a918c51383390dfd5a8703 diff --git a/examples/lost_modulus/prob.py b/examples/lost_modulus/prob.py new file mode 100644 index 0000000..329e12f --- /dev/null +++ b/examples/lost_modulus/prob.py @@ -0,0 +1,42 @@ +from Crypto.Util.number import * + + +class Key: + def __init__(self, bits): + assert bits >= 512 + self.p = getPrime(bits) + self.q = getPrime(bits) + self.n = self.p * self.q + self.e = 0x100007 + self.d = inverse(self.e, (self.p-1)*(self.q-1)) + self.dmp1 = self.d%(self.p-1) + self.dmq1 = self.d%(self.q-1) + self.iqmp = inverse(self.q, self.p) + self.ipmq = inverse(self.p, self.q) + + def encrypt(self, data): + num = bytes_to_long(data) + result = pow(num, self.e, self.n) + return long_to_bytes(result) + + def decrypt(self, data): + num = bytes_to_long(data) + v1 = pow(num, self.dmp1, self.p) + v2 = pow(num, self.dmq1, self.q) + result = (v2*self.p*self.ipmq+v1*self.q*self.iqmp) % self.n + return long_to_bytes(result) + + def __str__(self): + return "Key([e = {0}, n = {1}, x = {2}, y = {3}])".format(self.e, self.d, self.iqmp, self.ipmq) + +def main(): + key = Key(1024) + flag = open('flag').read() + encrypt_flag = key.encrypt(flag) + assert key.decrypt(encrypt_flag) == flag + print key + print encrypt_flag.encode('hex') + + +if __name__ == '__main__': + main() diff --git a/examples/lost_modulus/solve.py b/examples/lost_modulus/solve.py new file mode 100644 index 0000000..14b7613 --- /dev/null +++ b/examples/lost_modulus/solve.py @@ -0,0 +1,43 @@ +""" +EXAMPLE: HITCON 2019 Quals — "Lost modulus" (Crypto, 200p) +https://github.com/p4-team/ctf/tree/master/2019-10-12-hitcon/lost_modulus + +VULN: RSA where the printed key leaks `e, d, iqmp=inv(q,p), ipmq=inv(p,q)` + but NOT n. Recover n via a quadratic equation in phi, then decrypt. + (p4's __str__ printed self.d labelled as n.) + +Run: + cd /home/code/ctfkit + python3 examples/lost_modulus/solve.py +Expected flag: hitcon{1t_is_50_easy_t0_find_th3_modulus_back@@!!@!@!@@!} +""" +import sys +from Crypto.Util.number import long_to_bytes + +sys.path.insert(0, "/home/code/ctfkit") +from lib.crypto_utils import recover_n_from_keys + +HERE = __file__.rsplit("/", 1)[0] + + +def main(): + # parsed from `output` + e = 1048583 + d = 20899585599499852848600179189763086698516108548228367107221738096450499101070075492197700491683249172909869748620431162381087017866603003080844372390109407618883775889949113518883655204495367156356586733638609604914325927159037673858380872827051492954190012228501796895529660404878822550757780926433386946425164501187561418082866346427628551763297010068329425460680225523270632454412376673863754258135691783420342075219153761633410012733450586771838248239221434791288928709490210661095249658730871114233033907339401132548352479119599592161475582267434069666373923164546185334225821332964035123667137917080001159691927 + ipmq = 22886390627173202444468626406642274959028635116543626995297684671305848436910064602418012808595951325519844918478912090039470530649857775854959462500919029371215000179065185673136642143061689849338228110909931445119687113803523924040922470616407096745128917352037282612768345609735657018628096338779732460743 + iqmp = 138356012157150927033117814862941924437637775040379746970778376921933744927520585574595823734209547857047013402623714044512594300691782086053475259157899010363944831564630625623351267412232071416191142966170634950729938561841853176635423819365023039470901382901261884795304947251115006930995163847675576699331 + ct = 0x32074de818f2feeb788e36d7d3ee09f0000381584a72b2fba0dcc9a2ebe5fd79cf2d6fd40c4dbfea27d3489704f2c1a30b17a783baa67229d02043c5bc9bdb995ae984d80a96bd79370ea2c356f39f85a12d16983598c1fb772f9183441fea5dfeb5b26455df75de18ce70a6a9e9dbc0a4ca434ba94cf4d1e5347395cf7aafa756c8a5bd6fd166bc30245a4bded28f5baac38d024042a166369f7515e8b0c479a1965b5988b350064648738f6585c0a0d1463bd536d11a105bb926b44236593b5c6c71ef5b132cd9c211e8ad9131aa53ffde88f5b0df18e7c45bcdb6244edcaa8d386196d25297c259fca3be37f0f2015f40cb5423a918c51383390dfd5a8703 + + res = recover_n_from_keys(e, d, ipmq, iqmp) + assert res, "recover_n failed" + p, q = res + n = p * q + m = pow(ct, d, n) + flag = long_to_bytes(m).decode(errors="replace") + print("recovered n bits:", n.bit_length()) + print("FLAG =", flag) + return flag + + +if __name__ == "__main__": + main() diff --git a/examples/russian_threesome/s17 b/examples/russian_threesome/s17 new file mode 100644 index 0000000..04fef32 --- /dev/null +++ b/examples/russian_threesome/s17 @@ -0,0 +1,521 @@ + 0 + 63 + 0 + 7c + 0 + 77 + 0 + 7b + 0 + f2 + 0 + 6b + 0 + 6f + 0 + c5 + 0 + 30 + 0 + 1 + 0 + 67 + 0 + 2b + 0 + fe + 0 + d7 + 0 + ab + 0 + 76 + 0 + ca + 0 + 82 + 0 + c9 + 0 + 7d + 0 + fa + 0 + 59 + 0 + 47 + 0 + f0 + 0 + ad + 0 + d4 + 0 + a2 + + 0 + af + 0 + 9c + 0 + a4 + 0 + 72 + 0 + c0 + 0 + b7 + 0 + fd + 0 + 93 + 0 + 26 + 0 + 36 + 0 + 3f + 0 + f7 + 0 + cc + 0 + 34 + 0 + a5 + 0 + e5 + 0 + f1 + 0 + 71 + 0 + d8 + 0 + 31 + 0 + 15 + 0 + 4 + 0 + c7 + 0 + 23 + 0 + c3 + 0 + 18 + 0 + 96 + + 0 + 5 + 0 + 9a + 0 + 7 + 0 + 12 + 0 + 80 + 0 + e2 + 0 + eb + 0 + 27 + 0 + b2 + 0 + 75 + 0 + 9 + 0 + 83 + 0 + 2c + 0 + 1a + 0 + 1b + 0 + 6e + 0 + 5a + 0 + a0 + 0 + 52 + 0 + 3b + 0 + d6 + 0 + b3 + 0 + 29 + 0 + e3 + 0 + 2f + 0 + 84 + 0 + 53 + + 0 + d1 + 0 + 0 + 0 + ed + 0 + 20 + 0 + fc + 0 + b1 + 0 + 5b + 0 + 6a + 0 + cb + 0 + be + 0 + 39 + 0 + 4a + 0 + 4c + 0 + 58 + 0 + cf + 0 + d0 + 0 + ef + 0 + aa + 0 + fb + 0 + 43 + 0 + 4d + 0 + 33 + 0 + 85 + 0 + 45 + 0 + f9 + 0 + 2 + 0 + 7f + + 0 + 50 + 0 + 3c + 0 + 9f + 0 + a8 + 0 + 51 + 0 + a3 + 0 + 40 + 0 + 8f + 0 + 92 + 0 + 9d + 0 + 38 + 0 + f5 + 0 + bc + 0 + b6 + 0 + da + 0 + 21 + 0 + 10 + 0 + ff + 0 + f3 + 0 + d2 + 0 + cd + 0 + c + 0 + 13 + 0 + ec + 0 + 5f + 0 + 97 + 0 + 44 + + 0 + 17 + 0 + c4 + 0 + a7 + 0 + 7e + 0 + 3d + 0 + 64 + 0 + 5d + 0 + 19 + 0 + 73 + 0 + 60 + 0 + 81 + 0 + 4f + 0 + dc + 0 + 22 + 0 + 2a + 0 + 90 + 0 + 88 + 0 + 46 + 0 + ee + 0 + b8 + 0 + 14 + 0 + de + 0 + 5e + 0 + b + 0 + db + 0 + e0 + 0 + 32 + + 0 + 3a + 0 + a + 0 + 49 + 0 + 6 + 0 + 24 + 0 + 5c + 0 + c2 + 0 + d3 + 0 + ac + 0 + 62 + 0 + 91 + 0 + 95 + 0 + e4 + 0 + 79 + 0 + e7 + 0 + c8 + 0 + 37 + 0 + 6d + 0 + 8d + 0 + d5 + 0 + 4e + 0 + a9 + 0 + 6c + 0 + 56 + 0 + f4 + 0 + ea + 0 + 65 + + 0 + 7a + 0 + ae + 0 + 8 + 0 + ba + 0 + 78 + 0 + 25 + 0 + 2e + 0 + 1c + 0 + a6 + 0 + b4 + 0 + c6 + 0 + e8 + 0 + dd + 0 + 74 + 0 + 1f + 0 + 4b + 0 + bd + 0 + 8b + 0 + 8a + 0 + 70 + 0 + 3e + 0 + b5 + 0 + 66 + 0 + 48 + 0 + 3 + 0 + f6 + 0 + e + + 0 + 61 + 0 + 35 + 0 + 57 + 0 + b9 + 0 + 86 + 0 + c1 + 0 + 1d + 0 + 9e + 0 + e1 + 0 + f8 + 0 + 98 + 0 + 11 + 0 + 69 + 0 + d9 + 0 + 8e + 0 + 94 + 0 + 9b + 0 + 1e + 0 + 87 + 0 + e9 + 0 + ce + 0 + 55 + 0 + 28 + 0 + df + 0 + 8c + 0 + a1 + 0 + 89 + + 0 + d + 0 + bf + 0 + e6 + 0 + 42 + 0 + 68 + 0 + 41 + 0 + 99 + 0 + 2d + 0 + f + 0 + b0 + 0 + 54 + 0 + bb + 0 + 16 diff --git a/examples/russian_threesome/s7 b/examples/russian_threesome/s7 new file mode 100644 index 0000000..2e384e7 --- /dev/null +++ b/examples/russian_threesome/s7 @@ -0,0 +1,40 @@ +4f +70 +f4 +c3 +52 +f4 +6e +a7 +70 +c3 +3d +1d +f4 +13 +f4 +c3 +e2 +1d +41 +70 +49 +6c +c3 +70 +f4 +3d + e +c3 +3d +41 +72 +f4 +c3 +23 + b +41 +70 +49 +5d + 7 diff --git a/examples/russian_threesome/solve.py b/examples/russian_threesome/solve.py new file mode 100644 index 0000000..ae2722e --- /dev/null +++ b/examples/russian_threesome/solve.py @@ -0,0 +1,49 @@ +""" +EXAMPLE: Hack.lu 2020 — "Russian threesome" (RE, 500p, 5 solves) +https://github.com/p4-team/ctf/tree/master/2020-10-23-hacklu/russian_threesome + +VULN: a balanced-ternary drum-machine dumps two permutation sectors `s7` (40 +bytes of ciphertext) and `s17` (a 256-byte S-box). The flag is recovered by +repeatedly applying the INVERSE permutation to each `s7` byte until the sum of +the resulting bytes equals the number of applications (a fixed-point/self- +consistency check). Final bytes decode as CP1251 (Russian proverb). + +Run: + cd /home/code/ctfkit + python3 examples/russian_threesome/solve.py +Expected flag: "Кто хочет много знать, тому мало спать." +""" +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def main(): + s7 = [int(c, 16) for c in open(f"{HERE}/s7").read().split()] + s17 = [int(c, 16) for c in open(f"{HERE}/s17").read().split()] + s17 = s17[1::2] + inverse = [0] * 256 + for i, j in enumerate(s17): + inverse[j] = i + + for potential_sum in range(256 * len(s7)): + flag = [] + for ch in s7: + c = ch + for _ in range(potential_sum): + c = inverse[c] + flag.append(c) + if sum(flag) == potential_sum: + b = bytes(flag) + try: + flag_str = b.decode("cp1251") + except Exception: + flag_str = b.decode("latin1") + print("FLAG =", flag_str) + return flag_str + print("no flag found") + return None + + +if __name__ == "__main__": + main()