├── lib ├── __init__.py ├── utils_test.py └── utils.py ├── __init__.py ├── set2 ├── ch09.py ├── ch10.py ├── pkcs7.py ├── ch11.py ├── ch13.py ├── ch16.py ├── data │ └── 10.txt ├── ch12.py └── aes_lib.py ├── set1 ├── ch03.py ├── ch04.py ├── ch01.py ├── ch05.py ├── ch07.py ├── ch08.py ├── encode_xor.py ├── ch02.py ├── plaintext_verifier.py ├── decode_xor.py ├── data │ ├── 6.txt │ └── 7.txt ├── ch06.py └── utils │ └── encoding_utils.py ├── set6 ├── utils.py ├── primes.py ├── ch46.py ├── data │ └── 44.txt ├── ch45.py ├── ch43.py ├── ch41.py ├── rsa.py ├── ch44.py ├── ch47.py └── ch42.py ├── set3 ├── data │ ├── 7.txt │ └── 19.txt ├── ch23.py ├── ch22.py ├── ch21.py ├── ch24.py ├── plaintext_verifier.py ├── ch18.py ├── ch20.py └── ch19.py ├── set4 ├── block_utils.py ├── ch26.py ├── ctr.py ├── ch30.py ├── ch32.py ├── ch25.py ├── ch29.py ├── ch31.py ├── ch27.py ├── data │ └── 25.txt ├── ch28.py └── md4_mac.py ├── set5 ├── primes.py ├── ch40.py ├── ch33.py ├── simplified_srp_client.py ├── dh_client.py ├── dh_server.py ├── ch34.py ├── srp_server.py ├── ch36_37_client.py ├── simplified_srp_server.py ├── ch39.py ├── ch38.py └── ch35.py ├── set7 ├── ch52_part2.py ├── ch49.py ├── ch56.py ├── ch50.py ├── md4.py ├── ch51.py ├── ch52.py └── ch53.py └── README.md /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """Recognizes this path as a Python package.""" 2 | -------------------------------------------------------------------------------- /set2/ch09.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 2, challenge 9: implement PKCS#7 padding. 4 | 5 | from pkcs7 import Pkcs7 6 | 7 | if __name__ == '__main__': 8 | print(Pkcs7(b'YELLOW SUBMARINE', 20)) 9 | 10 | -------------------------------------------------------------------------------- /set1/ch03.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 1, challenge 3: single-byte XOR cipher 4 | 5 | import decode_xor 6 | 7 | if __name__ == '__main__': 8 | s = '1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736' 9 | decoder = decode_xor.XORDecoder(8) 10 | 11 | candidates = decoder.DecodeHex(s) 12 | if candidates: 13 | print(str(len(candidates)) + ' candidate(s) found: ') 14 | 15 | for c in candidates: 16 | print (c[1] + ': ' + c[0]) 17 | else: 18 | print ('No candidates found.') 19 | -------------------------------------------------------------------------------- /set1/ch04.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 1, challenge 4: detect single-character XOR. 4 | 5 | import decode_xor 6 | 7 | if __name__ == '__main__': 8 | strings = [] 9 | 10 | with open('data/encrypted.txt', 'r') as data_file: 11 | strings = data_file.readlines() 12 | 13 | decoder = decode_xor.XORDecoder(8) 14 | 15 | for string in strings: 16 | # Remove trailing newlines. 17 | string = string.strip() 18 | 19 | candidates = decoder.DecodeHex(string) 20 | 21 | if candidates: 22 | print('String %s decoded to \"%s\"' 23 | % (string, candidates[0][0])) 24 | break 25 | 26 | -------------------------------------------------------------------------------- /set6/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Taken from 4 | # http://stackoverflow.com/questions/5486204/fast-modulo-calculations-in-python-and-ruby 5 | # Required because any regular library blows up computing this with such huge numbers. 6 | def modexp(g, u, p): 7 | """Computes s = (g ^ u) mod p 8 | Args are base, exponent, modulus 9 | (see Bruce Schneier's book, _Applied Cryptography_ p. 244) 10 | """ 11 | if g == 0: 12 | return 0 13 | 14 | s = 1 15 | while u != 0: 16 | if u & 1: 17 | s = (s * g) % p 18 | u >>= 1 19 | g = (g * g) % p 20 | return s -------------------------------------------------------------------------------- /set1/ch01.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 1, challenge 1: convert hex to base64. 4 | 5 | from utils import encoding_utils as enclib 6 | 7 | if __name__ == '__main__': 8 | b = enclib.HexToBase64('49276d206b696c6c696e6720796f757220627261696e206c696b65206120706f69736f6e6f7573206d757368726f6f6d') 9 | expected = 'SSdtIGtpbGxpbmcgeW91ciBicmFpbiBsaWtlIGEgcG9pc29ub3VzIG11c2hyb29t' 10 | 11 | if b == expected: 12 | print('Conversion from hex to base64 correct') 13 | else: 14 | print('Incorrect conversion from hex to base64. Got %s expected %s' 15 | % (b, expected)) 16 | -------------------------------------------------------------------------------- /set2/ch10.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 2, challenge 10: Implement CBC mode. 4 | 5 | import aes_lib 6 | import base64 7 | 8 | if __name__ == '__main__': 9 | ciphertext = '' 10 | 11 | with open('data/10.txt', 'r') as input_file: 12 | ciphertext = input_file.read() 13 | 14 | # This exercise also requires a specific IV! 15 | aes = aes_lib.AESCipher(b'YELLOW SUBMARINE', iv=b'\x00' * 16) 16 | 17 | # Decode the base64 ciphertext into a byte string. 18 | byte_cipher = base64.b64decode(ciphertext) 19 | 20 | # Note: this is where the actual code lives. 21 | print (aes.SimulateCBCDecryption(byte_cipher).decode('ascii')) 22 | -------------------------------------------------------------------------------- /set3/data/7.txt: -------------------------------------------------------------------------------- 1 | MDAwMDAwTm93IHRoYXQgdGhlIHBhcnR5IGlzIGp1bXBpbmc= 2 | MDAwMDAxV2l0aCB0aGUgYmFzcyBraWNrZWQgaW4gYW5kIHRoZSBWZWdhJ3MgYXJlIHB1bXBpbic= 3 | MDAwMDAyUXVpY2sgdG8gdGhlIHBvaW50LCB0byB0aGUgcG9pbnQsIG5vIGZha2luZw== 4 | MDAwMDAzQ29va2luZyBNQydzIGxpa2UgYSBwb3VuZCBvZiBiYWNvbg== 5 | MDAwMDA0QnVybmluZyAnZW0sIGlmIHlvdSBhaW4ndCBxdWljayBhbmQgbmltYmxl 6 | MDAwMDA1SSBnbyBjcmF6eSB3aGVuIEkgaGVhciBhIGN5bWJhbA== 7 | MDAwMDA2QW5kIGEgaGlnaCBoYXQgd2l0aCBhIHNvdXBlZCB1cCB0ZW1wbw== 8 | MDAwMDA3SSdtIG9uIGEgcm9sbCwgaXQncyB0aW1lIHRvIGdvIHNvbG8= 9 | MDAwMDA4b2xsaW4nIGluIG15IGZpdmUgcG9pbnQgb2g= 10 | MDAwMDA5aXRoIG15IHJhZy10b3AgZG93biBzbyBteSBoYWlyIGNhbiBibG93 11 | -------------------------------------------------------------------------------- /set1/ch05.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 1, challenge 5: implement repeating-key XOR 4 | 5 | import encode_xor 6 | 7 | if __name__ == '__main__': 8 | encoder = encode_xor.XOREncoder() 9 | text = ('Burning \'em, if you ain\'t quick and nimble\n' 10 | 'I go crazy when I hear a cymbal') 11 | 12 | res = encoder.Encode(text, 'ICE') 13 | expected = ('0b3637272a2b2e63622c2e69692a23693a2a3c6324202d623d63343c2a' 14 | '26226324272765272a282b2f20430a652e2c652a3124333a653e2b2027' 15 | '630c692b20283165286326302e27282f') 16 | 17 | if res == expected: 18 | print('XOR encoding correct') 19 | else: 20 | print('XOR encoding incorrect. Got %s expected %s' % (res, expected)) -------------------------------------------------------------------------------- /set4/block_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import math 4 | from Crypto.Cipher import AES 5 | 6 | def BlockXOR(b1, b2): 7 | """XOR of two blocks of bytes. b2 is allowed to be longer 8 | than b1, the extra bytes will simply be ignored.""" 9 | res = b'' 10 | 11 | for bIndex in range(0, len(b1)): 12 | res += bytes([b1[bIndex] ^ b2[bIndex]]) 13 | 14 | return res 15 | 16 | def GetNumBlocks(text, blockSize=AES.block_size): 17 | return int(math.ceil(len(text) / blockSize)) 18 | 19 | def GetSingleBlock(text, index, blockSize=AES.block_size): 20 | # If at the end, simply get all the remaining bytes; there 21 | # will be exactly AES.block_size or less of them. Otherwise, 22 | # get a block. 23 | if index == GetNumBlocks(text, blockSize): 24 | block = text[index * blockSize :] 25 | else: 26 | block = text[index * blockSize : (index + 1) * blockSize] 27 | 28 | return block -------------------------------------------------------------------------------- /set1/ch07.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 1, challenge 7: AES in ECB mode. 4 | 5 | import base64 6 | from Crypto.Cipher import AES 7 | from Crypto import Random 8 | import struct 9 | 10 | class AESCipher(): 11 | def __init__(self, key, mode=AES.MODE_ECB): 12 | """Initialize a AES cipher with the given mode, and key (as a byte string)""" 13 | self._iv = Random.new().read(AES.block_size) 14 | self._cipher = AES.new(key, mode=mode, IV=self._iv) 15 | 16 | def aes_decrypt(self, message): 17 | """Decrypt a message under the cipher. The message should be a byte string.""" 18 | return self._cipher.decrypt(message) 19 | 20 | if __name__ == '__main__': 21 | ciphertext = '' 22 | 23 | with open('data/7.txt', 'r') as input_file: 24 | ciphertext = input_file.read() 25 | 26 | aes = AESCipher(b'YELLOW SUBMARINE') 27 | print(aes.aes_decrypt(base64.b64decode(ciphertext)).decode('ascii')) 28 | -------------------------------------------------------------------------------- /set5/primes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set of utilities for generating prime numbers for RSA 4 | # key generation. 5 | 6 | # Taken from https://github.com/akalin/cryptopals-python3 7 | 8 | import random 9 | 10 | smallPrimes = [2, 3, 5, 7, 11, 13, 17, 19] 11 | 12 | def hasSmallPrimeFactor(p): 13 | for x in smallPrimes: 14 | if p % x == 0: 15 | return True 16 | return False 17 | 18 | def isProbablePrime(p, n): 19 | for i in range(n): 20 | a = random.randint(1, p) 21 | if pow(a, p - 1, p) != 1: 22 | return False 23 | return True 24 | 25 | def getProbablePrime(bitcount): 26 | """Get prime numbers that are represented by |bitcount| bits. 27 | They are prime with a good probability, but not 100 percent certainty. 28 | """ 29 | while True: 30 | p = random.randint(2**(bitcount - 1), 2**bitcount - 1) 31 | if not hasSmallPrimeFactor(p) and isProbablePrime(p, 5): 32 | return p -------------------------------------------------------------------------------- /set6/primes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set of utilities for generating prime numbers for RSA 4 | # key generation. 5 | 6 | # Taken from https://github.com/akalin/cryptopals-python3 7 | 8 | import random 9 | 10 | smallPrimes = [2, 3, 5, 7, 11, 13, 17, 19] 11 | 12 | def hasSmallPrimeFactor(p): 13 | for x in smallPrimes: 14 | if p % x == 0: 15 | return True 16 | return False 17 | 18 | def isProbablePrime(p, n): 19 | for i in range(n): 20 | a = random.randint(1, p) 21 | if pow(a, p - 1, p) != 1: 22 | return False 23 | return True 24 | 25 | def getProbablePrime(bitcount): 26 | """Get prime numbers that are represented by |bitcount| bits. 27 | They are prime with a good probability, but not 100 percent certainty. 28 | """ 29 | while True: 30 | p = random.randint(2**(bitcount - 1), 2**bitcount - 1) 31 | if not hasSmallPrimeFactor(p) and isProbablePrime(p, 5): 32 | return p -------------------------------------------------------------------------------- /lib/utils_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import utils 3 | 4 | class testUtils(unittest.TestCase): 5 | 6 | def testHexToBin(self): 7 | self.assertEqual('10101011', utils.hex_to_bin('ab')) 8 | 9 | def testHexToBin_Invalid(self): 10 | self.assertRaises( 11 | utils.InvalidArgumentError, 12 | utils.hex_to_bin, 'gggg') 13 | 14 | def testBinToHex(self): 15 | self.assertEqual('ab', utils.bin_to_hex('10101011')) 16 | 17 | def testBinToHex_Incomplete(self): 18 | self.assertEqual('a', utils.bin_to_hex('1010101')) 19 | 20 | def testBinToHex_Invalid(self): 21 | self.assertRaises( 22 | utils.InvalidArgumentError, 23 | utils.bin_to_hex, '1012') 24 | 25 | def testBase64ToBin(self): 26 | self.assertEqual('010011010110000101101110', 27 | utils.base64_to_bin('TWFu')) 28 | 29 | def testBase64ToAscii(self): 30 | self.assertEqual('Man', utils.base64_to_ascii('TWFu')) 31 | 32 | def testHammingDistance(self): 33 | self.assertEqual(37, utils.HammingDistanceAscii( 34 | 'this is a test', 'wokka wokka!!!')) -------------------------------------------------------------------------------- /set2/pkcs7.py: -------------------------------------------------------------------------------- 1 | # PKCS7 utilities: apply and strip padding to strings. 2 | 3 | # This also serves as solution for Set 2, Challenge 15. 4 | 5 | def _Padding(string, blockSize): 6 | """Returns the amount of padding bytes we need to add.""" 7 | if len(string) % blockSize == 0: 8 | return 0 9 | 10 | return blockSize - (len(string) % blockSize) 11 | 12 | 13 | def Pkcs7(string, blockSize): 14 | numPadding = _Padding(string, blockSize) 15 | paddedString = string 16 | 17 | for i in range(0, numPadding): 18 | paddedString += bytes([numPadding]) 19 | 20 | return paddedString 21 | 22 | def StripPkcs7(string, blockSize): 23 | paddingFound = False 24 | 25 | if len(string) % blockSize != 0: 26 | raise Exception('String has not been padded properly.') 27 | 28 | index = len(string) - 1 29 | 30 | lastCh = string[index] 31 | if lastCh > blockSize: 32 | return string 33 | 34 | num_padding = 1 35 | while True: 36 | index -= 1 37 | if string[index] != lastCh: 38 | break 39 | 40 | num_padding += 1 41 | 42 | if num_padding != lastCh: 43 | raise Exception('Wrong padding applied.') 44 | 45 | return string[:index+1] 46 | 47 | -------------------------------------------------------------------------------- /set5/ch40.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import math 4 | import ch39 as rsa 5 | 6 | # Set 5, challenge 40: Implement a e=3 RSA broadcast attack. 7 | 8 | def BreakRSA(C, N): 9 | n12 = N[1] * N[2] 10 | n02 = N[0] * N[2] 11 | n01 = N[0] * N[1] 12 | n012 = N[0] * N[1] * N[2] 13 | 14 | # Using the Chinese Remainder Theorem we can decompose C and N. 15 | r0 = C[0] * n12 * rsa.invmod(n12, N[0]) 16 | r1 = C[1] * n02 * rsa.invmod(n02, N[1]) 17 | r2 = C[2] * n01 * rsa.invmod(n01, N[2]) 18 | 19 | res = r0 + r1 + r2 20 | res = res % n012 21 | 22 | # Compute the cube root and round to the nearest integer. 23 | return round(res ** (1.0 / 3.0)) 24 | 25 | 26 | if __name__ == '__main__': 27 | publicKey1, _ = rsa.GenerateRSAPairBroadcast(31, 7) 28 | publicKey2, _ = rsa.GenerateRSAPairBroadcast(17, 13) 29 | publicKey3, _ = rsa.GenerateRSAPairBroadcast(37, 11) 30 | 31 | # We encrypt the same plaintext with three different public keys, all 32 | # of which use E=3 internally. 33 | num = 42 34 | c1 = rsa.Encrypt(publicKey1, num) 35 | c2 = rsa.Encrypt(publicKey2, num) 36 | c3 = rsa.Encrypt(publicKey3, num) 37 | 38 | recoveredPt = BreakRSA([c1, c2, c3], [publicKey1[1], publicKey2[1], publicKey3[1]]) 39 | if recoveredPt == num: 40 | print('[**] Correct!') 41 | else: 42 | print('[!!] Failed.') 43 | -------------------------------------------------------------------------------- /set5/ch33.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import hashlib 4 | import random 5 | 6 | # Set 5, challenge 33: Implement Diffie-Hellman. 7 | 8 | # Taken from 9 | # http://stackoverflow.com/questions/5486204/fast-modulo-calculations-in-python-and-ruby 10 | # Required because any regular library blows up computing this with such huge numbers. 11 | def modexp(g, u, p): 12 | """Computes s = (g ^ u) mod p 13 | Args are base, exponent, modulus 14 | (see Bruce Schneier's book, _Applied Cryptography_ p. 244) 15 | """ 16 | s = 1 17 | while u != 0: 18 | if u & 1: 19 | s = (s * g) % p 20 | u >>= 1 21 | g = (g * g) % p 22 | return s 23 | 24 | def DiffieHelman(p, g): 25 | a = random.randint(0, p - 1) 26 | A = modexp(g, a, p) 27 | 28 | b = random.randint(0, g - 1) 29 | B = modexp(g, b, p) 30 | s = modexp(B, a, p) 31 | 32 | return s 33 | 34 | 35 | if __name__ == '__main__': 36 | p = 'ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff' 37 | print(DiffieHelman(int(p, 16), 2)) 38 | -------------------------------------------------------------------------------- /set1/ch08.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Set 1, challenge 8: detect AES in ECB mode. 4 | 5 | import collections 6 | 7 | def CountRepeatedBlocks(hex_string): 8 | """Counts how many 16-bytes chunks in the string are 9 | repeated more than once. The input is an hexadecimal string.""" 10 | chunk_start = 0 11 | CHUNK_SIZE = 32 12 | 13 | # New keys get a default value of 0. The dictionary will 14 | # count the number of occurrences of each 16-bytes chunk in 15 | # the string. 16 | chunkCounter = collections.defaultdict(int) 17 | 18 | while chunk_start < len(hex_string): 19 | chunk = hex_string[chunk_start : chunk_start + CHUNK_SIZE] 20 | chunkCounter[chunk] += 1 21 | chunk_start += CHUNK_SIZE 22 | 23 | # This generator adds 1 for each value in the dictionary that 24 | # is greater than 1. 25 | return sum(1 for c in chunkCounter if chunkCounter[c] > 1) 26 | 27 | 28 | if __name__ == '__main__': 29 | ecb_candidates = [] 30 | 31 | with open('data/8.txt', 'r') as f: 32 | ecb_candidates = f.readlines() 33 | 34 | repetitions = {} 35 | 36 | for c in ecb_candidates: 37 | candidate = c.strip() # eliminates trailing newline 38 | repeatedCount = CountRepeatedBlocks(candidate) 39 | 40 | if repeatedCount > 0: 41 | repetitions[candidate] = repeatedCount 42 | 43 | # The block we find here is not encrypted with the 'YELLOW SUBMARINE' 44 | # key. I tried just in case there was a surprise :-) 45 | for c in repetitions: 46 | print (c + ' has ' + str(repetitions[c]) + 47 | ' block(s) repeated more than once.') -------------------------------------------------------------------------------- /set1/encode_xor.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from utils import encoding_utils as enclib 3 | 4 | class XOREncoder(object): 5 | 6 | def _ByteToPaddedBin(self, byte): 7 | bin_str = '{0:b}'.format(byte) 8 | 9 | while len(bin_str) < 8: 10 | bin_str = '0' + bin_str 11 | 12 | return bin_str 13 | 14 | def _DoEncode(self, text, key): 15 | """Computes the XOR between a text and a key. Both are expressed 16 | as list of ASCII characters. 17 | 18 | The text length is assumed to be a multiple of the key length; 19 | the key length is extended accordingly if shorted. 20 | 21 | Returns the encoded string as a binary string. 22 | """ 23 | text_bin = '' 24 | key_bin = '' 25 | 26 | for i in range(0, len(text)): 27 | text_byte = ord(text[i]) 28 | key_byte = ord(key[i % len(key)]) 29 | 30 | text_bin += self._ByteToPaddedBin(text_byte) 31 | key_bin += self._ByteToPaddedBin(key_byte) 32 | 33 | res_bin = '' 34 | 35 | for j in range(0, len(text_bin)): 36 | if text_bin[j] == key_bin[j]: 37 | res_bin += '0' 38 | else: 39 | res_bin += '1' 40 | 41 | return res_bin 42 | 43 | def Encode(self, text, key): 44 | """The result is the string as hexadecimals.""" 45 | bin_result = self._DoEncode(text, key) 46 | return enclib.BinToHex(bin_result) 47 | 48 | def EncodeAsAscii(self, text, key): 49 | """The result is an ASCII string.""" 50 | bin_result = self._DoEncode(text, key) 51 | ascii_result = enclib.BinToAscii(bin_result) 52 | 53 | # Output is not ASCII readable, discarding the result. 54 | if not ascii_result[1]: 55 | return '' 56 | 57 | return ascii_result[0] -------------------------------------------------------------------------------- /set1/ch02.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 1, challenge 2: fixed XOR. 4 | 5 | from utils import encoding_utils as enclib 6 | 7 | class Error(Exception): 8 | pass 9 | 10 | class InvalidArgumentError(Error): 11 | pass 12 | 13 | 14 | def FixedXORBins(b1, b2): 15 | """Computes the fixed XOR between two binary strings.""" 16 | 17 | if len(b1) != len(b2): 18 | raise InvalidArgumentError( 19 | "Tried to compute fixed XOR between strings of different length.") 20 | res_b = '' 21 | 22 | for i in range(0, len(b1)): 23 | # Not a binary digit. 24 | if b1[i] not in ('0', '1') or b2[i] not in ('0', '1'): 25 | raise InvalidArgumentError( 26 | "Tried to compute binary XOR between non-binary strings.") 27 | 28 | if b1[i] == b2[i]: 29 | res_b += '0' 30 | else: 31 | res_b += '1' 32 | 33 | return res_b 34 | 35 | 36 | def FixedXOR(s1, s2): 37 | """Computes the fixed XOR between two hexadecimal strings.""" 38 | try: 39 | b1 = enclib.HexToBin(s1) 40 | b2 = enclib.HexToBin(s2) 41 | res_b = FixedXORBins(b1, b2) 42 | except enclib.InvalidArgumentError as err: 43 | # Raise again as an error of this module for better clarity. 44 | raise InvalidArgumentError(err) 45 | 46 | return enclib.BinToHex(res_b) 47 | 48 | 49 | if __name__ == '__main__': 50 | s1 = '1c0111001f010100061a024b53535009181c' 51 | s2 = '686974207468652062756c6c277320657965' 52 | expected = '746865206b696420646f6e277420706c6179' 53 | res = FixedXOR(s1, s2) 54 | 55 | if res == expected: 56 | print('Fixed XOR correct') 57 | else: 58 | print('Fixed XOR incorrect: got %s expected %s' 59 | % (res, expected)) 60 | -------------------------------------------------------------------------------- /set3/ch23.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 3, challenge 23: Clone a MT19937 RNG from its output 4 | 5 | from ch21 import MersenneTwister 6 | 7 | def Untemper(y): 8 | y = y ^ (y >> 18) 9 | y = y ^ ((y << 15) & 4022730752) 10 | 11 | mask = 2636928640 12 | a = y << 7 13 | b = y ^ (a & mask) 14 | c = b << 7 15 | d = y ^ (c & mask) 16 | e = d << 7 17 | f = y ^ (e & mask) 18 | g = f << 7 19 | h = y ^ (g & mask) 20 | i = h << 7 21 | y = y ^ (i & mask) 22 | 23 | z = y >> 11 24 | x = y ^ z 25 | s = x >> 11 26 | y = y ^ s 27 | return y 28 | 29 | def FindStateFromOutputs(realOutputs): 30 | state = [] 31 | isFirst = True 32 | 33 | for output in realOutputs: 34 | state.append(Untemper(output)) 35 | 36 | return state 37 | 38 | if __name__ == '__main__': 39 | realMt = MersenneTwister(10) 40 | realOutputs = [] 41 | 42 | for i in range(0, 624): 43 | realOutputs.append(realMt.randomNumber()) 44 | 45 | print('First 10 outputs of the real generator: %s' % (str(realOutputs[:10]))) 46 | 47 | # Reconstruct the internal state array from the random outputs. 48 | state = FindStateFromOutputs(realOutputs) 49 | clonedMt = MersenneTwister(seed=10, injectedState=state) 50 | clonedOutputs = [] 51 | 52 | # Generates again all numbers depending on the current state 53 | # from the cloned generator. 54 | for i in range(0, 624): 55 | clonedOutputs.append(clonedMt.randomNumber()) 56 | 57 | print('First 10 outputs of the cloned generator: %s' % (str(clonedOutputs[:10]))) 58 | 59 | # If the match, success. 60 | if realOutputs == clonedOutputs: 61 | print('[**] Successfully cloned the Mersenne Twister generator.') 62 | else: 63 | print('[**] The outputs are not the same.') 64 | -------------------------------------------------------------------------------- /set3/data/19.txt: -------------------------------------------------------------------------------- 1 | SSBoYXZlIG1ldCB0aGVtIGF0IGNsb3NlIG9mIGRheQ== 2 | Q29taW5nIHdpdGggdml2aWQgZmFjZXM= 3 | RnJvbSBjb3VudGVyIG9yIGRlc2sgYW1vbmcgZ3JleQ== 4 | RWlnaHRlZW50aC1jZW50dXJ5IGhvdXNlcy4= 5 | SSBoYXZlIHBhc3NlZCB3aXRoIGEgbm9kIG9mIHRoZSBoZWFk 6 | T3IgcG9saXRlIG1lYW5pbmdsZXNzIHdvcmRzLA== 7 | T3IgaGF2ZSBsaW5nZXJlZCBhd2hpbGUgYW5kIHNhaWQ= 8 | UG9saXRlIG1lYW5pbmdsZXNzIHdvcmRzLA== 9 | QW5kIHRob3VnaHQgYmVmb3JlIEkgaGFkIGRvbmU= 10 | T2YgYSBtb2NraW5nIHRhbGUgb3IgYSBnaWJl 11 | VG8gcGxlYXNlIGEgY29tcGFuaW9u 12 | QXJvdW5kIHRoZSBmaXJlIGF0IHRoZSBjbHViLA== 13 | QmVpbmcgY2VydGFpbiB0aGF0IHRoZXkgYW5kIEk= 14 | QnV0IGxpdmVkIHdoZXJlIG1vdGxleSBpcyB3b3JuOg== 15 | QWxsIGNoYW5nZWQsIGNoYW5nZWQgdXR0ZXJseTo= 16 | QSB0ZXJyaWJsZSBiZWF1dHkgaXMgYm9ybi4= 17 | VGhhdCB3b21hbidzIGRheXMgd2VyZSBzcGVudA== 18 | SW4gaWdub3JhbnQgZ29vZCB3aWxsLA== 19 | SGVyIG5pZ2h0cyBpbiBhcmd1bWVudA== 20 | VW50aWwgaGVyIHZvaWNlIGdyZXcgc2hyaWxsLg== 21 | V2hhdCB2b2ljZSBtb3JlIHN3ZWV0IHRoYW4gaGVycw== 22 | V2hlbiB5b3VuZyBhbmQgYmVhdXRpZnVsLA== 23 | U2hlIHJvZGUgdG8gaGFycmllcnM/ 24 | VGhpcyBtYW4gaGFkIGtlcHQgYSBzY2hvb2w= 25 | QW5kIHJvZGUgb3VyIHdpbmdlZCBob3JzZS4= 26 | VGhpcyBvdGhlciBoaXMgaGVscGVyIGFuZCBmcmllbmQ= 27 | V2FzIGNvbWluZyBpbnRvIGhpcyBmb3JjZTs= 28 | SGUgbWlnaHQgaGF2ZSB3b24gZmFtZSBpbiB0aGUgZW5kLA== 29 | U28gc2Vuc2l0aXZlIGhpcyBuYXR1cmUgc2VlbWVkLA== 30 | U28gZGFyaW5nIGFuZCBzd2VldCBoaXMgdGhvdWdodC4= 31 | VGhpcyBvdGhlciBtYW4gSSBoYWQgZHJlYW1lZA== 32 | QSBkcnVua2VuLCB2YWluLWdsb3Jpb3VzIGxvdXQu 33 | SGUgaGFkIGRvbmUgbW9zdCBiaXR0ZXIgd3Jvbmc= 34 | VG8gc29tZSB3aG8gYXJlIG5lYXIgbXkgaGVhcnQs 35 | WWV0IEkgbnVtYmVyIGhpbSBpbiB0aGUgc29uZzs= 36 | SGUsIHRvbywgaGFzIHJlc2lnbmVkIGhpcyBwYXJ0 37 | SW4gdGhlIGNhc3VhbCBjb21lZHk7 38 | SGUsIHRvbywgaGFzIGJlZW4gY2hhbmdlZCBpbiBoaXMgdHVybiw= 39 | VHJhbnNmb3JtZWQgdXR0ZXJseTo= 40 | QSB0ZXJyaWJsZSBiZWF1dHkgaXMgYm9ybi4= -------------------------------------------------------------------------------- /set6/ch46.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import base64 4 | import math 5 | import utils 6 | import rsa 7 | 8 | # Set 6, challenge 46: RSA parity oracle. 9 | 10 | class ParityOracle(object): 11 | def __init__(self, key): 12 | self._key = key 13 | 14 | def IsEven(self, ciphernum): 15 | plainnum = rsa.Decrypt(self._key, ciphernum) 16 | return (plainnum % 2 == 0) 17 | 18 | def numtobytes(k): 19 | return k.to_bytes((k.bit_length() + 7) // 8, byteorder='big') 20 | 21 | if __name__ == '__main__': 22 | # Generate pair, "give" the private key to the oracle. 23 | publicKey, privateKey = rsa.GenerateRSAPair(1024) 24 | oracle = ParityOracle(privateKey) 25 | 26 | # Get the secret text, transform it into a number (the byte order 27 | # is not important as long as you always use the same), and encrypt 28 | # it with the public key. 29 | secretText = base64.b64decode('VGhhdCdzIHdoeSBJIGZvdW5kIHlvdSBkb24ndCBwbGF5IGFyb3VuZCB3aXRoIHRoZSBGdW5reSBDb2xkIE1lZGluYQ==') 30 | secret = int.from_bytes(secretText, byteorder='big') 31 | ciphernum = rsa.Encrypt(publicKey, secret) 32 | 33 | lowerBound = 0 34 | upperBound = privateKey[1] # i.e. n 35 | e, n = publicKey 36 | 37 | m = utils.modexp(2, e, n) 38 | c = ciphernum 39 | 40 | while lowerBound != upperBound: 41 | c = (c * m) % n 42 | 43 | if oracle.IsEven(c): 44 | upperBound -= (upperBound - lowerBound) // 2 45 | else: 46 | lowerBound += (upperBound - lowerBound) // 2 47 | 48 | if upperBound - lowerBound == 1: 49 | break 50 | 51 | if upperBound < lowerBound: 52 | raise Exception('nope') 53 | 54 | # TODO: find out why there are rounding errors at the last 1-2 bytes 55 | # which means we don't always get the exact result at the end. 56 | print(numtobytes(upperBound)) 57 | -------------------------------------------------------------------------------- /set3/ch22.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ch21 import MersenneTwister 4 | import random 5 | import time 6 | 7 | # Set 3, challenge 22: crack a MT19937 seed. 8 | 9 | MIN_PAUSE = 40 10 | MAX_PAUSE = 1000 11 | 12 | def RandWithTimestampedSeed(): 13 | pause = random.randint(MIN_PAUSE, MAX_PAUSE) 14 | time.sleep(pause) 15 | 16 | seed = int(time.time()) 17 | # Just to know if we did it right. 18 | print('[**] Leaking the seed for verification purposes: ' + str(seed)) 19 | mt = MersenneTwister(seed) 20 | 21 | pause = random.randint(MIN_PAUSE, MAX_PAUSE) 22 | time.sleep(pause) 23 | 24 | return mt.randomNumber() 25 | 26 | # I am somewhat unsure about this approach, it seems really crude. 27 | # The other option is to perform the Mersenne Twister computation 28 | # in reverse to return from the generated number to the seed, since 29 | # the algorithm and parameters are common knowledge anyway. 30 | # 31 | # UPDATE! I have researched this in more details, and it seems the 32 | # relationship between the seed and the first generated number is 33 | # not 1:1, but rather there are multiple (similar) seeds that can 34 | # give rise to the same initial number. So inversion will find one 35 | # such seed, but it's not guaranteed to be the one you actually used. 36 | # A few tries shows that the first 3 digits are correct, but the 37 | # rest may not be. 38 | def CrackSeed(generatedNumber): 39 | currentTime = int(time.time()) 40 | 41 | for tries in range(0, 1000000): 42 | mt = MersenneTwister(currentTime) 43 | if mt.randomNumber() == generatedNumber: 44 | return currentTime 45 | 46 | currentTime -= 1 47 | 48 | return None 49 | 50 | if __name__ == '__main__': 51 | num = RandWithTimestampedSeed() 52 | discoveredSeed = CrackSeed(num) 53 | print('[**] The cracked seed is ' + str(discoveredSeed)) 54 | -------------------------------------------------------------------------------- /set7/ch52_part2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from Crypto.Cipher import AES 4 | from Crypto.Cipher import Blowfish 5 | import math 6 | import itertools 7 | from ch52 import * 8 | 9 | def F(message): 10 | return MerkleDamgard(padPKCS7(message), state=b'\x07\x87', stateLen=2) 11 | 12 | # This is a MD construction like in F, but with a larger state length, 13 | # different initial state, and Blowfish instead of AES. 14 | def G(message, state=b'\x00\x00', stateLen=3): 15 | newState = state 16 | newState = padPKCS7(newState) 17 | 18 | for i in range(GetNumBlocks(message)): 19 | cipher = Blowfish.new(newState, Blowfish.MODE_ECB) 20 | newState = cipher.encrypt(GetBlock(message, i)) 21 | newState = padPKCS7(newState[:stateLen]) 22 | 23 | return newState[:stateLen] 24 | 25 | def H(message): 26 | return F(message) + G(message) 27 | 28 | # Takes a list of collisions and returns those that are collisions in G 29 | # too. 30 | def FindGCollisions(collisions): 31 | hashDict = {} 32 | newCollisions = [] 33 | 34 | for c in collisions: 35 | h = G(c) 36 | 37 | if h not in hashDict: 38 | hashDict[h] = [c] 39 | else: 40 | hashDict[h].append(c) 41 | 42 | for k in hashDict: 43 | if len(hashDict[k]) > 1: 44 | newCollisions.extend(hashDict[k]) 45 | 46 | return newCollisions 47 | 48 | if __name__ == '__main__': 49 | stateLen = 2 50 | 51 | # This finds a good number of collisions in G (and therefore H). 52 | collisions = FindCollisions(stateLen, 8192, b'\x07\x87') 53 | print('Found %d collisions for F' % len(collisions)) 54 | 55 | if not VerifyCollisions(collisions, b'\x07\x87'): 56 | print('!! Error') 57 | else: 58 | hCollisions = FindGCollisions(collisions) 59 | 60 | if len(hCollisions) > 0: 61 | print('[**] Found collisions for both F and G:', hCollisions) 62 | else: 63 | print('[**] Collisions were only valid for F') 64 | -------------------------------------------------------------------------------- /set4/ch26.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ctr import DoCTR 4 | 5 | # Set 4, challenge 26: CTR bitflipping. 6 | 7 | def Encrypt(inputBytes): 8 | # Cannot cheat! :) 9 | if ord('=') in inputBytes: 10 | raise Exception('Invalid character in input text.') 11 | 12 | plaintext = b'comment1=cooking%20MCs;userdata=' 13 | plaintext += inputBytes 14 | plaintext += b';comment2=%20like%20a%20pound%20of%20bacon' 15 | 16 | # There is no padding in CTR, so we do not need to apply/strip one. 17 | # Technically we should randomize the key and nonce, but we are only 18 | # going to encrypt once so I am not doing it. 19 | return DoCTR(plaintext, b'YELLOW SUBMARINE', 0) 20 | 21 | def DecryptAndVerify(ciphertext): 22 | plaintext = DoCTR(ciphertext, b'YELLOW SUBMARINE', 0) 23 | return (b'admin=true' in plaintext) 24 | 25 | if __name__ == '__main__': 26 | pt = b'adminXtrue' 27 | offset = 37 # offset of the X in the complete plaintext. 28 | ciphertext = Encrypt(pt) 29 | 30 | # The byte at position 'offset' in the plaintext is the XOR of the 31 | # same byte in the ciphertext (let's call it C) and of a byte which 32 | # is derived by applying AES encryption on the nonce + counter (we'll 33 | # call it A). This last byte is the same both when encrypting and 34 | # decrypting, due to how CTR works. 35 | # Therefore I have that A xor C = 'X'. To transform that into '=' I 36 | # compute 'X' xor 'X' xor '='. 'X' xor 'X' is 0, 0 xor 'something' 37 | # is 'something' so I get what I want. I also need to apply the same 38 | # thing on the left-hand side of the equation: 39 | # A xor C xor 'X' xor '=' = '='. 40 | flipped = ciphertext[offset] ^ ord('X') ^ ord('=') 41 | 42 | flippedCiphertext = ciphertext[:offset] 43 | flippedCiphertext += bytes([flipped]) 44 | flippedCiphertext += ciphertext[offset + 1:] 45 | 46 | if DecryptAndVerify(flippedCiphertext): 47 | # Success. 48 | print('[**] This is an admin account.') 49 | else: 50 | print("[**] This is NOT an admin account.") 51 | -------------------------------------------------------------------------------- /set4/ctr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import block_utils 4 | from Crypto.Cipher import AES 5 | 6 | class AESCtr(object): 7 | def __init__(self, key, nonce): 8 | self._nonce = nonce 9 | # In CTR mode, we use a regular ECB cipher to encrypt the 10 | # individual keystream blocks. 11 | self._cipher = AES.new(key=key, mode=AES.MODE_ECB) 12 | 13 | def OneBlockCrypt(self, block, counter): 14 | # The keystream generator is composed by 8 bytes of the nonce + 15 | # 8 bytes of counter, little-endian. 16 | keystreamGen = bytes([self._nonce]) * 8 17 | # This is a quick hack that only works if there are not enough 18 | # blocks of text to need 2 bytes to represent the counter. That 19 | # will mean less than 2^8 blocks, which is reasonable for now 20 | # so we can be lazy. 21 | keystreamGen += bytes([counter, 0, 0, 0, 0, 0, 0, 0]) 22 | 23 | # Careful, we always encrypt the keystream even when decrypting. 24 | keystream = self._cipher.encrypt(keystreamGen) 25 | 26 | # The PT is simply the CT XORed with the keystream, and vice versa. 27 | # Since the PT only depends on the same block of CT plus a predictable 28 | # keystream, CTR is very suited for parallel decryption of many blocks. 29 | return block_utils.BlockXOR(block, keystream) 30 | 31 | def Crypt(self, text): 32 | """The algorithm is the same for both encryption and decryption.""" 33 | resultingText = b'' 34 | keystream = b'' 35 | 36 | # There is no padding in CTR mode, so both a plaintext and a ciphertext 37 | # can be of non-exact block size. We decrypt the last block by generating 38 | # the keystream and only using the bytes we need. 39 | numBlocks = block_utils.GetNumBlocks(text) 40 | counter = 0 41 | 42 | # +1 to get the final bytes that do not fit in a block. 43 | for blockIndex in range(0, numBlocks + 1): 44 | block = block_utils.GetSingleBlock(text, blockIndex) 45 | resultingText += self.OneBlockCrypt(block, counter) 46 | counter += 1 47 | 48 | return resultingText 49 | 50 | def DoCTR(text, key, nonce): 51 | return AESCtr(key, nonce).Crypt(text) -------------------------------------------------------------------------------- /set3/ch21.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 3, challenge 20: Implement the MT19937 Mersenne Twister RNG. 4 | 5 | class MersenneTwister(object): 6 | # Algorithm constants for MT19937. 7 | # Degree of recurrence (after this many calls to randomNumber, 8 | # the cycle begins anew). 9 | N = 624 10 | 11 | def __init__(self, seed, injectedState=None): 12 | """If an injected state is defined, we put that into the state array 13 | and set the index to 0. Otherwise, we generate a new proper state from 14 | the seed and twist it when required.""" 15 | self.index = self.N 16 | self.state = [0] * self.index 17 | 18 | if injectedState is None: 19 | self.state[0] = seed 20 | 21 | for i in range(1, self.index): 22 | self.state[i] = int( 23 | 1812433253 * (self.state[i - 1] ^ (self.state[i - 1] >> 30)) 24 | + i) & 0xFFFFFFFF 25 | 26 | else: 27 | self.state = injectedState 28 | self.index = 0 29 | 30 | def randomNumber(self): 31 | if self.index >= self.N: 32 | self._twist() 33 | 34 | y = self.state[self.index] 35 | 36 | # Right shift by 11 bits 37 | y = y ^ y >> 11 38 | # Shift y left by 7 and take the bitwise and of 2636928640 39 | y = y ^ y << 7 & 2636928640 40 | # Shift y left by 15 and take the bitwise and of y and 4022730752 41 | y = y ^ y << 15 & 4022730752 42 | # Right shift by 18 bits 43 | y = y ^ y >> 18 44 | 45 | self.index += 1 46 | return int(y) 47 | 48 | def _twist(self): 49 | for i in range(self.N): 50 | # Get the most significant bit and add it to the less significant 51 | # bits of the next number 52 | y = int((self.state[i] & 0x80000000) + 53 | (self.state[(i + 1) % self.N] & 0x7fffffff)) 54 | self.state[i] = self.state[(i + 397) % self.N] ^ y >> 1 55 | 56 | if y % 2 != 0: 57 | self.state[i] = self.state[i] ^ 0x9908b0df 58 | self.index = 0 59 | 60 | 61 | if __name__ == '__main__': 62 | print('[**] Let\'s generate a few numbers.') 63 | mt = MersenneTwister(500) 64 | 65 | for i in range(10): 66 | print(mt.randomNumber()) 67 | -------------------------------------------------------------------------------- /set4/ch30.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from md4_mac import MD4Sign 4 | from md4_mac import MD4 5 | from md4_mac import _pad 6 | import binascii 7 | import struct 8 | 9 | # Set 4, challenge 30: Break an MD4 keyed MAC using length extension. 10 | 11 | def makeGluePadding(message, keylen): 12 | byteLength = len(message) + keylen 13 | bitLength = byteLength * 8 14 | index = (bitLength >> 3) & 0x3f 15 | 16 | padLength = 120 - index 17 | if index < 56: 18 | padLength = 56 - index 19 | 20 | padding = b'\x80' + b'\x00'*63 21 | return padding[:padLength] + struct.pack(' maxTiming: 73 | maxTiming = functiming 74 | maxByte = byte 75 | 76 | print('Recovered as ' + str(bytes([maxByte]))) 77 | recoveredSigPart += bytes([maxByte]) 78 | 79 | print(recoveredSigPart) 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README # 2 | 3 | Solutions for the Matasano crypto challenges as described at http://cryptopals.com . 4 | 5 | All problems are solved in Python 3. [pycrypto](https://pypi.python.org/pypi/pycrypto) has been used 6 | as a library for common algorithms and utilities. 7 | 8 | ## What's missing 9 | 10 | The solution to [MD4 collisions](http://cryptopals.com/sets/7/challenges/55) from set 7 is not complete. 11 | This is due to me getting bored and moving to set 8 early. I plan to revisit it once I finish set 8, which 12 | might take a while (read: an inordinate amount of time, don't hold your breath). 13 | 14 | ### Set 8 15 | 16 | As I explained on my blog, the challenge authors have asked not to share the challenges of set 8. I believe this implies not sharing the 17 | solutions either, since they would give a pretty good insight into the original challenge and all the required parameters. 18 | 19 | I asked the cryptopals team for permission to share the solutions here, but have not received any reply in several months. 20 | Therefore for now they remain in a private repository; sorry about that. I still plan to write about interesting things 21 | I learn thanks to these challenges, on the blog. 22 | 23 | ## Walkthroughs 24 | 25 | When I started this repository I also started a [walkthrough](https://www.gitbook.com/book/shainer/matasano-crypto-challenges-walkthrough/details) on 26 | GitBook. The walkthrough fell behind quite soon (it does not explain all the solutions I have actually implemented), and to be honest I am not sure 27 | if I will ever complete it. However explanations for more "challenging" (ahem...) challenges can be found on some blog posts I published: 28 | 29 | - [Overview of DSA](https://shainer.github.io/crypto/python/matasano/2016/10/07/dsa.html) 30 | - [Cloning the Mersenne Twister generator](https://shainer.github.io/crypto/python/matasano/random/2016/10/27/mersenne-twister-p2.html) 31 | - [Challenge 51, or the CRIME attack](https://shainer.github.io/crypto/2017/01/02/crime-attack.html) 32 | - [Challenges 52 and 53, or how to multiply hash collisions](https://shainer.github.io/crypto/2017/01/22/multiplying-hash-collisions.html) 33 | - [Challenges 47 and 48, on the RSA padding oracle](https://shainer.github.io/crypto/matasano/2017/10/14/rsa-padding-oracle-attack.html) 34 | 35 | The code should also be reasonably commented (if I can say so myself :-)). 36 | -------------------------------------------------------------------------------- /set5/simplified_srp_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import ch33 as dh 4 | import hashlib 5 | import random 6 | import socket 7 | 8 | # Set 5, challenge 38: Offline dictionary attack on simplified SRP. Client. 9 | # 10 | # The corresponding server can be found at simplified_srp_server.py 11 | # This is similar to the client for challenges 36 and 37, the only difference 12 | # is in the computation of u. As for the server, I made a different application 13 | # for readability purposes. 14 | 15 | def ReadUntilNewline(clientsocket): 16 | data = b'' 17 | 18 | while b'\n' not in data: 19 | data += clientsocket.recv(1) 20 | 21 | return data[:-1] 22 | 23 | def SRPSetup(sock, email, password): 24 | g = 2 25 | k = 3 26 | N = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e340' 27 | '4ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f40' 28 | '6b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8f' 29 | 'd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff', 16) 30 | 31 | sock.send(email.encode() + b'\n') 32 | 33 | a = random.randint(0, 10000) 34 | A = dh.modexp(g, a, N) 35 | 36 | message = (str(A) + '\n').encode() 37 | sock.send(message) 38 | 39 | salt = ReadUntilNewline(sock) 40 | B = ReadUntilNewline(sock) 41 | u = ReadUntilNewline(sock) 42 | 43 | sha = hashlib.sha256() 44 | sha.update(salt) 45 | sha.update(password.encode()) 46 | x = int(sha.hexdigest(), 16) 47 | 48 | exp = (a + int(u) * x) 49 | S = dh.modexp(int(B), exp, N) 50 | 51 | sha = hashlib.sha256() 52 | sha.update(str(S).encode()) 53 | K = sha.hexdigest() 54 | 55 | return K, salt 56 | 57 | if __name__ == '__main__': 58 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 59 | sock.connect(('localhost', 10001)) 60 | 61 | email = input('[**] Enter email address: ') 62 | password = input('[**] Enter password: ') 63 | K, salt = SRPSetup(sock, email, password) 64 | 65 | sha = hashlib.sha256() 66 | sha.update(salt) 67 | sha.update(K.encode()) 68 | digest = sha.hexdigest() 69 | 70 | sock.send(digest.encode()) 71 | reply = sock.recv(2) 72 | 73 | if reply == b'OK': 74 | print('[**] Password accepted. You have full control of the nuclear reactor!') 75 | elif reply == b'NO': 76 | print('[!!] Incorrect password.') 77 | else: 78 | print('Unrecognized reply by the server:', reply) 79 | 80 | sock.close() 81 | -------------------------------------------------------------------------------- /set3/ch24.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import random 4 | import time 5 | from ch21 import MersenneTwister 6 | 7 | # Set 3, challenge 24: create the MT19937 stream cipher and break it. 8 | 9 | def BlockXor(completeText, stream): 10 | """Quick XOR between two bytes.""" 11 | assert len(completeText) == len(stream) 12 | 13 | res = b'' 14 | streamIndex = 0 15 | 16 | for ch in completeText: 17 | res += bytes([ch ^ stream[streamIndex]]) 18 | streamIndex += 1 19 | 20 | return res 21 | 22 | def MtStream(textLen, seed): 23 | """Generates the stream of random bytes.""" 24 | generator = MersenneTwister(seed) 25 | stream = [] 26 | 27 | for i in range(0, textLen): 28 | stream.append(generator.randomNumber() % (2 ** 8)) 29 | 30 | return stream 31 | 32 | def EncryptMt(plaintext, seed): 33 | prepend = b'Y' * random.randint(1, 10) 34 | completeText = prepend + plaintext 35 | 36 | stream = MtStream(len(completeText), seed) 37 | return BlockXor(completeText, stream) 38 | 39 | if __name__ == '__main__': 40 | plaintext = b'X' * 14 41 | maxSeed = 65535 # 16-bit seed. 42 | seed = random.randint(0, maxSeed) 43 | 44 | # Useful to verify we got it right :) 45 | print ('[**] The random seed is ' + str(seed)) 46 | ciphertext = EncryptMt(plaintext, seed) 47 | 48 | # How many characters in the ciphertext are random characters that 49 | # were prepended. We pretend we do not know what these characters 50 | # are. 51 | randomLen = len(ciphertext) - 14 52 | internalState = [] 53 | 54 | # This gives the generated random numbers from randomLen on; we 55 | # cannot retrieve the first ones because we do not know the 56 | # associated plaintext characters. 57 | for ch in range(randomLen, len(ciphertext)): 58 | internalState += BlockXor(bytes([ciphertext[ch]]), b'X') 59 | 60 | # For each possible seed, we build a MT random generator, generate 61 | # numbers and compare them with the retrieve internal state. If 62 | # they match, we found the right seed. This takes at most 1-2 minutes 63 | # when the random seed is close to maxSeed. 64 | # 65 | # TODO: find a more efficient cracking way than bruteforce. 66 | for probableSeed in range(0, maxSeed): 67 | mt = MersenneTwister(probableSeed) 68 | 69 | reproducedState = [] 70 | for i in range(0, len(ciphertext)): 71 | reproducedState.append(mt.randomNumber() % (2 ** 8)) 72 | 73 | if reproducedState[randomLen:] == internalState: 74 | print('[**] The cracked seed is: ', str(probableSeed)) 75 | break 76 | else: 77 | print('[!!] Could not crack seed.') 78 | -------------------------------------------------------------------------------- /set1/plaintext_verifier.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | class PlaintextVerifier(object): 4 | def __init__(self): 5 | pass 6 | 7 | def _MatchedParentheses(self, s): 8 | """Checks whether open parentheses and quotes are always closed.""" 9 | braces = 0 10 | brackets = 0 11 | regulars = 0 12 | quotes = 0 13 | 14 | for ch in s: 15 | if ch == '{': 16 | braces += 1 17 | elif ch == '}': 18 | braces -= 1 19 | 20 | elif ch == '[': 21 | brackets += 1 22 | elif ch == ']': 23 | brackets -= 1 24 | 25 | elif ch == '(': 26 | regulars += 1 27 | elif ch == ')': 28 | regulars -= 1 29 | elif ch == "\"": 30 | quotes += 1 31 | 32 | return ((braces == 0) and 33 | (brackets == 0) and 34 | (regulars == 0) and 35 | (quotes % 2 == 0)) 36 | 37 | 38 | def _FollowedBySpace(self, s, ch): 39 | try: 40 | i = s.index(ch) 41 | return (i == len(s) - 1 or s[i + 1] == ' ') 42 | except ValueError: 43 | # Trivial case: the character is not in the string. 44 | return True 45 | 46 | 47 | def _CheckPunctuaction(self, s): 48 | """Checks whether some punctuaction characters are always 49 | followed by a whitespace, unless they are at the end.""" 50 | return (self._FollowedBySpace(s, '!') and 51 | self._FollowedBySpace(s, '?') and 52 | self._FollowedBySpace(s, '.')) 53 | 54 | def _SortByFrequency(self, s): 55 | """Returns a list of characters in s, sorted by their frequency in it.""" 56 | # Builds a frequency map (mapping between char and number of 57 | # occurrences). 58 | freq_map = collections.defaultdict(int) 59 | 60 | for ch in s: 61 | freq_map[ch] += 1 62 | 63 | # Sort the map keys by their value, in reverse order, so the most 64 | # common character is first in the list. 65 | return sorted(freq_map.keys(), 66 | key = lambda x : freq_map[x], 67 | reverse=True) 68 | 69 | def CheckFrequency(self, s): 70 | """Checks whether the most frequent character in the text is in the list 71 | of most frequent characters in English text.""" 72 | freq_list = self._SortByFrequency(s) 73 | return (freq_list[0] in 'ETAOIN ') 74 | 75 | def IsEnglishPlaintext(self, s, check_frequency=True): 76 | # Text starts with a capital letter. 77 | if s[0] < 'A' or s[0] > 'Z': 78 | return False 79 | 80 | if not self._MatchedParentheses(s): 81 | return False 82 | 83 | if not self._CheckPunctuaction(s): 84 | return False 85 | 86 | if check_frequency and not self.CheckFrequency(s): 87 | return False 88 | 89 | return True -------------------------------------------------------------------------------- /set3/plaintext_verifier.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | class PlaintextVerifier(object): 4 | def __init__(self): 5 | pass 6 | 7 | def _MatchedParentheses(self, s): 8 | """Checks whether open parentheses and quotes are always closed.""" 9 | braces = 0 10 | brackets = 0 11 | regulars = 0 12 | quotes = 0 13 | 14 | for ch in s: 15 | if ch == '{': 16 | braces += 1 17 | elif ch == '}': 18 | braces -= 1 19 | 20 | elif ch == '[': 21 | brackets += 1 22 | elif ch == ']': 23 | brackets -= 1 24 | 25 | elif ch == '(': 26 | regulars += 1 27 | elif ch == ')': 28 | regulars -= 1 29 | elif ch == "\"": 30 | quotes += 1 31 | 32 | return ((braces == 0) and 33 | (brackets == 0) and 34 | (regulars == 0) and 35 | (quotes % 2 == 0)) 36 | 37 | 38 | def _FollowedBySpace(self, s, ch): 39 | try: 40 | i = s.index(ch) 41 | return (i == len(s) - 1 or s[i + 1] == ' ') 42 | except ValueError: 43 | # Trivial case: the character is not in the string. 44 | return True 45 | 46 | 47 | def _CheckPunctuaction(self, s): 48 | """Checks whether some punctuaction characters are always 49 | followed by a whitespace, unless they are at the end.""" 50 | return (self._FollowedBySpace(s, '!') and 51 | self._FollowedBySpace(s, '?') and 52 | self._FollowedBySpace(s, '.')) 53 | 54 | def _SortByFrequency(self, s): 55 | """Returns a list of characters in s, sorted by their frequency in it.""" 56 | # Builds a frequency map (mapping between char and number of 57 | # occurrences). 58 | freq_map = collections.defaultdict(int) 59 | 60 | for ch in s: 61 | freq_map[ch] += 1 62 | 63 | # Sort the map keys by their value, in reverse order, so the most 64 | # common character is first in the list. 65 | return sorted(freq_map.keys(), 66 | key = lambda x : freq_map[x], 67 | reverse=True) 68 | 69 | def CheckFrequency(self, s): 70 | """Checks whether the most frequent character in the text is in the list 71 | of most frequent characters in English text.""" 72 | freq_list = self._SortByFrequency(s) 73 | return (freq_list[0] in 'ETAOIN ') 74 | 75 | def IsEnglishPlaintext(self, s, check_frequency=True): 76 | # Text starts with a capital letter. 77 | if s[0] < 'A' or s[0] > 'Z': 78 | return False 79 | 80 | if not self._MatchedParentheses(s): 81 | return False 82 | 83 | if not self._CheckPunctuaction(s): 84 | return False 85 | 86 | if check_frequency and not self.CheckFrequency(s): 87 | return False 88 | 89 | return True -------------------------------------------------------------------------------- /set1/decode_xor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Decode an hexadecimal string that was XOR-encoded using a shorter key. 3 | """ 4 | 5 | import itertools 6 | from ch02 import FixedXORBins 7 | from plaintext_verifier import PlaintextVerifier 8 | 9 | from utils import encoding_utils as enclib 10 | import math 11 | 12 | class XORDecoder(object): 13 | def __init__(self, key_length): 14 | """Init the decoder with a specific key length.""" 15 | self._key_length = key_length 16 | 17 | # Generates all the possible keys for the encoding, as 18 | # binary strings. 19 | self._all_keys = [] 20 | for byte in map(''.join, itertools.product( 21 | '01', repeat=key_length)): 22 | self._all_keys.append(byte) 23 | 24 | def DecodeBin(self, encoded_string, frequency_only=False): 25 | """Decodes a string represented in binary format. 26 | Returns a list of tuple. Each element contains a decoding we detected 27 | to likely be English plaintext, and the binary key used for that decoding. 28 | 29 | If frequency_only is True, only consider character frequency when deciding 30 | whether a string is English plaintext. 31 | 32 | Raises exceptions if the string is not in the expected format. 33 | """ 34 | decodings = [] 35 | verifier = PlaintextVerifier() 36 | 37 | for key in self._all_keys: 38 | # Repeats the key as many time as necessary to produce a string 39 | # of the same length as the one we are decoding. 40 | fixed_key = key * int(len(encoded_string) / self._key_length) 41 | 42 | # Computes the XOR between the string and the key. The result 43 | # is again in binary form. 44 | decoded_bin = FixedXORBins(fixed_key, encoded_string) 45 | ascii_candidate, is_valid_candidate = enclib.BinToAscii( 46 | decoded_bin) 47 | 48 | if not is_valid_candidate: 49 | continue 50 | 51 | if frequency_only and verifier.CheckFrequency(ascii_candidate): 52 | decodings.append((ascii_candidate, key)) 53 | elif not frequency_only and verifier.IsEnglishPlaintext(ascii_candidate): 54 | decodings.append((ascii_candidate, key)) 55 | 56 | return decodings 57 | 58 | def DecodeHex(self, encoded_string, frequency_only=False): 59 | """Decodes a string represented in hexadecimal ASCII format. 60 | Returns a list of tuple. Each element contains a decoding we detected 61 | to likely be English plaintext, and the binary key used for that decoding. 62 | 63 | Raises exceptions if the string is not in the expected format. 64 | """ 65 | bin_string = enclib.HexToBin(encoded_string) 66 | return self.DecodeBin(bin_string, frequency_only) -------------------------------------------------------------------------------- /set3/ch18.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import base64 4 | from Crypto.Cipher import AES 5 | from Crypto.Util import Counter 6 | 7 | # Set 3, Challenge 18: implement CTR, the stream cipher mode. 8 | 9 | def BlockXOR(b1, b2): 10 | """XOR of two blocks of bytes. b2 is allowed to be longer 11 | than b1, the extra bytes will simply be ignored.""" 12 | res = b'' 13 | 14 | for bIndex in range(0, len(b1)): 15 | res += bytes([b1[bIndex] ^ b2[bIndex]]) 16 | 17 | return res 18 | 19 | def DoCTR(text, key, nonce): 20 | """The algorithm is the same for both encryption and decryption.""" 21 | # Uses ECB inside for a single block. 22 | cipher = AES.new(key=key, mode=AES.MODE_ECB) 23 | resultingText = b'' 24 | 25 | keystream = b'' 26 | # There is no padding in CTR mode, so both a plaintext and a ciphertext 27 | # can be of non-exact block size. We decrypt the last block by generating 28 | # the keystream and only using the bytes we need. 29 | numBlocks = int(len(text) / AES.block_size) 30 | counter = 0 31 | 32 | # +1 to get the final bytes that do not fit in a block. 33 | for blockIndex in range(0, numBlocks + 1): 34 | # If at the end, simply get all the remaining bytes; there 35 | # will be exactly AES.block_size or less of them. Otherwise, 36 | # get a block. 37 | if blockIndex == numBlocks: 38 | block = text[blockIndex * AES.block_size :] 39 | else: 40 | block = text[blockIndex * AES.block_size : (blockIndex + 1) * AES.block_size] 41 | 42 | # The keystream generator is composed by 8 bytes of the nonce + 43 | # 8 bytes of counter, little-endian. 44 | keystreamGen = bytes([nonce]) * 8 45 | # This is a quick hack that only works if there are not enough 46 | # blocks of text to need the second digit of the counter too. 47 | # Since that is a lot of blocks we can be a bit lazy. 48 | keystreamGen += bytes([counter, 0, 0, 0, 0, 0, 0, 0]) 49 | 50 | # Careful, we always encrypt the keystream even when decrypting. 51 | keystream = cipher.encrypt(keystreamGen) 52 | 53 | # The PT is simply the CT XORed with the keystream, and vice versa. 54 | # Since the PT only depends on the same block of CT plus a predictable 55 | # keystream, CTR is very suited for parallel decryption of many blocks. 56 | resultingText += BlockXOR(block, keystream) 57 | counter += 1 58 | 59 | return resultingText 60 | 61 | if __name__ == '__main__': 62 | c64 = 'L77na/nrFsKvynd6HzOoG7GHTLXsTVu9qvY/2syLXzhPweyyMTJULu/6/kXX0KSvoOLSFQ==' 63 | ciphertext = base64.b64decode(c64) 64 | 65 | print(DoCTR(ciphertext, b'YELLOW SUBMARINE', 0).decode('ascii')) 66 | -------------------------------------------------------------------------------- /set7/ch49.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from Crypto.Cipher import AES 4 | from Crypto import Random 5 | from Crypto.Util.strxor import strxor 6 | 7 | # Set 7, challenge 49: CBC-MAC message forgery. 8 | 9 | def GenerateTransaction(aSender, aDest, amount): 10 | t = 'from=%s&to=%s&amount=%s' % (aSender, aDest, str(amount)) 11 | # Converts to byte string. 12 | return str.encode(t) 13 | 14 | # Computes CBC-MAC. 15 | def ComputeMac(message, cipher): 16 | ct = cipher.encrypt(message) 17 | return ct[-AES.block_size:] 18 | 19 | # Here we just build the request, assume we send it to the server and 20 | # handle their response too :) 21 | def SendMessage(message): 22 | iv = Random.new().read(AES.block_size) 23 | key = b'Sixteen byte key' 24 | 25 | cipher = AES.new(key, AES.MODE_CBC, iv) 26 | return (message + iv + ComputeMac(message, cipher)) 27 | 28 | def VerifyRequest(request): 29 | mac = request[-AES.block_size:] 30 | iv = request[-(AES.block_size * 2):-AES.block_size] 31 | message = request[:-(AES.block_size * 2)] 32 | 33 | # Key is shared between server and client. IV is taken from the 34 | # request, as it's different for each message. 35 | cipher = AES.new(b'Sixteen byte key', AES.MODE_CBC, iv) 36 | myMac = ComputeMac(message, cipher) 37 | return (myMac == mac) 38 | 39 | if __name__ == "__main__": 40 | # Assume we can send this message and it will get rejected because 41 | # -1 is not a valid account ID. 42 | firstMessage = GenerateTransaction('n.-1', 'n.20', 1000000) 43 | firstRequest = SendMessage(firstMessage) 44 | 45 | # As attackers, we sniff the request and are able to retrieve the IV 46 | # and MAC. 47 | iv = firstRequest[-(AES.block_size * 2):-AES.block_size] 48 | mac = firstRequest[-AES.block_size:] 49 | 50 | # Generate the transaction we actually want. 51 | forgedMessage = GenerateTransaction('n.00', 'n.20', 1000000) 52 | # Derive forgedIv such that: 53 | # (firstMessage XOR iv) = (forgedMessage XOR forgedIv) 54 | # If this happens, since the key is always the same, we are guaranteed 55 | # this request has the same MAC as the first one. 56 | forgedIv = strxor(iv, strxor(firstMessage[:16], forgedMessage[:16])) 57 | 58 | # Create the request with the forged IV and the previous MAC. 59 | forgedRequest = forgedMessage + forgedIv + mac 60 | print('Forged request', forgedRequest) 61 | 62 | if VerifyRequest(forgedRequest): 63 | print('Forgery successful') 64 | else: 65 | print('Forgery failed') 66 | -------------------------------------------------------------------------------- /set6/ch45.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import dsa 4 | 5 | # Set 6, challenge 45: DSA parameter tampering. 6 | 7 | # We do not actually run it because to do it we need to relax the DSA implementation. 8 | # In the sign() function, the generated r, the first part of the signature, is always 0, 9 | # which is not actually allowed by the specification. This is because when you compute 10 | # modexp(g, u, p), you get 0^u = 0, and then 0 % p = 0 (for any u and p). 11 | # 12 | # If you remove the restriction and return a signature with r=0, it does not verify 13 | # the same string it signed. 14 | def Generator0(): 15 | dsa_params = { 16 | 'Q': int("0xf4f47f05794b256174bba6e9b396a7707e563c5b", 16), 17 | 'P': int("0x800000000000000089e1855218a0e7dac38136ffafa72eda7859f2171e25e65eac698c1702578b07dc2a1076da241c76c62d374d8389ea5aeffd3226a0530cc565f3bf6b50929139ebeac04f48c3c84afb796d61e5a4f9a8fda812ab59494232c7d2b4deb50aa18ee9e132bfa85ac4374d7f9091abc3d015efc871a584471bb1", 16), 18 | 'G': 0, 19 | } 20 | 21 | privateKey, publicKey = dsa.generate_pair(dsa_params['P'], dsa_params['G'], dsa_params['Q']) 22 | 23 | sig = dsa.dsa_sign(dsa_params['Q'], dsa_params['P'], dsa_params['G'], privateKey, dsa.HashMessage(b'And now for something completely different')) 24 | print(sig) 25 | 26 | res = dsa.dsa_verify(sig[0], sig[1], dsa_params['G'], dsa_params['P'], dsa_params['Q'], publicKey, dsa.HashMessage(b'And now for something completely different')) 27 | print(res) 28 | 29 | def GeneratorPPlus1(): 30 | dsa_params = { 31 | 'Q': int("0xf4f47f05794b256174bba6e9b396a7707e563c5b", 16), 32 | 'P': int("0x800000000000000089e1855218a0e7dac38136ffafa72eda7859f2171e25e65eac698c1702578b07dc2a1076da241c76c62d374d8389ea5aeffd3226a0530cc565f3bf6b50929139ebeac04f48c3c84afb796d61e5a4f9a8fda812ab59494232c7d2b4deb50aa18ee9e132bfa85ac4374d7f9091abc3d015efc871a584471bb1", 16), 33 | } 34 | dsa_params['G'] = dsa_params['P'] + 1 35 | 36 | privateKey, publicKey = dsa.generate_pair(dsa_params['P'], dsa_params['G'], dsa_params['Q']) 37 | 38 | # The generated signature is verified against any string, provided the public 39 | # key is correct. 40 | sig = dsa.dsa_sign(dsa_params['Q'], dsa_params['P'], dsa_params['G'], privateKey, dsa.HashMessage(b'Hello world!')) 41 | res = dsa.dsa_verify(sig[0], sig[1], dsa_params['G'], dsa_params['P'], dsa_params['Q'], publicKey, dsa.HashMessage(b'Goodbye world!')) 42 | if res: 43 | print('[**] Attack with G=P+1 successful.') 44 | else: 45 | print('[!!] Attack with G=P+1 failed.') 46 | 47 | 48 | if __name__ == '__main__': 49 | # Generator0() 50 | GeneratorPPlus1() 51 | -------------------------------------------------------------------------------- /set7/ch56.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from Crypto.Cipher import ARC4 4 | from Crypto import Random 5 | import base64 6 | import collections 7 | 8 | # Set 7, challenge 56: RC4 single-byte biases. 9 | 10 | def RC4Oracle(request): 11 | cookie = base64.b64decode('QkUgU1VSRSBUTyBEUklOSyBZT1VSIE9WQUxUSU5F') 12 | plaintext = request + cookie 13 | 14 | key = Random.new().read(16) 15 | cipher = ARC4.new(key) 16 | return cipher.encrypt(plaintext) 17 | 18 | def IsReadableAscii(byte): 19 | return byte >= 32 and byte <= 126 20 | 21 | if __name__ == '__main__': 22 | # This is a bit more than 16 millions, so recovering each byte 23 | # takes around 25 minutes. Less means we don't get a clear signal 24 | # to pick the right byte. The challenge suggested 2 ** 32 but that 25 | # would be much slower. 26 | n = 2 ** 24 27 | 28 | # I only wrote the part for the 16th byte; this means that we can 29 | # only recover the first 16 bytes of the cookie (or less if there 30 | # is a lower limit on the size of the request we must pass); we 31 | # can recover the rest by doing the same with biasIndex = 31; 32 | # the bias this time is toward byte 224. 33 | biasIndex = 15 34 | recoveredCookie = '' 35 | 36 | for reqIndex in range(biasIndex): 37 | # Fill up the request with random data so that the "next" 38 | # byte of the cookie (from 0 to 16) is aligned with byte 16 39 | # of the RC4 keystream. 40 | request = b'A' * (biasIndex - reqIndex) 41 | # We count the number of occurrences for each byte here. We 42 | # could skip non-readable bytes but that does not help us 43 | # much. 44 | occurrences16 = collections.defaultdict(int) 45 | 46 | for i in range(n): 47 | if i % (10 ** 6) == 0: 48 | print('Iteration', i) 49 | 50 | # The idea is that if we bet that byte 16 of the keystream 51 | # is always equal to the bias, 240 here, then the plaintext 52 | # byte is the bias XOR the ciphertext byte. 53 | # This is not true for every case, but it is more likely; 54 | # enough encryptions of the same plaintext with a different 55 | # key are going to show a bias toward a specific byte, which 56 | # we are going to mark as our recovered plaintext byte. 57 | ciphertext = RC4Oracle(request) 58 | byte16 = ciphertext[biasIndex] ^ 240 59 | 60 | occurrences16[byte16] += 1 61 | 62 | # Get the key whose value is the maximum in the dictionary, 63 | # converted to an ASCII character for readability. 64 | recoveredCookie += chr(max(occurrences16.keys(), key=lambda k: occurrences16[k])) 65 | print('Recovered cookie is:', recoveredCookie) 66 | 67 | print(recoveredCookie) 68 | 69 | -------------------------------------------------------------------------------- /set2/ch11.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 2, challenge 11: An ECB/CBC detection oracle. 4 | 5 | from Crypto.Cipher import AES 6 | from Crypto import Random 7 | 8 | import aes_lib 9 | import collections 10 | import random 11 | 12 | def RandomEncrypt(plaintext): 13 | """Returns a tuple with the encrypted text and the mode used.""" 14 | # Random key. 15 | key = Random.new().read(AES.block_size) 16 | 17 | # Random padding both before and after the plaintext. The 18 | # size of the second padding cannot be random since the result 19 | # needs to have a number of bytes multiple of 16. 20 | paddingSize = random.randint(5, 10) 21 | prepend = Random.new().read(paddingSize) 22 | append = Random.new().read(AES.block_size - paddingSize) 23 | 24 | # Pick encryption mode at random. 25 | mode = None 26 | if random.randint(0, 1) == 0: 27 | mode = AES.MODE_ECB 28 | else: 29 | mode = AES.MODE_CBC 30 | 31 | # Perform the encryption. 32 | aes = aes_lib.AESCipher(key, mode=mode) 33 | text = prepend + plaintext + append 34 | return (aes.aes_encrypt(text), mode) 35 | 36 | def CountRepeatedBlocks(byte_string): 37 | """Counts how many 16-bytes chunks in the string are 38 | repeated more than once. The input is a byte string.""" 39 | chunk_start = 0 40 | CHUNK_SIZE = 16 41 | 42 | # New keys get a default value of 0. The dictionary will 43 | # count the number of occurrences of each 16-bytes chunk in 44 | # the string. 45 | chunkCounter = collections.defaultdict(int) 46 | 47 | while chunk_start < len(byte_string): 48 | chunk = byte_string[chunk_start : chunk_start + CHUNK_SIZE] 49 | chunkCounter[chunk] += 1 50 | chunk_start += CHUNK_SIZE 51 | 52 | # This generator adds 1 for each value in the dictionary that 53 | # is greater than 1. 54 | return sum(1 for c in chunkCounter if chunkCounter[c] > 1) 55 | 56 | def DetectEncryptionMode(ciphertext): 57 | """We can only recognize ECB if there is at least one repeated block 58 | in the ciphertext. But this won't always work, as different input 59 | blocks will produce different output blocks with ECB too.""" 60 | repetitions = CountRepeatedBlocks(ciphertext) 61 | return AES.MODE_ECB if repetitions > 0 else AES.MODE_CBC 62 | 63 | def ModeToString(mode): 64 | """Convenience method for printing AES modes.""" 65 | if mode == 1: 66 | return 'ECB' 67 | elif mode == 2: 68 | return 'CBC' 69 | 70 | if __name__ == '__main__': 71 | plaintext = b'B' * 1024 72 | 73 | ciphertext, realMode = RandomEncrypt(plaintext) 74 | mode = DetectEncryptionMode(ciphertext) 75 | 76 | print ('[**] Mode ' + ModeToString(mode) + ' detected.') 77 | print ('[**] Real mode was ' + ModeToString(realMode)) 78 | -------------------------------------------------------------------------------- /set4/ch25.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 4, challenge 25: Break "random access read/write" AES CTR. 4 | 5 | import base64 6 | import block_utils 7 | import ctr 8 | from Crypto.Cipher import AES 9 | from Crypto import Random 10 | 11 | 12 | def EditCTR(ciphertext, offset, newText, ctrObj): 13 | numBlocks = block_utils.GetNumBlocks(ciphertext) 14 | 15 | # Sanity checking. 16 | if offset < 0 or offset > numBlocks - 1: 17 | raise ValueError("Invalid offset.") 18 | 19 | if len(newText) != AES.block_size: 20 | raise ValueError("New plaintext must be 1 block in size") 21 | 22 | # Encrypt the new block of text using the value of the 23 | # counter for the 'offset' block of the ciphertext. The idea 24 | # is that newBlock will replace the block at position 'offset' 25 | # in the ciphertext, although here we do not perform the 26 | # actual substitution. 27 | newBlock = ctrObj.OneBlockCrypt(newText, offset) 28 | return newBlock 29 | 30 | # This function is only here to recover the text as explained 31 | # in the challenge. Of course here we need to know the key :) 32 | def GetCTRCiphertext(sourceFilename): 33 | originalText = '' 34 | 35 | with open(sourceFilename, 'r') as input_file: 36 | originalText = input_file.read() 37 | 38 | aes = AES.new(b'YELLOW SUBMARINE', mode=AES.MODE_ECB, 39 | IV=Random.new().read(AES.block_size)) 40 | plaintext = aes.decrypt(base64.b64decode(originalText)) 41 | return ctr.DoCTR(plaintext, b'YELLOW SUBMARINE', 0) 42 | 43 | if __name__ == '__main__': 44 | ciphertext = GetCTRCiphertext('data/25.txt') 45 | numBlocks = block_utils.GetNumBlocks(ciphertext) 46 | 47 | # Here we should hide the key and nonce, pretend they are both 48 | # generated internally to AESCtr :) 49 | ctrObj = ctr.AESCtr(b'YELLOW SUBMARINE', 0) 50 | recoveredPlaintext = '' 51 | 52 | # Very simple idea: to do this replacement of ciphertext 53 | # blocks, I need to use the same nonce and counter as in 54 | # the original encryption, otherwise decryption will not 55 | # work. 56 | # But doing this, I can retrieve what the corresponding 57 | # byte of the keystream is, and from that the original 58 | # plaintext block by XORin the recovered keystream with 59 | # the same byte in the original ciphertext. Not very 60 | # smart... 61 | for blockIndex in range(0, numBlocks): 62 | newBlock = EditCTR( 63 | ciphertext, blockIndex, b'my new plaintext', ctrObj) 64 | keystream = block_utils.BlockXOR(b'my new plaintext', newBlock) 65 | 66 | plainBlock = block_utils.BlockXOR( 67 | keystream, block_utils.GetSingleBlock(ciphertext, blockIndex)) 68 | recoveredPlaintext += plainBlock.decode('ascii') 69 | 70 | print(recoveredPlaintext) 71 | -------------------------------------------------------------------------------- /set5/dh_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from Crypto.Cipher import AES 4 | from Crypto import Random 5 | 6 | import ch33 as dh 7 | import hashlib 8 | import random 9 | import socket 10 | import sys 11 | 12 | # To start the verification process, we derive a private key from the 13 | # secret, encrypt a message using AES with the secret key, and send it to 14 | # the server. The server will verify it is able to decrypt the message. 15 | # 16 | # For a complete verification we should also repeat this the other 17 | # way around and verify we are able to decrypt the server's messages. 18 | def VerifyKey(socket, secret): 19 | sha1 = hashlib.sha1() 20 | sha1.update(str(secret).encode()) 21 | privateKey = sha1.hexdigest() 22 | 23 | message = b'I am making a note here, huge success!' 24 | message += b'\x00' * 10 25 | iv = b'\x00' * AES.block_size 26 | cipher = AES.new(key=privateKey[:16], mode=AES.MODE_CBC, IV=iv) 27 | 28 | ciphertext = cipher.encrypt(message) 29 | socket.send(ciphertext) 30 | 31 | def DHExchangeClient(serverHost, port): 32 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 33 | sock.connect((serverHost, port)) 34 | 35 | # Usual parameters as recommended by NIST. 36 | pHex = ('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e340' 37 | '4ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f40' 38 | '6b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8f' 39 | 'd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff') 40 | p = int(pHex, 16) 41 | g = 2 42 | 43 | a = random.randint(0, p - 1) 44 | A = dh.modexp(g, a, p) 45 | B = 0 46 | 47 | # Sends the message in the predefined format. 48 | message = 'BEGIN\n%s\n%s\n%s\nEND' % (str(p), str(g), str(A)) 49 | sock.send(message.encode()) 50 | 51 | # Gets a similar message back from the server. See the comments 52 | # on dh_server which has a similar loop. 53 | exchange = b'' 54 | while b'D' not in exchange: 55 | exchange += sock.recv(100) 56 | 57 | exchange = exchange.decode() 58 | pieces = exchange.split('\n') 59 | 60 | B = int(pieces[1]) 61 | secret = dh.modexp(B, a, p) 62 | 63 | print('My secret is', str(secret)) 64 | VerifyKey(sock, secret) 65 | 66 | sock.close() 67 | 68 | if __name__ == '__main__': 69 | if len(sys.argv) != 2: 70 | print('Usage: ./dh_client {good|bad}') 71 | sys.exit(1) 72 | 73 | mode = sys.argv[1] 74 | # Connects to the real server. 75 | if mode == 'good': 76 | DHExchangeClient('localhost', 10000) 77 | # Connects to our bad server doing a MITM attack. 78 | elif mode == 'bad': 79 | DHExchangeClient('localhost', 10002) 80 | else: 81 | print('Cannot recognize mode:', mode) 82 | -------------------------------------------------------------------------------- /set4/ch29.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ch28 import Sha1Sign 4 | from ch28 import Sha1Hash 5 | import hashlib 6 | import struct 7 | 8 | # Set 4, challenge 29: break a SHA-1 keyed MAC using length extension. 9 | 10 | def makeGluePadding(message, keylen): 11 | byteLength = len(message) + keylen 12 | padding = b'' 13 | 14 | # Appends the bit '1' to the message. 15 | padding += b'\x80' 16 | # Appends the byte '0' k times; k is such that the resulting 17 | # message length in bits is congruent to 64. 18 | padding += b'\x00' * ((56 - (byteLength + 1) % 64) % 64) 19 | # Appends the message length in bits as a 64-bit big-endian 20 | # integer (Q is unsigned long long for the struct module). 21 | padding += struct.pack(b'>Q', byteLength * 8) 22 | return padding 23 | 24 | def RecoverInternalState(secretDecimalHash): 25 | a = secretDecimalHash >> 128 26 | b = (secretDecimalHash >> 96) & 0xffffffff 27 | c = (secretDecimalHash >> 64) & 0xffffffff 28 | d = (secretDecimalHash >> 32) & 0xffffffff 29 | e = secretDecimalHash & 0xffffffff 30 | return [a, b, c, d, e] 31 | 32 | def GetRealDigest(message): 33 | sha = hashlib.sha1() 34 | sha.update(b'YELLOW SUBMARINE') 35 | sha.update(message) 36 | return sha.hexdigest() 37 | 38 | # FIXME: here we assume to know the exact length of the secret key 39 | # used to produce the MAC. We need our algorithm to discover the 40 | # length itself. 41 | if __name__ == '__main__': 42 | message = b'comment1=cooking%20MCs;userdata=foo;comment2=%20like%20a%20pound%20of%20bacon' 43 | secretHash = Sha1Sign(message) 44 | 45 | # Even if we try all possibilities up to 10.000, the loop only 46 | # takes a few seconds, so brute force works here. Secret keys are 47 | # also generally smaller than 10.000 bytes. 48 | for keyLen in range(1, 10000): 49 | decimalHash = int(secretHash, 16) 50 | forgedPart = b';admin=true' 51 | 52 | # The new message length is the length of the key + the 53 | # original message + the artificial padding glue + the 54 | # part we added ourselves. 55 | glue = makeGluePadding(message, keyLen) 56 | # After the forged part, the SHA1 algorithm may add more 57 | # padding, but that happens internally so we do not need 58 | # to factor its length here. 59 | fakeLen = keyLen + len(message) + len(glue) + len(forgedPart) 60 | 61 | # Create a SHA1 object with our own H vector. This allows us 62 | # to extend the hash from where we started. 63 | h = RecoverInternalState(decimalHash) 64 | sha1 = Sha1Hash(h) 65 | digest = sha1.digest(b';admin=true', fakeLen) 66 | realThing = GetRealDigest(message + glue + forgedPart) 67 | 68 | if digest == realThing: 69 | print('[**] We did it') 70 | print(digest) 71 | print('Key length was', keyLen) 72 | break 73 | else: 74 | print('Nothing found for key lengths up to 10000 bytes') 75 | -------------------------------------------------------------------------------- /set5/dh_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import ch33 as dh 4 | import hashlib 5 | from Crypto import Random 6 | from Crypto.Cipher import AES 7 | import random 8 | import socket 9 | 10 | # Well, we do not actually verify anything, but we print it. If the MITM 11 | # attack is successful, the server will pass this verification without 12 | # any knowledge that somebody else is reading the messages too. 13 | def VerifySecret(socket, message, secret): 14 | # Converts the secret to a string and hashes it with SHA1. 15 | sha1 = hashlib.sha1() 16 | sha1.update(str(secret).encode()) 17 | privateKey = sha1.hexdigest() 18 | 19 | # Laziness! This should be appended to the ciphertext itself by the client, 20 | # but I am going to skip that and just pretend we exchanged that already. 21 | iv = b'\x00' * AES.block_size 22 | # Taking only 16 digits from a longer hash does not seem like a very secure 23 | # method, but it's what the challenge recommends for longer hashes. 24 | cipher = AES.new(key=privateKey[:16], mode=AES.MODE_CBC, IV=iv) 25 | plaintext = cipher.decrypt(message) 26 | print(plaintext) 27 | 28 | 29 | def DHExchangeServer(clientsocket): 30 | A = 0 31 | p = 0 32 | g = 0 33 | exchange = b'' 34 | 35 | # In this scenario, the DH exchange message has this form: 36 | # BEGIN 37 | # p 38 | # g 39 | # A 40 | # END 41 | # (including newlines). Therefore we keep reading 100 bytes 42 | # until we read D. We do not try and read the whole word 43 | # because it could fall across a boundary. However this only 44 | # works because the D is unique (p, g and A are integers). 45 | while b'D' not in exchange: 46 | exchange += clientsocket.recv(100) 47 | 48 | exchange = exchange.decode() 49 | pieces = exchange.split('\n') 50 | 51 | p = int(pieces[1]) 52 | g = int(pieces[2]) 53 | A = int(pieces[3]) 54 | 55 | # Computes B and then the secret. 56 | b = random.randint(0, g - 1) 57 | B = dh.modexp(g, b, p) 58 | secret = dh.modexp(A, b, p) 59 | 60 | # Perform our side of the exchange, sending B to the client 61 | # with the same format. 62 | messageForClient = 'BEGIN\n%s\nEND' % str(B) 63 | clientsocket.send(messageForClient.encode()) 64 | return secret 65 | 66 | 67 | if __name__ == '__main__': 68 | serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 69 | # Bind the socket locally. 70 | serversocket.bind(('localhost', 10000)) 71 | serversocket.listen(5) 72 | 73 | # Dumb single-threaded server. 74 | while True: 75 | clientsocket, _ = serversocket.accept() 76 | print('- Accepted new connection.') 77 | 78 | # Perform the DH exchange, server-side, and compute 79 | # the secret number. 80 | secret = DHExchangeServer(clientsocket) 81 | print('The computed secret is', str(secret)) 82 | 83 | verificationMessage = clientsocket.recv(48) 84 | VerifySecret(clientsocket, verificationMessage, secret) 85 | -------------------------------------------------------------------------------- /set6/ch43.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import dsa 4 | from utils import modexp 5 | import rsa 6 | 7 | # Set 6, challenge 43: DSA key recovery from nonce. 8 | 9 | # Finds the solution in around 16 seconds. 10 | def BreakDSA(p, g, q, r, s): 11 | publicKey = int('0x84ad4719d044495496a3201c8ff484feb45b962e7302e56a392aee4abab3e4bdebf2955b4736012f21a08084056b19bcd7fee56048e004e44984e2f411788efdc837a0d2e5abb7b555039fd243ac01f0fb2ed1dec568280ce678e931868d23eb095fde9d3779191b8c0299d6e07bbb283e6633451e535c45513b2d33c99ea17', 16) 12 | 13 | H = dsa.HashMessage(b'For those that envy a MC it can be hazardous to your health\n' 14 | b'So be friendly, a matter of life and death, just like a etch-a-sketch\n') 15 | 16 | k = 0 17 | brokenKey = None 18 | 19 | for k in range(1, 2 ** 16 + 1): 20 | top = (s * k) - H 21 | bottom = rsa.invmod(r, q) 22 | privateKey = (top * bottom) % q 23 | 24 | # Derive the public key from the private key and compare it 25 | # with the one we know. 26 | testPub = modexp(g, privateKey, p) 27 | if testPub == publicKey: 28 | brokenKey = privateKey 29 | break 30 | else: 31 | print('[!!] Unable to break private key') 32 | return 33 | 34 | print('[**] Success!') 35 | print('K:', k) 36 | print('X:', brokenKey) 37 | return brokenKey, k 38 | 39 | 40 | if __name__ == '__main__': 41 | message = b'For those that envy a MC it can be hazardous to your health\nSo be friendly, a matter of life and death, just like a etch-a-sketch\n' 42 | r = 548099063082341131477253921760299949438196259240 43 | s = 857042759984254168557880549501802188789837994940 44 | 45 | dsa_params = { 46 | 'Q': int("0xf4f47f05794b256174bba6e9b396a7707e563c5b", 16), 47 | 'P': int("0x800000000000000089e1855218a0e7dac38136ffafa72eda7859f2171e25e65eac698c1702578b07dc2a1076da241c76c62d374d8389ea5aeffd3226a0530cc565f3bf6b50929139ebeac04f48c3c84afb796d61e5a4f9a8fda812ab59494232c7d2b4deb50aa18ee9e132bfa85ac4374d7f9091abc3d015efc871a584471bb1", 16), 48 | 'G': int("5958c9d3898b224b12672c0b98e06c60df923cb8bc999d119458fef538b8fa4046c8db53039db620c094c9fa077ef389b5322a559946a71903f990f1f7e0e025e2d7f7cf494aff1a0470f5b64c36b625a097f1651fe775323556fe00b3608c887892878480e99041be601a62166ca6894bdd41a7054ec89f756ba9fc95302291", 16), 49 | } 50 | 51 | # To verify we have found the right k and private key, we sign the same message 52 | # using both of them. If the signatures match with the one we got from the 53 | # challenge, it is correct. 54 | brokenKey, brokenK = BreakDSA(dsa_params['P'], dsa_params['G'], dsa_params['Q'], r, s) 55 | sig = dsa.dsa_sign(dsa_params['Q'], dsa_params['P'], dsa_params['G'], brokenKey, 56 | dsa.HashMessage(message), k=brokenK) 57 | 58 | if sig == (r, s): 59 | print('[**] The signatures (with real and broken private key) match!') 60 | else: 61 | print('[!!] The signatures (with real and broken private key) do not match!') 62 | -------------------------------------------------------------------------------- /set4/ch31.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import hashlib 4 | import hmac 5 | import time 6 | 7 | # Set 4, challenge 31: Implement and break HMAC-SHA1 with an artificial timing leak. 8 | 9 | # Compute the HMAC of the file content. 10 | def ComputeHMAC(filename): 11 | bindata = None 12 | 13 | with open(filename, 'rb') as binfile: 14 | bindata = binfile.read() 15 | 16 | h = hmac.new(key=b'YELLOW SUBMARINE', digestmod=hashlib.sha1) 17 | h.update(bindata) 18 | return h.digest() 19 | 20 | def InsecureCompare(sig1, sig2): 21 | for b in range(len(sig1)): 22 | if sig1[b] != sig2[b]: 23 | return False 24 | time.sleep(0.05) 25 | 26 | return True 27 | 28 | def verifySignature(filename, signature): 29 | realSig = ComputeHMAC(filename) 30 | return InsecureCompare(realSig, signature) 31 | 32 | 33 | if __name__ == '__main__': 34 | filename = 'data/25.txt' 35 | 36 | # "Cheating" to verify we cracked it right. 37 | realSig = ComputeHMAC(filename) 38 | print('[**] Real signature is ' + str(realSig)) 39 | 40 | recoveredSigPart = b'' 41 | hacked = False 42 | 43 | for sigChar in range(0, 20): 44 | print('[**] Recovering character ' + str(sigChar)) 45 | if hacked: 46 | break 47 | 48 | # We try each possible byte; we stop when we find the byte 49 | # for which the verification signature takes more than 50 ms times 50 | # how many correct bytes we have before this one. 51 | # 52 | # It is also possible that we get subsequent bytes correct by 53 | # accident. This means we are cracking the byte of index 0, but 54 | # byte of index 1 is '\x00' so that is correct too. If this happens 55 | # we still get a running time greater than 50 ms times the correct 56 | # bytes we know about, so it works. It is not very efficient though 57 | # as we will repeat the same cracking procedure for byte 1 despite 58 | # already discovering its value; we can consider the situation unlikely 59 | # enough to not have a great impact on performance in the average case. 60 | for byte in range(0, 256): 61 | missingPartLen = 20 - sigChar - 1 62 | signature = recoveredSigPart + bytes([ byte ]) + b'\x00' * missingPartLen 63 | 64 | start_time = time.time() 65 | matches = verifySignature(filename, signature) 66 | end_time = time.time() 67 | 68 | if matches: 69 | print('Signature hacked: ' + str(signature)) 70 | hacked = True 71 | break 72 | 73 | functiming = (end_time - start_time) * 1000 74 | # This requires us "knowing" or "discovering" the 50ms timing leak, which 75 | # stands out quite a bit if you look at all the timings for each possible 76 | # byte value. 77 | # Another option is to keep the value which generates the maximum running 78 | # time. Both methods should break at the next challenge 79 | if functiming >= (50 * (sigChar + 1)): 80 | print('Recovered as ' + str(bytes([byte]))) 81 | recoveredSigPart += bytes([byte]) 82 | break 83 | 84 | print(recoveredSigPart) 85 | -------------------------------------------------------------------------------- /set6/ch41.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import random 4 | import rsa 5 | 6 | # Set 6, challenge 41: Implement unpadded message recovery oracle. 7 | 8 | # Implements our "bad" server for the challenge. It stores all 9 | # ciphertexts it sees and raises an error if asked to decrypt 10 | # a ciphertext more than once. 11 | class Oracle(object): 12 | def __init__(self, privateKey): 13 | self._past_ciphertexts = {} 14 | self._privateKey = privateKey 15 | 16 | def decrypt(self, ciphertext): 17 | if ciphertext in self._past_ciphertexts: 18 | raise ValueError('Ciphertext was already decrypted') 19 | 20 | self._past_ciphertexts[ciphertext] = True 21 | return rsa.Decrypt(self._privateKey, ciphertext) 22 | 23 | 24 | # Taken from 25 | # http://stackoverflow.com/questions/5486204/fast-modulo-calculations-in-python-and-ruby 26 | # Required because any regular library blows up computing this with such huge numbers. 27 | def modexp(g, u, p): 28 | """Computes s = (g ^ u) mod p 29 | Args are base, exponent, modulus 30 | (see Bruce Schneier's book, _Applied Cryptography_ p. 244) 31 | """ 32 | s = 1 33 | while u != 0: 34 | if u & 1: 35 | s = (s * g) % p 36 | u >>= 1 37 | g = (g * g) % p 38 | return s 39 | 40 | # Generates 5 different random numbers to run through this attack. 41 | def GetCiphertexts(publicKey): 42 | plaintexts = set() 43 | ciphertexts = [] 44 | 45 | while len(plaintexts) < 5: 46 | plaintexts.add(random.randint(0, 50)) 47 | 48 | for pt in plaintexts: 49 | ciphertexts.append(rsa.Encrypt(publicKey, pt)) 50 | 51 | print('My random messages:', plaintexts) 52 | return ciphertexts 53 | 54 | 55 | # Modifies the ciphertext so that it is accepted by the oracle, but 56 | # the plaintext can be recovered anyway. 57 | def Hide(c, publicKey): 58 | e, n = publicKey 59 | s = 14 # random s > 1 60 | 61 | newCiphertext = (modexp(s, e, n) * c) % n 62 | return newCiphertext 63 | 64 | 65 | # The opposite as Hide; it takes the plaintext as decrypted by 66 | # the oracle and retrieves the one we meant. 67 | def Reveal(text, publicKey): 68 | _, n = publicKey 69 | s = 14 # same as above 70 | 71 | # We are in modulo N group, so instead of dividing by n, we 72 | # need to multiply by its inverse in the group. 73 | inverse = rsa.invmod(s, n) 74 | return (text * inverse) % n 75 | 76 | 77 | if __name__ == '__main__': 78 | publicKey, privateKey = rsa.GenerateRSAPair(128) 79 | oracle = Oracle(privateKey) 80 | 81 | ciphertexts = GetCiphertexts(publicKey) 82 | 83 | plaintexts = [oracle.decrypt(ct) for ct in ciphertexts] 84 | print('I decrypted with the oracle:', plaintexts) 85 | 86 | newCiphertexts = [Hide(ct, publicKey) for ct in ciphertexts] 87 | # Without the hiding step, this would raise errors. 88 | intermediate = [oracle.decrypt(new) for new in newCiphertexts] 89 | 90 | plaintexts = [Reveal(i, publicKey) for i in intermediate] 91 | print('I decrypted by cheating the oracle:', plaintexts) 92 | -------------------------------------------------------------------------------- /set2/ch13.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 2, challenge 13: ECB cut-and-paste 4 | 5 | import aes_lib 6 | from pkcs7 import Pkcs7 7 | from pkcs7 import StripPkcs7 8 | 9 | # Block size used by AES here. 10 | BLOCK_SIZE = 16 11 | 12 | def ParseCookie(cookie): 13 | params = {} 14 | print(cookie) 15 | 16 | for url_param in cookie.decode().split('&'): 17 | kv = url_param.split('=') 18 | params[kv[0]] = kv[1] 19 | 20 | return params 21 | 22 | def EncodeProfile(profile): 23 | # Ensures all profile info are always encoded in the same order. 24 | return 'email=%s&uid=%s&role=%s' % ( 25 | profile['email'], profile['uid'], profile['role']) 26 | 27 | def CreateProfile(email_address): 28 | # Don't allow encoding special characters in email addresses. 29 | if '&' in email_address or '=' in email_address: 30 | raise Exception('Invalid characters in profile.') 31 | 32 | profile = {} 33 | profile['email'] = email_address 34 | profile['uid'] = 10 35 | profile['role'] = 'user' 36 | 37 | return EncodeProfile(profile) 38 | 39 | def CreateEncryptedProfile(email_address, encrypter): 40 | profile = CreateProfile(email_address) 41 | return encrypter.aes_pad_and_encrypt(bytes(profile, 'ascii')) 42 | 43 | def DecryptRole(encrypted_profile, encrypter): 44 | pt = encrypter.aes_decrypt_and_depad(encrypted_profile) 45 | profile = ParseCookie(pt) 46 | return profile 47 | 48 | def StartPositionEqualBytes(s1, s2): 49 | assert len(s1) == len(s2) 50 | start_pos = -1 51 | size = 0 52 | found_equal_byte = False 53 | 54 | for i in range(0, len(s1)): 55 | if s1[i] == s2[i]: 56 | if not found_equal_byte: 57 | start_pos = i 58 | found_equal_byte = True 59 | size += 1 60 | 61 | if found_equal_byte and s1[i] != s2[i]: 62 | break 63 | 64 | return (start_pos, size) 65 | 66 | if __name__ == "__main__": 67 | enc = aes_lib.AESCipher() 68 | 69 | # The length of the email address is such that the encoded 70 | # profile string will be composed for 36 bytes: the last 4 bytes, 71 | # containing the word 'user', will lay at the beginning of a new 72 | # block (with padding). 73 | p1 = CreateEncryptedProfile('aname@bar.com', enc) 74 | 75 | # The second address is such that the second 16-byte block will 76 | # start with admin; the remaining bytes are filled with the Pkcs7 77 | # padding that we know our oracle uses internally. The rest of 78 | # the address is optional, but we include it in case our oracle 79 | # performs some basic validation of email addresses. 80 | address = 'XXXXXXXXXXadmin' 81 | address += chr(11) * 11 82 | address += '@bar.it' 83 | 84 | p2 = CreateEncryptedProfile(address, enc) 85 | # The second block contains the ciphertext we are looking for. 86 | encrypted_admin_block = p2[16:32] 87 | 88 | # We replace the last block of p1 with the encrypted "admin" 89 | # block. 90 | fake_admin = p1[:32] 91 | fake_admin += encrypted_admin_block 92 | 93 | # TADA! Same profile, but role is now admin. 94 | print(DecryptRole(fake_admin, enc)) 95 | -------------------------------------------------------------------------------- /set5/ch34.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import ch33 as dh 4 | import hashlib 5 | from Crypto.Cipher import AES 6 | import random 7 | import socket 8 | 9 | # Set 5, challenge 34: Implement a MITM key-fixing attack on Diffie-Hellman with parameter injection. 10 | 11 | def DHExchangeBadServer(clientsocket, mysocket): 12 | A = 0 13 | p = 0 14 | g = 0 15 | exchange = b'' 16 | 17 | while b'D' not in exchange: 18 | exchange += clientsocket.recv(100) 19 | 20 | exchange = exchange.decode() 21 | pieces = exchange.split('\n') 22 | 23 | # We do not care about A, but we need p and g. 24 | p = int(pieces[1]) 25 | g = int(pieces[2]) 26 | 27 | # Relay a message to the good server by replacing A with p. 28 | # This means that B will be modexp(p, b, p), which, no matter 29 | # what the value of b is, is equal to 0. 30 | relayedMessage = 'BEGIN\n%s\n%s\n%s\nEND' % (str(p), str(g), str(p)) 31 | mysocket.send(relayedMessage.encode()) 32 | 33 | # For the same reason, relay a message to the client where B 34 | # is replaced by p. Now A will be equal to 0 too. 35 | messageForClient = 'BEGIN\n%s\nEND' % str(p) 36 | clientsocket.send(messageForClient.encode()) 37 | return 0 38 | 39 | # This is the same as for the 'good' server, since we correctly 40 | # guessed the secret key. 41 | def VerifySecret(clientsocket, message, secret): 42 | # Converts the secret to a string and hashes it with SHA1. 43 | sha1 = hashlib.sha1() 44 | sha1.update(str(secret).encode()) 45 | privateKey = sha1.hexdigest() 46 | 47 | # Laziness! This should be appended to the ciphertext itself by the client, 48 | # but I am going to skip that and just pretend we exchanged that already. 49 | iv = b'\x00' * AES.block_size 50 | # Taking only 16 digits from a longer hash does not seem like a very secure 51 | # method, but it's what the challenge recommends for longer hashes. 52 | cipher = AES.new(key=privateKey[:16], mode=AES.MODE_CBC, IV=iv) 53 | plaintext = cipher.decrypt(message) 54 | print(plaintext) 55 | 56 | if __name__ == '__main__': 57 | serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 58 | # Bind the socket locally. 59 | serversocket.bind(('localhost', 10002)) 60 | serversocket.listen(5) 61 | 62 | # Dumb single-threaded server. 63 | while True: 64 | clientsocket, _ = serversocket.accept() 65 | print('- Accepted new connection.') 66 | 67 | # We open a new connection for the 'good' server every time 68 | # to simulate the client behaviour. 69 | mysocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 70 | mysocket.connect(('localhost', 10000)) 71 | 72 | # If you read the function you find out the secret can be 73 | # predicted without computation :) 74 | secret = DHExchangeBadServer(clientsocket, mysocket) 75 | print('The computed secret is', str(secret)) 76 | 77 | # Gets the verification message, decrypts it, and then 78 | # relays it to the server so nothing is suspected. 79 | verificationMessage = clientsocket.recv(48) 80 | VerifySecret(clientsocket, verificationMessage, secret) 81 | mysocket.send(verificationMessage) 82 | 83 | mysocket.close() 84 | -------------------------------------------------------------------------------- /set7/ch50.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from Crypto.Cipher import AES 3 | from Crypto.Util.strxor import strxor 4 | 5 | import codecs 6 | 7 | # Set 7, challenge 50: hashing with CBC-MAC. 8 | 9 | def padPKCS7(message, k=16): 10 | """Apply PKCS7 padding to a message, if required.""" 11 | if len(message) % k == 0: 12 | return message 13 | 14 | ch = k - (len(message) % k) 15 | return message + bytes([ch] * ch) 16 | 17 | def GetMac(message): 18 | """Returns the CBC-MAC of a message with the key and IV of this challenge.""" 19 | 20 | iv = b'\x00' * AES.block_size 21 | cipher = AES.new(b'YELLOW SUBMARINE', AES.MODE_CBC, iv) 22 | ct = cipher.encrypt(padPKCS7(message)) 23 | return ct[-AES.block_size:] 24 | 25 | def GetBeforeMac(message): 26 | """ 27 | Returns the block before the MAC (i.e. the second-to-last one) in the 28 | CBC encryption of a message. We assume the ciphertext has at least two 29 | blocks. 30 | """ 31 | 32 | iv = b'\x00' * AES.block_size 33 | cipher = AES.new(b'YELLOW SUBMARINE', AES.MODE_CBC, iv) 34 | ct = cipher.encrypt(padPKCS7(message)) 35 | return ct[-(AES.block_size * 2):-AES.block_size] 36 | 37 | if __name__ == '__main__': 38 | code = b"alert('MZA who was that?');\n" 39 | mac1 = GetMac(code) 40 | 41 | # How this works: we want to add a block at the end of codeToForge such that 42 | # the resulting MAC is the same as mac1 above. We end our code block with 43 | # a comment so that we can put anything we want afterwards. 44 | # 45 | # Given how CBC works, and the fact that we always encrypt with the same 46 | # algorithm, AES, and the same key, we need to append a block to codeToForge 47 | # such that 48 | # beforeMac1 XOR last-block-of-code = mac2 XOR block-to-insert 49 | # where beforeMac1 is the second-to-last block in the ciphertext of |code| 50 | # (the one before the MAC), last-block-of-code is the last block in the 51 | # plaintext |code|, mac2 is the MAC of |codeToForge| below, and block-to-insert 52 | # is what we are trying to achieve. With XOR arithmetics we get: 53 | # 54 | # block-to-insert = beforeMac1 XOR last-block-of-code XOR mac2. 55 | # 56 | # (NOTE: to be completely realistic, the new block we find should be 57 | # valid ASCII; we do not enforce that here, perhaps I'll do it for a 58 | # later improvement). 59 | 60 | codeToForge = b"alert('Ayo, the Wu is back!');//" 61 | mac2 = GetMac(codeToForge) 62 | 63 | pt1 = padPKCS7(code)[-AES.block_size:] 64 | piece = strxor(pt1, GetBeforeMac(code)) 65 | 66 | extension = strxor(mac2, piece) 67 | # Note that here we do not need to pad, if we needed to, it needs to be 68 | # applied here and not to the whole collision to repeat the same CBC 69 | # process. 70 | collision = padPKCS7(codeToForge) + extension 71 | 72 | macCollision = GetMac(collision) 73 | if macCollision == mac1: 74 | print('Collision successfully found:', collision) 75 | else: 76 | print('Collision not found, first MAC is %s but second MAC is %s' % 77 | (codecs.encode(mac1, 'hex'), codecs.encode(macCollision, 'hex'))) 78 | -------------------------------------------------------------------------------- /set5/srp_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import hashlib 4 | import random 5 | import socket 6 | import ch33 as dh 7 | 8 | SERVER_PORT = 10000 9 | # Server and client already agreed on this password. 10 | PASSWORD = b'mybeautifulpassword' 11 | 12 | # Secure Remote Password server used in a few challenges of this set. 13 | 14 | # Dumb utility: we read one byte at a time to avoid reading 15 | # two newlines in the same pass, since we may send pretty 16 | # small numbers. 17 | def ReadUntilNewline(clientsocket): 18 | data = b'' 19 | 20 | while b'\n' not in data: 21 | data += clientsocket.recv(1) 22 | 23 | return data[:-1] 24 | 25 | def SRPSetup(clientsocket): 26 | g = 2 27 | k = 3 28 | # Use the same large prime we used before. 29 | N = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e340' 30 | '4ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f40' 31 | '6b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8f' 32 | 'd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff', 16) 33 | 34 | # Random salt. 35 | salt = random.randint(1, 100000) 36 | saltBytes = str(salt).encode() 37 | 38 | # Hash salt|password and convert the result to an integer, x. We do 39 | # this by simply taking the decimal equivalent of the hash. 40 | sha = hashlib.sha256() 41 | sha.update(saltBytes) 42 | sha.update(PASSWORD) 43 | xhash = sha.hexdigest() 44 | 45 | x = int(xhash, 16) 46 | v = dh.modexp(g, x, N) 47 | 48 | # We don't actually make any use of the email in this exercise, 49 | # but in a real implementation we would associate the email with 50 | # the expected password, and/or do other verifications. 51 | email = ReadUntilNewline(clientsocket) 52 | A = ReadUntilNewline(clientsocket) 53 | 54 | b = random.randint(1, 10000) 55 | B = k*v + dh.modexp(g, b, N) 56 | 57 | clientsocket.send(saltBytes + b'\n') 58 | clientsocket.send(str(B).encode() + b'\n') 59 | 60 | sha = hashlib.sha256() 61 | sha.update(A) 62 | sha.update(str(B).encode()) 63 | u = int(sha.hexdigest(), 16) 64 | 65 | S = dh.modexp((int(A) * dh.modexp(v, u, N)), b, N) 66 | 67 | sha = hashlib.sha256() 68 | sha.update(str(S).encode()) 69 | K = sha.hexdigest() 70 | 71 | # Returns both K and saltBytes as byte strings. This helps later 72 | # since we hash them. 73 | return K.encode(), saltBytes 74 | 75 | if __name__ == '__main__': 76 | serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 77 | # Bind the socket locally. 78 | serversocket.bind(('localhost', SERVER_PORT)) 79 | serversocket.listen(5) 80 | 81 | # Dumb single-threaded server. 82 | while True: 83 | clientsocket, _ = serversocket.accept() 84 | print('- Accepted new connection.') 85 | 86 | K, salt = SRPSetup(clientsocket) 87 | 88 | sha = hashlib.sha256() 89 | sha.update(salt) 90 | sha.update(K) 91 | 92 | digest = sha.hexdigest().encode() 93 | password = clientsocket.recv(64) 94 | 95 | # Compare our digest with the one produced by the client. 96 | if password == digest: 97 | clientsocket.send(b'OK') 98 | else: 99 | clientsocket.send(b'NO') 100 | 101 | clientsocket.close() 102 | -------------------------------------------------------------------------------- /set6/rsa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # RSA implementation, taken from set 5. 4 | 5 | import random 6 | from utils import modexp 7 | from primes import getProbablePrime 8 | 9 | # Computes the GCD between two numbers using the extended Euclidean formulation. 10 | # This means we find solutions to this equation: 11 | # a*x + b*y = gcd(x, y) 12 | # and we return a tuple with the gcd, x and y in this order. 13 | # 14 | # We take two integers, a and b, which we assume positive. We compute a/b and store 15 | # the integer quotient and remainder. Then we do the same with the quotient and 16 | # remainder. This produces a strictly decreasing sequence of remainders, which 17 | # terminates at 0; the last nonzero remainder in the sequence is the GCD. 18 | # 19 | # This works because if a = bq + r, then gcd(a, b) = gcd(b, r). At the last step 20 | # we get gcd(r, r) = r. 21 | def egcd(a, b): 22 | lastremainder, remainder = abs(a), abs(b) 23 | x, lastx, y, lasty = 0, 1, 1, 0 24 | 25 | while remainder: 26 | # divmod was introduced in Python 3, and returns both the quotient 27 | # and remainder of an integer division, in a tuple. 28 | lastremainder, (quotient, remainder) = remainder, divmod(lastremainder, remainder) 29 | x, lastx = lastx - quotient*x, x 30 | y, lasty = lasty - quotient*y, y 31 | 32 | return lastremainder, lastx, lasty 33 | 34 | # Computes the modular multiplicative inverse of a, with modulo m. This is an 35 | # integer x such that ax = 1 (mod m), that is the inverse of a in the 36 | # ring of integers modulo m, called Zm. 37 | # Such an inverse exists only if a and m are coprime, so their gcd is 1. 38 | # 39 | # Let's see how to use the extended Euclidean algortihm to find the invmod.a 40 | # We find solutions to the Bezout's identity: 41 | # ax + by = gcd(a, b) 42 | # If ax = 1 (mod m), this implies that m is a divisor of ax-1. Therefore: 43 | # ax - 1 = qm 44 | # ax - qm = 1 45 | # This is the same equation solved by the extended Euclidean, but we know 46 | # the gcd already, since we say it must be 1. 47 | def invmod(a, m): 48 | g, x, y = egcd(a, m) 49 | if g != 1: 50 | raise ValueError('modular inverse does not exist') 51 | else: 52 | return x % m 53 | 54 | def FindSmallerCoprime(n): 55 | for e in range(2, n): 56 | gcd = egcd(n, e) 57 | if gcd[0] == 1: 58 | return e 59 | 60 | raise ValueError('Could not find coprime') 61 | 62 | def GenerateRSAPair(keysize): 63 | """Generates a RSA key pair given the size in bits.""" 64 | e = 3 # we know this is not a good choice, but it works here :D 65 | bitcount = (keysize + 1) // 2 + 1 66 | 67 | p = 7 68 | while (p - 1) % e == 0: 69 | p = getProbablePrime(bitcount) 70 | 71 | q = p 72 | while q == p or (q - 1) % e == 0: 73 | q = getProbablePrime(bitcount) 74 | 75 | n = p * q 76 | et = (p - 1) * (q - 1) 77 | d = invmod(e, et) 78 | return ((e, n), (d, n)) 79 | 80 | def Encrypt(rsaKey, num): 81 | e, n = rsaKey 82 | return modexp(num, e, n) 83 | 84 | def Decrypt(rsaKey, num): 85 | d, n = rsaKey 86 | return modexp(num, d, n) 87 | 88 | if __name__ == '__main__': 89 | publicKey, privateKey = GenerateRSAPair(1024) 90 | 91 | num = 42 92 | ciphertext = Encrypt(publicKey, num) 93 | 94 | if Decrypt(privateKey, ciphertext) == num: 95 | print('[**] Correct!') 96 | else: 97 | print('[!!] Your RSA pair is wrong.') 98 | -------------------------------------------------------------------------------- /set5/ch36_37_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import ch33 as dh 4 | import hashlib 5 | import random 6 | import socket 7 | import sys 8 | 9 | # Set 5, challenge 36-37. 10 | 11 | def ReadUntilNewline(clientsocket): 12 | data = b'' 13 | 14 | while b'\n' not in data: 15 | data += clientsocket.recv(1) 16 | 17 | return data[:-1] 18 | 19 | # If isClientGood == True, we do SRP for real (Challenge 36). 20 | # Otherwise, we break the exchange as per Challenge 37. 21 | def SRPSetup(sock, email, password, isClientGood): 22 | g = 2 23 | k = 3 24 | N = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e340' 25 | '4ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f40' 26 | '6b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8f' 27 | 'd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff', 16) 28 | 29 | sock.send(email.encode() + b'\n') 30 | 31 | a = random.randint(0, 10000) 32 | 33 | if isClientGood: 34 | A = dh.modexp(g, a, N) 35 | else: 36 | # Sending this means that S = 0 server-side, since 37 | # A is the base of the modexp computing S, so all the 38 | # other parameters get ignored. 39 | # The other tweaking proposed, A = N or A = kN, have the 40 | # exact same effect, since you compute S = (A ** x) % A 41 | # which is equal to 0 no matter what 'x' is. 42 | A = 0 43 | 44 | message = (str(A) + '\n').encode() 45 | sock.send(message) 46 | 47 | salt = ReadUntilNewline(sock) 48 | B = ReadUntilNewline(sock) 49 | 50 | sha = hashlib.sha256() 51 | sha.update(str(A).encode()) 52 | sha.update(B) 53 | u = int(sha.hexdigest(), 16) 54 | 55 | sha = hashlib.sha256() 56 | sha.update(salt) 57 | sha.update(password.encode()) 58 | x = int(sha.hexdigest(), 16) 59 | 60 | if isClientGood: 61 | exp = (a + u * x) 62 | base = (int(B) - k * dh.modexp(g, x, N)) 63 | S = dh.modexp(base, exp, N) 64 | else: 65 | # If we have been bad, we know the server has computed S = 0, 66 | # so we do the same on our side. We could also avoid computing 67 | # a bunch of other parameters before (namely, x and u). 68 | # At this point the secret is independent of which password 69 | # we send to the server, so we can send an empty one and still 70 | # be accepted as good users. 71 | S = 0 72 | 73 | sha = hashlib.sha256() 74 | sha.update(str(S).encode()) 75 | K = sha.hexdigest() 76 | 77 | return K, salt 78 | 79 | if __name__ == '__main__': 80 | isGoodMode = True 81 | 82 | if len(sys.argv) > 1: 83 | if sys.argv[1] == 'good': 84 | isGoodMode = True 85 | elif sys.argv[1] == 'bad': 86 | isGoodMode = False 87 | 88 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 89 | sock.connect(('localhost', 10000)) 90 | 91 | email = input('[**] Enter email address: ') 92 | password = input('[**] Enter password: ') 93 | K, salt = SRPSetup(sock, email, password, isGoodMode) 94 | 95 | sha = hashlib.sha256() 96 | sha.update(salt) 97 | sha.update(K.encode()) 98 | digest = sha.hexdigest() 99 | 100 | sock.send(digest.encode()) 101 | reply = sock.recv(2) 102 | 103 | if reply == b'OK': 104 | print('[**] Password accepted. You have full control of the nuclear reactor!') 105 | elif reply == b'NO': 106 | print('[!!] Incorrect password.') 107 | else: 108 | print('Unrecognized reply by the server:', reply) 109 | 110 | sock.close() 111 | -------------------------------------------------------------------------------- /set6/ch44.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import hashlib 4 | import dsa 5 | import rsa 6 | 7 | # Set 6, challenge 44: DSA nonce recovery from repeated nonce. 8 | 9 | # Parses the messages and other data into a list of dictionaries. 10 | def ReadMessages(filename): 11 | content = None 12 | messages = [] 13 | 14 | with open(filename, 'r') as f: 15 | content = f.readlines() 16 | 17 | message = {} 18 | num = 0 19 | 20 | for line in content: 21 | if line.startswith('msg: '): 22 | if message: 23 | messages.append(message) 24 | message = {} 25 | 26 | message['message'] = line[5:-1] 27 | elif line.startswith('s: '): 28 | message['s'] = int(line[3:-1]) 29 | elif line.startswith('r: '): 30 | message['r'] = int(line[3:-1]) 31 | elif line.startswith('m: '): 32 | message['m'] = line[3:-1] 33 | 34 | return messages 35 | 36 | def BreakDSA(m1, m2, publicKey, dsa_params): 37 | # m1 and m2 are interchangeable, but if you compute m1['s'] - m2['s'] 38 | # (and the rest accordingly), you get a negative K, which is not valid 39 | # according to the spec, and our implementation hangs trying to sign 40 | # a message. 41 | k = rsa.invmod((m2['s'] - m1['s']), dsa_params['Q']) 42 | k *= (int(m2['m'], 16) - int(m1['m'], 16)) 43 | 44 | top = (m1['s'] * k) - int(m1['m'], 16) 45 | bottom = rsa.invmod(m1['r'], dsa_params['Q']) 46 | privateKey = (top * bottom) % dsa_params['Q'] 47 | 48 | # Derive the public key from the private key and compare it 49 | # with the one we know. 50 | testPub = dsa.modexp(dsa_params['G'], privateKey, dsa_params['P']) 51 | if testPub == publicKey: 52 | print('[**] Success!') 53 | print('K:', k) 54 | print('X:', privateKey) 55 | 56 | # Sign one of the two messages with the private key and compare 57 | # the signature with the one we got from the file. 58 | sig = dsa.dsa_sign(dsa_params['Q'], dsa_params['P'], dsa_params['G'], privateKey, 59 | int('0x' + m1['m'], 16), k=k) 60 | if sig == (m1['r'], m1['s']): 61 | print('[**] Broken private key passes validation') 62 | return True 63 | 64 | return False 65 | 66 | if __name__ == '__main__': 67 | messages = ReadMessages('data/44.txt') 68 | 69 | publicKey = int('0x2d026f4bf30195ede3a088da85e398ef869611d0f68f0713d51c9c1a3a26c95105d915e2d8cdf26d056b86b8a7b85519b1c23cc3ecdc6062650462e3063bd179c2a6581519f674a61f1d89a1fff27171ebc1b93d4dc57bceb7ae2430f98a6a4d83d8279ee65d71c1203d2c96d65ebbf7cce9d32971c3de5084cce04a2e147821', 16) 70 | 71 | dsa_params = { 72 | 'Q': int("0xf4f47f05794b256174bba6e9b396a7707e563c5b", 16), 73 | 'P': int("0x800000000000000089e1855218a0e7dac38136ffafa72eda7859f2171e25e65eac698c1702578b07dc2a1076da241c76c62d374d8389ea5aeffd3226a0530cc565f3bf6b50929139ebeac04f48c3c84afb796d61e5a4f9a8fda812ab59494232c7d2b4deb50aa18ee9e132bfa85ac4374d7f9091abc3d015efc871a584471bb1", 16), 74 | 'G': int("5958c9d3898b224b12672c0b98e06c60df923cb8bc999d119458fef538b8fa4046c8db53039db620c094c9fa077ef389b5322a559946a71903f990f1f7e0e025e2d7f7cf494aff1a0470f5b64c36b625a097f1651fe775323556fe00b3608c887892878480e99041be601a62166ca6894bdd41a7054ec89f756ba9fc95302291", 16), 75 | } 76 | 77 | success = False 78 | 79 | # We simply try our break algorithm for every possible pair 80 | # of messages, since there are not that many. 81 | for i in range(0, len(messages)): 82 | for j in range(i + 1, len(messages)): 83 | m1 = messages[i] 84 | m2 = messages[j] 85 | 86 | if BreakDSA(m1, m2, publicKey, dsa_params): 87 | success = True 88 | break 89 | if success: 90 | break 91 | 92 | if not success: 93 | print('[!!] Failed breaking DSA') 94 | -------------------------------------------------------------------------------- /set5/simplified_srp_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import hashlib 4 | import random 5 | import socket 6 | import ch33 as dh 7 | 8 | # Set 5, challenge 38: Offline dictionary attack on simplified SRP. Server. 9 | # 10 | # This is very similar to the proper SRP server found in srp_server.py; given 11 | # that there are a few small changes I decided to make a separate application, 12 | # so it's easier to explain on its own. 13 | 14 | SERVER_PORT = 10001 15 | # Server and client already agreed on this password. 16 | PASSWORD = b'valedictorian' 17 | 18 | # "Simplified" SRP server for challenge 38 specifically. 19 | 20 | # Dumb utility: we read one byte at a time to avoid reading 21 | # two newlines in the same pass, since we send pretty small messages. 22 | def ReadUntilNewline(clientsocket): 23 | data = b'' 24 | 25 | while b'\n' not in data: 26 | data += clientsocket.recv(1) 27 | 28 | return data[:-1] 29 | 30 | def SRPSetup(clientsocket): 31 | g = 2 32 | k = 3 33 | N = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e340' 34 | '4ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f40' 35 | '6b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8f' 36 | 'd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff', 16) 37 | 38 | # Random salt. 39 | salt = str(random.randint(1, 100000)).encode() 40 | 41 | # Hash salt|password and convert the result to an integer, x. We do 42 | # this by simply taking the decimal equivalent of the hash. 43 | sha = hashlib.sha256() 44 | sha.update(salt) 45 | sha.update(PASSWORD) 46 | xhash = sha.hexdigest() 47 | 48 | x = int(xhash, 16) 49 | v = dh.modexp(g, x, N) 50 | 51 | # We don't actually make any use of the email in this exercise, 52 | # but in a real implementation we would associate the email with 53 | # the expected password, and/or do other verifications. 54 | email = ReadUntilNewline(clientsocket) 55 | A = ReadUntilNewline(clientsocket) 56 | 57 | b = random.randint(1, 10000) 58 | B = dh.modexp(g, b, N) 59 | 60 | clientsocket.send(salt + b'\n') 61 | clientsocket.send(str(B).encode() + b'\n') 62 | 63 | u = random.getrandbits(128) 64 | clientsocket.send(str(u).encode() + b'\n') 65 | 66 | S = dh.modexp((int(A) * dh.modexp(v, u, N)), b, N) 67 | 68 | sha = hashlib.sha256() 69 | sha.update(str(S).encode()) 70 | K = sha.hexdigest() 71 | 72 | # Returns both K and salt as byte strings. This is used for verification. 73 | return K.encode(), salt 74 | 75 | if __name__ == '__main__': 76 | serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 77 | # Bind the socket locally. 78 | serversocket.bind(('localhost', SERVER_PORT)) 79 | serversocket.listen(5) 80 | 81 | # Dumb single-threaded server. 82 | while True: 83 | clientsocket, _ = serversocket.accept() 84 | print('- Accepted new connection.') 85 | 86 | K, salt = SRPSetup(clientsocket) 87 | 88 | sha = hashlib.sha256() 89 | sha.update(salt) 90 | sha.update(K) 91 | 92 | digest = sha.hexdigest().encode() 93 | password = clientsocket.recv(64) 94 | 95 | # Compare our digest with the one produced by the client. 96 | if password == digest: 97 | clientsocket.send(b'OK') 98 | else: 99 | clientsocket.send(b'NO') 100 | 101 | clientsocket.close() 102 | -------------------------------------------------------------------------------- /set4/ch27.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from Crypto.Cipher import AES 4 | from Crypto import Random 5 | from Crypto.Util.strxor import strxor 6 | 7 | class Server(object): 8 | def __init__(self): 9 | self._key = Random.new().read(AES.block_size) 10 | self._iv = self._key 11 | print('[**] Random key is', self._key) 12 | 13 | def encrypt(self, plaintext): 14 | """CBC encryption.""" 15 | cipher = AES.new(key=self._key, mode=AES.MODE_ECB) 16 | 17 | # The full URL is not necessary for this setup, so I am just encrypting 18 | # the plaintext as it is. I don't even need to support padding. 19 | prev_ct = self._iv 20 | block_index = 0 21 | ciphertext = b'' 22 | 23 | # The loop simulates encryption through AES in CBC mode. 24 | while block_index < len(plaintext): 25 | block = plaintext[block_index : block_index + AES.block_size] 26 | final_block = strxor(block, prev_ct) 27 | 28 | cipher_block = cipher.encrypt(final_block) 29 | prev_ct = cipher_block 30 | ciphertext += cipher_block 31 | 32 | block_index += AES.block_size 33 | 34 | return ciphertext 35 | 36 | def decrypt(self, ciphertext): 37 | """CBC decryption.""" 38 | cipher = AES.new(key=self._key, mode=AES.MODE_ECB) 39 | 40 | prev_ct = self._iv 41 | block_index = 0 42 | plaintext = b'' 43 | 44 | # The loop simulates decryption through AES in CBC mode. 45 | while block_index < len(ciphertext): 46 | block = ciphertext[block_index : block_index + AES.block_size] 47 | 48 | prep_plaintext = cipher.decrypt(block) 49 | plaintext += strxor(prev_ct, prep_plaintext) 50 | prev_ct = block 51 | 52 | block_index += AES.block_size 53 | 54 | # Here we should check if this is all readable ASCII, and raise an 55 | # exception if it's not. However that part is not really necessary, 56 | # and converting from Exception object to byte string (instead of a 57 | # usual string) does not look great so let's be lazy :) 58 | return plaintext 59 | 60 | 61 | if __name__ == "__main__": 62 | server = Server() 63 | 64 | # Minimal massage required here to fit exactly 3 blocks :-) 65 | message = b'This message should consist of exactly 3 blocks.' 66 | ciphertext = server.encrypt(message) 67 | 68 | # Why does this work? Let's call K the key (and IV), PTi the ith block 69 | # of the plaintext and CTi the ith block of the ciphertext. 70 | # 71 | # When I decrypt the modified ciphertext, what I get is: 72 | # PT1 = IV XOR AES(K, CT1) = K XOR AES(K, CT1) 73 | # PT3 = CT2 XOR AES(K, CT1) = 0 XOR AES(K, CT1) 74 | # where 0 is of course a block filled with 0s. 75 | # 76 | # Now PT1 XOR PT3 = K XOR 0 XOR AES(K, CT1) XOR AES(K, CT1) = K XOR 0 = K. 77 | # XOR is commutative, plus XORing something with itself yields 0, while 78 | # scoring something with 0 leaves it unchanged. And voila we got the key. 79 | # 80 | # Note that two conditions are required for this property to hold: the IV 81 | # is equal to the key, and the server returns the plaintext even though it 82 | # won't be valid ASCII. 83 | 84 | newMessage = ciphertext[:AES.block_size] 85 | newMessage += b'\x00' * AES.block_size 86 | newMessage += ciphertext[:AES.block_size] 87 | 88 | plaintext = server.decrypt(newMessage) 89 | block1 = plaintext[:AES.block_size] 90 | block3 = plaintext[AES.block_size*2 : AES.block_size*3] 91 | 92 | recoveredKey = strxor(block1, block3) 93 | print('[**] The cracked key is', recoveredKey) 94 | -------------------------------------------------------------------------------- /set5/ch39.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import random 4 | from primes import getProbablePrime 5 | 6 | # Set 5, challenge 39: Implement RSA. 7 | 8 | # Computes the GCD between two numbers using the extended Euclidean formulation. 9 | # This means we find solutions to this equation: 10 | # a*x + b*y = gcd(x, y) 11 | # and we return a tuple with the gcd, x and y in this order. 12 | # 13 | # We take two integers, a and b, which we assume positive. We compute a/b and store 14 | # the integer quotient and remainder. Then we do the same with the quotient and 15 | # remainder. This produces a strictly decreasing sequence of remainders, which 16 | # terminates at 0; the last nonzero remainder in the sequence is the GCD. 17 | # 18 | # This works because if a = bq + r, then gcd(a, b) = gcd(b, r). At the last step 19 | # we get gcd(r, r) = r. 20 | def egcd(a, b): 21 | lastremainder, remainder = abs(a), abs(b) 22 | x, lastx, y, lasty = 0, 1, 1, 0 23 | 24 | while remainder: 25 | # divmod was introduced in Python 3, and returns both the quotient 26 | # and remainder of an integer division, in a tuple. 27 | lastremainder, (quotient, remainder) = remainder, divmod(lastremainder, remainder) 28 | x, lastx = lastx - quotient*x, x 29 | y, lasty = lasty - quotient*y, y 30 | 31 | return lastremainder, lastx, lasty 32 | 33 | # Computes the modular multiplicative inverse of a, with modulo m. This is an 34 | # integer x such that ax = 1 (mod m), that is the inverse of a in the 35 | # ring of integers modulo m, called Zm. 36 | # Such an inverse exists only if a and m are coprime, so their gcd is 1. 37 | # 38 | # Let's see how to use the extended Euclidean algortihm to find the invmod.a 39 | # We find solutions to the Bezout's identity: 40 | # ax + by = gcd(a, b) 41 | # If ax = 1 (mod m), this implies that m is a divisor of ax-1. Therefore: 42 | # ax - 1 = qm 43 | # ax - qm = 1 44 | # This is the same equation solved by the extended Euclidean, but we know 45 | # the gcd already, since we say it must be 1. 46 | def invmod(a, m): 47 | g, x, y = egcd(a, m) 48 | if g != 1: 49 | raise ValueError('modular inverse does not exist') 50 | else: 51 | return x % m 52 | 53 | def FindSmallerCoprime(n): 54 | for e in range(2, n): 55 | gcd = egcd(n, e) 56 | if gcd[0] == 1: 57 | return e 58 | 59 | raise ValueError('Could not find coprime') 60 | 61 | def GenerateRSAPair(p, q): 62 | n = p * q 63 | totient = n - (p + q - 1) 64 | e = FindSmallerCoprime(totient) 65 | d = invmod(e, totient) 66 | 67 | return (e, n), (d, n) 68 | 69 | # For the E=3 broadcast attack, we follow a different path: we fix 70 | # e=3 and then compute p and q as two large primes such that the 71 | # totient is coprime with e; this means that both p-1 and q-1 need 72 | # to be coprime with e; if this is not satisfied the invmod won't 73 | # exist. 74 | def GenerateRSAPairBroadcast(p, q): 75 | e = 3 76 | p = 7 77 | while (p - 1) % e == 0: 78 | # Large primes for the win! 79 | p = getProbablePrime(256) 80 | 81 | q = p 82 | while q == p or (q - 1) % e == 0: 83 | q = getProbablePrime(256) 84 | 85 | n = p * q 86 | totient = (p - 1) * (q - 1) 87 | d = invmod(e, totient) 88 | 89 | return (e, n), (d, n) 90 | 91 | def Encrypt(rsaKey, num): 92 | e, n = rsaKey 93 | return (num ** e) % n 94 | 95 | def Decrypt(rsaKey, num): 96 | d, n = rsaKey 97 | return (num ** d) % n 98 | 99 | if __name__ == '__main__': 100 | publicKey, privateKey = GenerateRSAPair(61, 53) 101 | 102 | num = 42 103 | ciphertext = Encrypt(publicKey, num) 104 | 105 | if Decrypt(privateKey, ciphertext) == num: 106 | print('[**] Correct!') 107 | else: 108 | print('[!!] Your RSA pair is wrong.') 109 | -------------------------------------------------------------------------------- /set2/ch16.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 2, Challenge 16: CBC bitflipping attacks. 4 | 5 | import aes_lib 6 | from Crypto.Cipher import AES 7 | 8 | def _Padding(string, blockSize): 9 | """Returns the amount of padding bytes we need to add.""" 10 | if len(string) % blockSize == 0: 11 | return 0 12 | 13 | return blockSize - (len(string) % blockSize) 14 | 15 | def StripPkcs7(string, blockSize): 16 | paddingFound = False 17 | 18 | if len(string) % blockSize != 0: 19 | raise Exception('String has not been padded properly.') 20 | 21 | index = len(string) - 1 22 | 23 | lastCh = string[index] 24 | if lastCh > blockSize: 25 | return string 26 | 27 | num_padding = 1 28 | while True: 29 | index -= 1 30 | if string[index] != lastCh: 31 | break 32 | 33 | num_padding += 1 34 | 35 | if num_padding != lastCh: 36 | raise Exception('Wrong padding applied.') 37 | 38 | return string[:index+1] 39 | 40 | def Pkcs7(string, blockSize): 41 | numPadding = _Padding(string, blockSize) 42 | paddedString = string 43 | 44 | for i in range(0, numPadding): 45 | paddedString += bytes(bytearray([numPadding])) 46 | 47 | return paddedString 48 | 49 | def BitflippingEncryption(aes, inputBytes): 50 | """Encloses the input byte string in a cookie, and encrypts 51 | with AES in CBC mode.""" 52 | 53 | # Avoids injection of admin=true by disallowing the '=' character. 54 | if ord('=') in inputBytes: 55 | raise Exception('Invalid characters in input text') 56 | 57 | plaintext = b'comment1=cooking%20MCs;userdata=' 58 | plaintext += inputBytes 59 | plaintext += b';comment2=%20like%20a%20pound%20of%20bacon' 60 | plaintext = Pkcs7(plaintext, 16) # padding 61 | 62 | return aes.SimulateCBCEncryption(plaintext) 63 | 64 | def BitflippingDecryption(aes, ciphertext): 65 | plaintext = aes.SimulateCBCDecryption(ciphertext) 66 | return StripPkcs7(plaintext, 16) 67 | 68 | def BitflippingDecryptAndVerify(aes, ciphertext): 69 | pt = BitflippingDecryption(aes, ciphertext) 70 | return (b'admin=true' in pt) 71 | 72 | if __name__ == "__main__": 73 | aes = aes_lib.AESCipher(mode=AES.MODE_ECB) 74 | 75 | # The idea behind this attack: my userdata cannot contain '=' 76 | # so I cannot simply inject an admin=true in it. I insert a 77 | # 'adminXtrue' (where 'X' is any other allowed character), then 78 | # exploit CBC to replace 'X' with '='. 79 | 80 | # It does not really matter what it goes in the userdata beside 81 | # adminXtrue, as long as you keep track of the length of the 82 | # text you add before it. 83 | pt = b'I am the queen of crypto' # len here is 24 84 | pt += b'adminXtrue' 85 | pt += b'Witness my power!' 86 | 87 | ct = BitflippingEncryption(aes, pt) 88 | 89 | # This is the index of the ciphertext byte that is in the previous 90 | # block of my X above, at the same offset from the beginning of 91 | # the block. So if my X is the third byte in its block, this will 92 | # be the index of the third byte of the previous block. The cookie 93 | # already guarantees me to have blocks before my userdata, otherwise 94 | # you could just use the data before adminXtrue. 95 | offset = len('comment1=cooking%20MCs;userdata=') + 24 + 5 - 16 96 | 97 | # To see why this correction works, a good explanation can be found 98 | # at http://resources.infosecinstitute.com/cbc-byte-flipping-attack-101-approach/ 99 | flipped = ct[offset] ^ ord('X') ^ ord('=') 100 | 101 | # Replaces only that byte in the ciphertext. 102 | flippedCipherText = ct[:offset] 103 | flippedCipherText += bytes([flipped]) 104 | flippedCipherText += ct[offset+1:] 105 | 106 | is_admin = BitflippingDecryptAndVerify(aes, flippedCipherText) 107 | 108 | if is_admin: 109 | print('Admin found in cookie.') 110 | else: 111 | print('Admin not found in cookie.') 112 | -------------------------------------------------------------------------------- /set7/md4.py: -------------------------------------------------------------------------------- 1 | # Thanks to http://www.acooke.org/cute/PurePython0.html for 2 | # this part. 3 | 4 | #!/usr/bin/python3 5 | 6 | import binascii 7 | from array import array 8 | from struct import pack, unpack 9 | 10 | def _pad(msg, fakeLen): 11 | n = len(msg) if fakeLen is None else fakeLen 12 | bit_len = n * 8 13 | index = (bit_len >> 3) & 0x3f 14 | 15 | pad_len = 120 - index 16 | if index < 56: 17 | pad_len = 56 - index 18 | 19 | padding = b'\x80' + b'\x00'*63 20 | padded_msg = msg + padding[:pad_len] + pack('> (32 - b))) & 0xffffffff 25 | 26 | def _f(x, y, z): return x & y | ~x & z 27 | def _g(x, y, z): return x & y | x & z | y & z 28 | def _h(x, y, z): return x ^ y ^ z 29 | 30 | def _f1(a, b, c, d, k, s, X): return _left_rotate(a + _f(b, c, d) + X[k], s) 31 | def _f2(a, b, c, d, k, s, X): return _left_rotate(a + _g(b, c, d) + X[k] + 0x5a827999, s) 32 | def _f3(a, b, c, d, k, s, X): return _left_rotate(a + _h(b, c, d) + X[k] + 0x6ed9eba1, s) 33 | 34 | # Implementation of MD4 hashing. 35 | class MD4: 36 | def __init__(self, internalState=None): 37 | if internalState is None: 38 | self.A = 0x67452301 39 | self.B = 0xefcdab89 40 | self.C = 0x98badcfe 41 | self.D = 0x10325476 42 | else: 43 | self.A = internalState[0] 44 | self.B = internalState[1] 45 | self.C = internalState[2] 46 | self.D = internalState[3] 47 | 48 | def update(self, message_string, fakeLen=None): 49 | msg_bytes = _pad(message_string, fakeLen) 50 | for i in range(0, len(msg_bytes), 64): 51 | self._compress(msg_bytes[i:i+64]) 52 | 53 | def _compress(self, block): 54 | a, b, c, d = self.A, self.B, self.C, self.D 55 | 56 | x = [] 57 | for i in range(0, 64, 4): 58 | x.append(unpack('> (32 - bits))) & 0xffffffff 18 | 19 | def _processChunk(self, chunk): 20 | """Processes a single chunk of 64 bytes, updating the 21 | internal state accordingly.""" 22 | assert len(chunk) == 64 23 | 24 | words = [0] * 80 25 | # Break chunk into sixteen 4-byte big-endian words w[i] 26 | for i in range(16): 27 | words[i] = struct.unpack(b'>I', chunk[i*4:i*4 + 4])[0] 28 | 29 | # Extend the sixteen 4-byte words into eighty 4-byte words: 30 | for i in range(16, 80): 31 | words[i] = self._leftRotate( 32 | words[i-3] ^ words[i-8] ^ words[i-14] ^ words[i-16], 1) 33 | 34 | a = self._h[0] 35 | b = self._h[1] 36 | c = self._h[2] 37 | d = self._h[3] 38 | e = self._h[4] 39 | 40 | for i in range(0, 80): 41 | if i >= 0 and i <= 19: 42 | f = (b & c) | ((~b) & d) 43 | k = 0x5A827999 44 | elif i >= 20 and i <= 39: 45 | f = b ^ c ^ d 46 | k = 0x6ED9EBA1 47 | elif i >= 40 and i <= 59: 48 | f = (b & c) | (b & d) | (c & d) 49 | k = 0x8F1BBCDC 50 | else: 51 | f = b ^ c ^ d 52 | k = 0xCA62C1D6 53 | 54 | tmp = ((self._leftRotate(a, 5) + f + e + k + words[i]) 55 | & 0xffffffff) 56 | e = d 57 | d = c 58 | c = self._leftRotate(b, 30) 59 | b = a 60 | a = tmp 61 | 62 | self._h[0] = (self._h[0] + a) & 0xffffffff 63 | self._h[1] = (self._h[1] + b) & 0xffffffff 64 | self._h[2] = (self._h[2] + c) & 0xffffffff 65 | self._h[3] = (self._h[3] + d) & 0xffffffff 66 | self._h[4] = (self._h[4] + e) & 0xffffffff 67 | 68 | # Computes the SHA1 digest of inputBytes. For length-extension attacks, 69 | # we may want to inject the message length. Otherwise, we use the legit 70 | # length of the input. 71 | def digest(self, inputBytes, fakeLen=None): 72 | """Computes the digest of a message, stored in inputBytes.""" 73 | # Initial state. 74 | if self._h is None: 75 | self._h = [ 76 | 0x67452301, 77 | 0xEFCDAB89, 78 | 0x98BADCFE, 79 | 0x10325476, 80 | 0xC3D2E1F0, 81 | ] 82 | 83 | message = inputBytes 84 | byteLength = len(message) if fakeLen is None else fakeLen 85 | 86 | # Appends the bit '1' to the message. 87 | message += b'\x80' 88 | # Appends the byte '0' k times; k is such that the resulting 89 | # message length in bits is congruent to 64. 90 | message += b'\x00' * ((56 - (byteLength + 1) % 64) % 64) 91 | # Appends the message length in bits as a 64-bit big-endian 92 | # integer (Q is unsigned long long for the struct module). 93 | message += struct.pack(b'>Q', byteLength * 8) 94 | 95 | # Processes each 64-bytes chunk separately. 96 | for i in range(0, int(len(message) / 64)): 97 | chunk = message[i * 64 : (i+1) * 64] 98 | self._processChunk(chunk) 99 | 100 | # Final digest. 101 | digest = (self._h[0] << 128) | (self._h[1] << 96) | (self._h[2] << 64) | (self._h[3] << 32) | self._h[4] 102 | return '%x' % digest # convert to hex string as most digests. 103 | 104 | 105 | def Sha1Sign(message): 106 | """Signs the message with SHA1 and a secret prefix. 107 | Both the input and output are byte strings.""" 108 | mac = b'YELLOW SUBMARINE' + message 109 | h = Sha1Hash() 110 | return h.digest(mac) 111 | 112 | def Sha1SignWithLib(message): 113 | sha = hashlib.sha1() 114 | sha.update(b'YELLOW SUBMARINE') 115 | sha.update(message) 116 | return sha.hexdigest() 117 | 118 | if __name__ == '__main__': 119 | text = b'some random text here' 120 | 121 | # Simple comparison to show that we produce the same result 122 | # as the standard library implementation. 123 | mac1 = Sha1Sign(text) 124 | mac2 = Sha1SignWithLib(text) 125 | 126 | print(mac1) 127 | print(mac2) 128 | -------------------------------------------------------------------------------- /set5/ch35.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import ch33 as dh 4 | import hashlib 5 | from Crypto.Cipher import AES 6 | import random 7 | import socket 8 | 9 | # Set 4, challenge 35: Implement DH with negotiated groups, and break with malicious "g" parameters. 10 | 11 | # This contains all the three attacks for Challenge 35. 12 | # I should have a command line parameter for deciding 13 | # which one to apply. 14 | 15 | # Common code factored out. 16 | def ReadOtherParameters(clientsocket): 17 | A = 0 18 | p = 0 19 | exchange = b'' 20 | 21 | while b'D' not in exchange: 22 | exchange += clientsocket.recv(100) 23 | 24 | exchange = exchange.decode() 25 | pieces = exchange.split('\n') 26 | 27 | p = int(pieces[1]) 28 | A = int(pieces[3]) 29 | return A, p 30 | 31 | # 1st attack: set g = 1. 32 | # The server computes B = 1^b mod p = 1 no matter the rest of the parameters, 33 | # and we pass B = 1 to the client. A stays correct. 34 | # At both sides, the final secret is 1. 35 | def DHExchangeBadServer1(clientsocket, mysocket): 36 | A, p = ReadOtherParameters(clientsocket) 37 | my_g = 1 38 | 39 | relayedMessage = 'BEGIN\n%s\n%s\n%s\nEND' % (str(p), str(my_g), str(A)) 40 | mysocket.send(relayedMessage.encode()) 41 | 42 | messageForClient = 'BEGIN\n1\nEND' 43 | clientsocket.send(messageForClient.encode()) 44 | return 1 45 | 46 | # 2nd attack: set g = p. 47 | # In this case we get that A = B = 0. However since the client computes 48 | # A before we can "inject" our bad g, we need to relay A = 0 to the 49 | # server, and discard the A passed by the client. 50 | # The final secret is also 0 at this point. 51 | def DHExchangeBadServer2(clientsocket, mysocket): 52 | _, p = ReadOtherParameters(clientsocket) 53 | my_g = p 54 | 55 | relayedMessage = 'BEGIN\n%s\n%s\n%s\nEND' % (str(p), str(my_g), str(0)) 56 | mysocket.send(relayedMessage.encode()) 57 | 58 | messageForClient = 'BEGIN\n0\nEND' 59 | clientsocket.send(messageForClient.encode()) 60 | return 0 61 | 62 | # 3rd attack: set g = p - 1. 63 | # THis is similar to the 1st attack because the final secret is 1, but 64 | # again to achieve that we need to inject our own A instead of the 65 | # one computed by the client. 66 | def DHExchangeBadServer3(clientsocket, mysocket): 67 | _, p = ReadOtherParameters(clientsocket) 68 | my_g = p - 1 69 | 70 | relayedMessage = 'BEGIN\n%s\n%s\n%s\nEND' % (str(p), str(my_g), str(1)) 71 | mysocket.send(relayedMessage.encode()) 72 | 73 | messageForClient = 'BEGIN\n1\nEND' 74 | clientsocket.send(messageForClient.encode()) 75 | return 1 76 | 77 | # This is the same as for the 'good' server, since we correctly 78 | # guessed the secret key. 79 | def VerifySecret(clientsocket, message, secret): 80 | # Converts the secret to a string and hashes it with SHA1. 81 | sha1 = hashlib.sha1() 82 | sha1.update(str(secret).encode()) 83 | privateKey = sha1.hexdigest() 84 | 85 | # Laziness! This should be appended to the ciphertext itself by the client, 86 | # but I am going to skip that and just pretend we exchanged that already. 87 | iv = b'\x00' * AES.block_size 88 | # Taking only 16 digits from a longer hash does not seem like a very secure 89 | # method, but it's what the challenge recommends for longer hashes. 90 | cipher = AES.new(key=privateKey[:16], mode=AES.MODE_CBC, IV=iv) 91 | plaintext = cipher.decrypt(message) 92 | print(plaintext) 93 | 94 | if __name__ == '__main__': 95 | serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 96 | # Bind the socket locally. 97 | serversocket.bind(('localhost', 10002)) 98 | serversocket.listen(5) 99 | 100 | # Dumb single-threaded server. 101 | while True: 102 | clientsocket, _ = serversocket.accept() 103 | print('- Accepted new connection.') 104 | 105 | # We open a new connection for the 'good' server every time 106 | # to simulate the client behaviour. 107 | mysocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 108 | mysocket.connect(('localhost', 10000)) 109 | 110 | # If you read the function you find out the secret can be 111 | # predicted without computation :) 112 | secret = DHExchangeBadServer3(clientsocket, mysocket) 113 | print('The computed secret is', str(secret)) 114 | 115 | # Gets the verification message, decrypts it, and then 116 | # relays it to the server so nothing is suspected. 117 | verificationMessage = clientsocket.recv(48) 118 | VerifySecret(clientsocket, verificationMessage, secret) 119 | mysocket.send(verificationMessage) 120 | 121 | mysocket.close() 122 | -------------------------------------------------------------------------------- /set4/md4_mac.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import binascii 4 | from array import array 5 | from struct import pack, unpack 6 | 7 | def _pad(msg, fakeLen): 8 | n = len(msg) if fakeLen is None else fakeLen 9 | bit_len = n * 8 10 | index = (bit_len >> 3) & 0x3f 11 | 12 | pad_len = 120 - index 13 | if index < 56: 14 | pad_len = 56 - index 15 | 16 | padding = b'\x80' + b'\x00'*63 17 | padded_msg = msg + padding[:pad_len] + pack('> (32 - b))) & 0xffffffff 22 | 23 | def _f(x, y, z): return x & y | ~x & z 24 | def _g(x, y, z): return x & y | x & z | y & z 25 | def _h(x, y, z): return x ^ y ^ z 26 | 27 | def _f1(a, b, c, d, k, s, X): return _left_rotate(a + _f(b, c, d) + X[k], s) 28 | def _f2(a, b, c, d, k, s, X): return _left_rotate(a + _g(b, c, d) + X[k] + 0x5a827999, s) 29 | def _f3(a, b, c, d, k, s, X): return _left_rotate(a + _h(b, c, d) + X[k] + 0x6ed9eba1, s) 30 | 31 | # Implementation of MD4 hashing. 32 | class MD4: 33 | def __init__(self, internalState=None): 34 | if internalState is None: 35 | self.A = 0x67452301 36 | self.B = 0xefcdab89 37 | self.C = 0x98badcfe 38 | self.D = 0x10325476 39 | else: 40 | self.A = internalState[0] 41 | self.B = internalState[1] 42 | self.C = internalState[2] 43 | self.D = internalState[3] 44 | 45 | def update(self, message_string, fakeLen=None): 46 | msg_bytes = _pad(message_string, fakeLen) 47 | for i in range(0, len(msg_bytes), 64): 48 | self._compress(msg_bytes[i:i+64]) 49 | 50 | def _compress(self, block): 51 | a, b, c, d = self.A, self.B, self.C, self.D 52 | 53 | x = [] 54 | for i in range(0, 64, 4): 55 | x.append(unpack('= 'A' and chr(byte) <= 'Z' 45 | 46 | def RecoverPlaintexts(ciphertextes): 47 | plaintextes = [''] * len(ciphertextes) 48 | allKeyBytes = range(0, 256) 49 | 50 | # Stores the reconstructed key, as an array of bytes. 51 | maxScoreWholeKey = [] 52 | 53 | # Explanation: to recover byte 1, we try all possible key bytes: for 54 | # every potential key byte, we compute the score by XORing the byte with 55 | # byte 1 of each ciphertext and adding all the individual scores up. The 56 | # key byte with the highest final score wins and is stored as the "cracked" 57 | # key byte for that position. 58 | # 59 | # Due to how the CTR keystream is generated, if I encrypt a text of length 60 | # 53, the key has also size 53. Therefore we need to crack it byte by byte, 61 | # we cannot try all key combinations. 62 | # 63 | # There is some adjustment because this does not actually work for the 64 | # first character: the if condition basically makes sure that the plaintext 65 | # byte we get with the key is always an uppercase letter or the character 66 | # "'" (apostrophe). I thought of using this heuristics by looking at the 67 | # recovered plaintexts in which the first character was gibberish. A little 68 | # trick that makes it more similar to challenge 19 :-) 69 | 70 | for ctIndex in range(len(ciphertextes[0])): 71 | maxScore = -1 72 | maxScoreKey = -1 73 | 74 | for keyByte in allKeyBytes: 75 | byteScore = 0 76 | 77 | for ct in ciphertextes: 78 | possiblePT = ct[ctIndex] ^ keyByte 79 | 80 | if ctIndex == 0 and not IsUpper(possiblePT) and not chr(possiblePT) == "'": 81 | byteScore = -1 82 | break 83 | 84 | # Very low score for non-readable characters. 85 | if not IsReadable(possiblePT): 86 | byteScore = -100 87 | break 88 | 89 | # This adds 0 if the character does not exist in the dictionary. 90 | byteScore += CHAR_FREQUENCIES[chr(possiblePT)] 91 | 92 | if byteScore > maxScore: 93 | maxScore = byteScore 94 | maxScoreKey = keyByte 95 | 96 | maxScoreWholeKey.append(maxScoreKey) 97 | 98 | index = 0 99 | for ct in ciphertextes: 100 | for ctByteIndex in range(len(ct)): 101 | plaintextes[index] += chr(ct[ctByteIndex] ^ maxScoreWholeKey[ctByteIndex]) 102 | index += 1 103 | 104 | return plaintextes 105 | 106 | if __name__ == '__main__': 107 | plaintextes = [] 108 | 109 | # Read the input as byte strings. 110 | with open('data/20.txt', 'rb') as data: 111 | plaintextes = data.readlines() 112 | 113 | # Reuse the faulty CTR encryption from challenge 19. 114 | ciphertextes = EncryptWithFixedNonce(plaintextes) 115 | 116 | # Compute the minimum length of all ciphertextes and truncate all of them 117 | # to this length. 118 | min_len = len(ciphertextes[0]) 119 | 120 | for ct in ciphertextes: 121 | if len(ct) < min_len: 122 | min_len = len(ct) 123 | 124 | truncatedCiphertexts = [ct[:min_len] for ct in ciphertextes] 125 | recoveredPlaintextes = RecoverPlaintexts(truncatedCiphertexts) 126 | 127 | # Of course we only recovered each plaintext up to min_len, but at this 128 | # point you can either reapply the algorithm on the longest strings, or 129 | # just move to guessing the rest. 130 | for pt in recoveredPlaintextes: 131 | print(pt) 132 | -------------------------------------------------------------------------------- /set1/utils/encoding_utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import itertools 3 | import string 4 | 5 | # These should not be accessed externally. 6 | HEX_TO_BIN_MAP = {'0': '0000', 7 | '1': '0001', 8 | '2': '0010', 9 | '3': '0011', 10 | '4': '0100', 11 | '5': '0101', 12 | '6': '0110', 13 | '7': '0111', 14 | '8': '1000', 15 | '9': '1001', 16 | 'a': '1010', 17 | 'b': '1011', 18 | 'c': '1100', 19 | 'd': '1101', 20 | 'e': '1110', 21 | 'f': '1111'} 22 | BIN_TO_HEX_MAP = {'0000': '0', 23 | '0001': '1', 24 | '0010': '2', 25 | '0011': '3', 26 | '0100': '4', 27 | '0101': '5', 28 | '0110': '6', 29 | '0111': '7', 30 | '1000': '8', 31 | '1001': '9', 32 | '1010': 'a', 33 | '1011': 'b', 34 | '1100': 'c', 35 | '1101': 'd', 36 | '1110': 'e', 37 | '1111': 'f'} 38 | 39 | class Error(Exception): 40 | pass 41 | 42 | class InvalidArgumentError(Error): 43 | pass 44 | 45 | 46 | def HexToBin(hex_string): 47 | """Converts an hexadecimal string to the corresponding binary string.""" 48 | res = '' 49 | 50 | for ch in hex_string: 51 | if ch not in HEX_TO_BIN_MAP: 52 | raise InvalidArgumentError( 53 | 'Called HexToBin with non-hex string: ' + hex_string) 54 | 55 | res += HEX_TO_BIN_MAP[ch] 56 | 57 | return res 58 | 59 | 60 | def HexToBase64(hex_string): 61 | """Converts an hexadecimal string into a base64-encoded string.""" 62 | bin_string = HexToBin(hex_string) 63 | 64 | # The list of all base64 characters, ordered by their numerical position. 65 | base64table = (list(string.ascii_uppercase) + list(string.ascii_lowercase) + 66 | [str(x) for x in range(10)] + ['+', '/']) 67 | BIN_TO_B64_MAP = {} 68 | index = 0 69 | 70 | # For each number between 0 and 64, in binary digits, map it to the base64 71 | # character at that position. 72 | for bin in map(''.join, itertools.product('01', repeat=6)): 73 | BIN_TO_B64_MAP[bin] = base64table[index] 74 | index += 1 75 | 76 | b64_string = '' 77 | for j in range(0, len(bin_string) - 5, 6): 78 | b64_string += BIN_TO_B64_MAP[bin_string[j:j+6]] 79 | 80 | return b64_string 81 | 82 | 83 | def BinToHex(bin_string): 84 | """Converts a binary string to the corresponding hexadecimal one.""" 85 | res_hex = '' 86 | 87 | # Divides the string in pieces of 4 characters each, then converts 88 | # each piece to an hexadecimal character. 89 | # 90 | # If the length of the input string is not divisible by 4, the last 91 | # bits will be ignored. 92 | for i in range(0, len(bin_string) - 3, 4): 93 | piece = bin_string[i:i+4] 94 | if piece not in BIN_TO_HEX_MAP: 95 | raise InvalidArgumentError( 96 | "Called BinToHex with non-bin string: " + bin_string) 97 | 98 | res_hex += BIN_TO_HEX_MAP[piece] 99 | 100 | return res_hex 101 | 102 | def ReadableAscii(i): 103 | return (i == 10 or (i > 31 and i < 127)) 104 | 105 | def BinToAscii(bin_string): 106 | """Returns a tuple of 2 elements: first one is the ASCII decoding of 107 | the binary string, second one is False if the decoding includes non-readable 108 | characters.""" 109 | ascii = '' 110 | is_readable = True 111 | 112 | for i in range(0, len(bin_string) - 7, 8): 113 | # Derive individual "slices" of the string, 8 bits each. 114 | piece = bin_string[i:i+8] 115 | # Convert to an integer. 116 | num_piece = int(piece, base=2) 117 | 118 | if not ReadableAscii(num_piece): 119 | is_readable = False 120 | ascii += chr(num_piece) 121 | 122 | return (ascii, is_readable) 123 | 124 | def Base64ToBin(text64): 125 | """Converts a base64-encoded string into a binary string.""" 126 | text = base64.b64decode(text64) 127 | bin_text = '' 128 | 129 | for ch in text: 130 | # We remove the leading '0b' added by bin() 131 | bin_ch = bin(ch)[2:] 132 | 133 | # Each 'ch' is a decimal ASCII character, so the resulting 134 | # binary number must have 8 digits. We pad with zeroes when 135 | # shorter. 136 | while len(bin_ch) < 8: 137 | bin_ch = '0' + bin_ch 138 | 139 | bin_text += bin_ch 140 | 141 | return bin_text 142 | 143 | 144 | def Base64ToAscii(text64): 145 | """Converts a base64-encoded string into an ASCII string.""" 146 | text = base64.b64decode(text64) 147 | ascii_text = '' 148 | 149 | # The loop gives us the (decimal) ASCII number for each character, 150 | # so we only need to convert it to an actual character. 151 | for ch in text: 152 | ascii_text += chr(ch) 153 | 154 | return ascii_text 155 | 156 | def HammingDistance(b1, b2): 157 | """Computes the Hamming distance between two binary strings. 158 | 159 | No verification is done on the strings (I am lazy), so use 160 | at your own risk. 161 | """ 162 | assert len(b1) == len(b2) 163 | distance = 0 164 | 165 | for i in range(len(b1)): 166 | if b1[i] != b2[i]: 167 | distance += 1 168 | 169 | return distance 170 | -------------------------------------------------------------------------------- /lib/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities not related to a specific challenge.""" 2 | 3 | import base64 4 | 5 | class Error(Exception): 6 | pass 7 | 8 | class InvalidArgumentError(Error): 9 | pass 10 | 11 | 12 | # These should not be accessed externally. 13 | hex_to_bin_map = {'0': '0000', 14 | '1': '0001', 15 | '2': '0010', 16 | '3': '0011', 17 | '4': '0100', 18 | '5': '0101', 19 | '6': '0110', 20 | '7': '0111', 21 | '8': '1000', 22 | '9': '1001', 23 | 'a': '1010', 24 | 'b': '1011', 25 | 'c': '1100', 26 | 'd': '1101', 27 | 'e': '1110', 28 | 'f': '1111'} 29 | bin_to_hex_map = {'0000': '0', 30 | '0001': '1', 31 | '0010': '2', 32 | '0011': '3', 33 | '0100': '4', 34 | '0101': '5', 35 | '0110': '6', 36 | '0111': '7', 37 | '1000': '8', 38 | '1001': '9', 39 | '1010': 'a', 40 | '1011': 'b', 41 | '1100': 'c', 42 | '1101': 'd', 43 | '1110': 'e', 44 | '1111': 'f'} 45 | 46 | def hex_to_bin(hex_string): 47 | """Converts an hexadecimal string to the corresponding binary string.""" 48 | res = '' 49 | 50 | for ch in hex_string: 51 | if ch not in hex_to_bin_map: 52 | raise InvalidArgumentError( 53 | "Called hex_to_bin with non-hex string: " + hex_string) 54 | 55 | res += hex_to_bin_map[ch] 56 | 57 | return res 58 | 59 | def bin_to_hex(bin_string): 60 | """Converts a binary string to the corresponding hexadecimal one.""" 61 | res_hex = '' 62 | 63 | # Divides the string in pieces of 4 characters each, then converts 64 | # each piece to an hexadecimal character. 65 | # 66 | # If the length of the input string is not divisible by 4, the last 67 | # bits will be ignored. 68 | for i in range(0, len(bin_string) - 3, 4): 69 | piece = bin_string[i:i+4] 70 | if piece not in bin_to_hex_map: 71 | raise InvalidArgumentError( 72 | "Called bin_to_hex with non-bin string: " + bin_string) 73 | 74 | res_hex += bin_to_hex_map[piece] 75 | 76 | return res_hex 77 | 78 | # TODO: enforce one naming convention. 79 | def ReadableAscii(i): 80 | return (i == 10 or (i > 31 and i < 127)) 81 | 82 | def bin_to_ascii(bin_string): 83 | """Returns a tuple of 2 elements: first one is the ASCII decoding of 84 | the binary string, second one is False if the decoding includes non-readable 85 | characters.""" 86 | ascii = '' 87 | is_readable = True 88 | 89 | for i in range(0, len(bin_string) - 7, 8): 90 | # Derive individual "slices" of the string, 8 bits each. 91 | piece = bin_string[i:i+8] 92 | # Convert to an integer. 93 | num_piece = int(piece, base=2) 94 | 95 | if not ReadableAscii(num_piece): 96 | is_readable = False 97 | ascii += chr(num_piece) 98 | 99 | return (ascii, is_readable) 100 | 101 | def base64_to_bin(text64): 102 | """Converts a base64-encoded string into a binary string.""" 103 | text = base64.b64decode(text64) 104 | bin_text = '' 105 | 106 | for ch in text: 107 | # We remove the leading '0b' added by bin() 108 | bin_ch = bin(ch)[2:] 109 | 110 | # Each 'ch' is a decimal ASCII character, so the resulting 111 | # binary number must have 8 digits. We pad with zeroes when 112 | # shorter. 113 | while len(bin_ch) < 8: 114 | bin_ch = '0' + bin_ch 115 | 116 | bin_text += bin_ch 117 | 118 | return bin_text 119 | 120 | def base64_to_ascii(text64): 121 | """Converts a base64-encoded string into an ASCII string.""" 122 | text = base64.b64decode(text64) 123 | ascii_text = '' 124 | 125 | # The loop gives us the (decimal) ASCII number for each character, 126 | # so we only need to convert it to an actual character. 127 | for ch in text: 128 | ascii_text += chr(ch) 129 | 130 | return ascii_text 131 | 132 | def ByteToPaddedBin(byte): 133 | """Converts a byte to a binary string of length 8.""" 134 | 135 | bin_str = '{0:b}'.format(byte) 136 | 137 | while len(bin_str) < 8: 138 | bin_str = '0' + bin_str 139 | 140 | return bin_str 141 | 142 | 143 | def HammingDistance(b1, b2): 144 | """Computes the Hamming distance between two binary strings. 145 | 146 | No verification is done on the strings (I am lazy), so use 147 | at your own risk. 148 | """ 149 | assert len(b1) == len(b2) 150 | distance = 0 151 | 152 | for i in range(len(b1)): 153 | if b1[i] != b2[i]: 154 | distance += 1 155 | 156 | return distance 157 | 158 | def HammingDistanceAscii(s1, s2): 159 | """Computes the Hamming distance between two ASCII strings.""" 160 | assert len(s1) == len(s2) 161 | distance = 0 162 | 163 | for i in range(0, len(s1)): 164 | s1_byte = ord(s1[i]) 165 | s2_byte = ord(s2[i]) 166 | 167 | # Each byte is converted to binary; the resulting string 168 | # must have length 8 in order to preserve the ASCII 169 | # information, therefore we pad it. 170 | s1_bin = ByteToPaddedBin(s1_byte) 171 | s2_bin = ByteToPaddedBin(s2_byte) 172 | 173 | for j in range(0, len(s1_bin)): 174 | if s1_bin[j] != s2_bin[j]: 175 | distance += 1 176 | 177 | return distance -------------------------------------------------------------------------------- /set6/ch47.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 6, challenge 47: Bleichenbacher's PKCS 1.5 Padding Oracle (Simple Case) 4 | 5 | from ch42 import BytesToInteger, IntegerToBytes 6 | import math 7 | import rsa 8 | from utils import modexp 9 | 10 | KEY_BYTESIZE = 32 11 | 12 | class ParityOracle(object): 13 | def __init__(self, key): 14 | self._key = key 15 | 16 | def IsValid(self, ciphernum): 17 | plainnum = rsa.Decrypt(self._key, ciphernum) 18 | plainbytes = IntegerToBytes(plainnum) 19 | 20 | # Pads the message in front, if necessary. This is because if the number 21 | # had a leading zero, it would not be translated into bytes. 22 | while len(plainbytes) < KEY_BYTESIZE: 23 | plainbytes = b'\x00' + plainbytes 24 | 25 | return plainbytes[0] == 0 and plainbytes[1] == 2 26 | 27 | def PKCS1_ENCODE(M): 28 | # Pad enough to get to the same length in bytes as the public modulo n. 29 | padding = (KEY_BYTESIZE - len(M) - 3) * b'\xff' 30 | EM = b'\x00\x02' + padding + b'\x00' + M 31 | return EM 32 | 33 | def PKCS1_DECODE(M): 34 | if M[0] != 0 or M[1] != 2: 35 | raise Exception('Cannot decode malformed message') 36 | 37 | # After the first 00 and 02 bytes, there should be a positive number of 38 | # non-zero bytes (the padding), then 00, then the message. 39 | for i in range(2, len(M)): 40 | if M[i] == 0 and i > 2: 41 | return M[i+1:] 42 | 43 | raise Exception('Cannot decode malformed message') 44 | 45 | # This is used for step 1 and for cases where there is more than one interval. 46 | def FindConformingBaseStep(startS, oracle, pubKey, ciphernum): 47 | e, n = pubKey 48 | s = startS 49 | 50 | while True: 51 | c = (ciphernum * pow(s, e, n)) % n 52 | if oracle.IsValid(c): 53 | return s 54 | s += 1 55 | 56 | # This is used in case there is exactly one interval, but we have not found 57 | # the solution yet. 58 | def FindConformingFromR(r, oracle, pubKey, ciphernum, interval): 59 | a, b = interval 60 | e, n = pubKey 61 | B = 2 ** (8 * (KEY_BYTESIZE-2)) 62 | 63 | # NOTE: for the upper bound of the range, the paper reports 64 | # (3*B - 1 + r*n) / a. I mistakenly added + 1 at the end to add one 65 | # to the numerator, since Python ranges do not include the upper bound. 66 | # However that actually adds 1 to the denominator due to precedence 67 | # rules. I kept because it makes the whole thing much faster. 68 | for si in range((2 * B + r * n + b - 1) // b, (3 * B - 1 + r * n) // a + 1): 69 | c = (ciphernum * pow(si, e, n)) % n 70 | if oracle.IsValid(c): 71 | return si 72 | 73 | return None 74 | 75 | # Step 3, the computation of new intervals. 76 | def ComputeNextIntervals(Mi, si, n): 77 | B = 2 ** (8 * (KEY_BYTESIZE-2)) 78 | Mnext = [] 79 | 80 | for a, b in Mi: 81 | for r in range((a*si - 3*B + 1) // n, ((b * si - 2*B) // n) + 1): 82 | newA = max(a, (2 * B + r * n) // si + 1) 83 | newB = min(b, (3 * B - 1 + r * n) // si) 84 | newInterval = (newA, newB) 85 | 86 | if newB >= newA and newInterval not in Mnext: 87 | Mnext.append(newInterval) 88 | 89 | return Mnext 90 | 91 | def Attack(ciphernum, pubKey, oracle): 92 | e, n = pubKey 93 | B = 2 ** (8 * (KEY_BYTESIZE-2)) 94 | 95 | i = 1 96 | c0 = ciphernum 97 | Mi = [(2*B, 3*B - 1)] 98 | # As suggested in the paper, we skip blinding and set s0 to 1. This is 99 | # because we are not forging signatures. 100 | s0 = 1 101 | 102 | s1 = math.ceil(n / (3 * B)) 103 | s1 = FindConformingBaseStep(s1, oracle, pubKey, c0) 104 | 105 | sNext = s1 106 | while True: 107 | si = sNext 108 | sNext = None 109 | 110 | if i == 1: 111 | s1 = math.ceil(n / (3 * B)) 112 | sNext = FindConformingBaseStep(s1, oracle, pubKey, c0) 113 | elif len(Mi) == 1: 114 | a, b = Mi[0] 115 | if a == b: 116 | print('Attack successful after %d iterations' % i) 117 | # This is simpler than the original calculation, but only 118 | # works because we artificially set s0 to 1 before. 119 | return a 120 | else: 121 | ri = (2 * b * si - 4 * B) // n 122 | 123 | while True: 124 | sNext = FindConformingFromR(ri, oracle, pubKey, ciphernum, Mi[0]) 125 | if sNext is not None: 126 | break 127 | ri += 1 128 | else: 129 | sNext = FindConformingBaseStep(s1 + 1, oracle, pubKey, c0) 130 | 131 | Mi = ComputeNextIntervals(Mi, sNext, n) 132 | i += 1 133 | 134 | def GetRSAPair(): 135 | E = 65537 136 | N = 61042692721884464235697738122307839813042477264962372870792878302623082159791 137 | D = 9898236043082017813338417428716075097851119224050395969169000482174371385137 138 | return (E, N), (D, N) 139 | 140 | if __name__ == '__main__': 141 | pubKey, privKey = GetRSAPair() 142 | oracle = ParityOracle(privKey) 143 | 144 | message = b'kick it, CC' 145 | print('[**] Encrypting message:', message) 146 | ciphernum = rsa.Encrypt(pubKey, BytesToInteger(PKCS1_ENCODE(message))) 147 | 148 | plaintextBytes = IntegerToBytes(Attack(ciphernum, pubKey, oracle)) 149 | while len(plaintextBytes) < KEY_BYTESIZE: 150 | plaintextBytes = b'\x00' + plaintextBytes 151 | 152 | recoveredPlaintext = PKCS1_DECODE(plaintextBytes) 153 | print('[**] Recovered plaintext:', recoveredPlaintext) 154 | -------------------------------------------------------------------------------- /set2/ch12.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Set 2, challenge 12: byte-at-a-time ECB decryption (simple). 4 | 5 | import aes_lib 6 | import base64 7 | import collections 8 | 9 | from Crypto.Cipher import AES 10 | from Crypto import Random 11 | 12 | def CountRepeatedBlocks(byte_string, chunk_size): 13 | """Counts how many chunks of given size in the string are 14 | repeated more than once..""" 15 | chunk_start = 0 16 | 17 | # New keys get a default value of 0. The dictionary will 18 | # count the number of occurrences of each 16-bytes chunk in 19 | # the string. 20 | chunkCounter = collections.defaultdict(int) 21 | 22 | while chunk_start < len(byte_string): 23 | chunk = byte_string[chunk_start : chunk_start + chunk_size] 24 | chunkCounter[chunk] += 1 25 | chunk_start += chunk_size 26 | 27 | # This generator adds 1 for each value in the dictionary that 28 | # is greater than 1. 29 | return sum(1 for c in chunkCounter if chunkCounter[c] > 1) 30 | 31 | 32 | def GetBlockSize(encrypter): 33 | pad = b'' 34 | initial_len = len(encrypter.Encrypt(pad)) 35 | l = initial_len 36 | 37 | # Find 'l' such that the complete plaintext used to produce it 38 | # has no padding at the end. This means the ciphertext length 39 | # actually increased when we added more plaintext, signaling 40 | # that we added a new ciphertext block. 41 | while l == initial_len: 42 | pad += b'A' 43 | l = len(encrypter.Encrypt(pad)) 44 | 45 | # Find 'l2' such as it is the next ciphertext length with the 46 | # same property as 'l'. We added yet another block to the 47 | # ciphertext. 48 | l2 = l 49 | while l2 == l: 50 | pad += b'A' 51 | l2 = len(encrypter.Encrypt(pad)) 52 | 53 | # The distance between them is the size of one block. 54 | return (l2 - l) 55 | 56 | def DetectEncryptionMode(ciphertext, block_size): 57 | """We can only recognize ECB if there is at least one repeated block 58 | in the ciphertext. But this won't always work, as different input 59 | blocks will produce different output blocks with ECB too.""" 60 | repetitions = CountRepeatedBlocks(ciphertext, block_size) 61 | return AES.MODE_ECB if repetitions > 0 else AES.MODE_CBC 62 | 63 | def ModeToString(mode): 64 | """Convenience method for printing AES modes.""" 65 | if mode == 1: 66 | return 'ECB' 67 | elif mode == 2: 68 | return 'CBC' 69 | 70 | def GetNumSecretBlocks(enc, block_size): 71 | # Add no plaintext of our own to find out how many are 72 | # appended by the encrypter. 73 | c = enc.Encrypt(b'') 74 | return int(len(c) / block_size) 75 | 76 | def BreakECB(enc, block_size): 77 | """Breaks this specific ECB oracle given the block size we computed before.""" 78 | broken_text = b'' 79 | num_secret_blocks = GetNumSecretBlocks(enc, block_size) 80 | 81 | # List of ASCII readable bytes as decimal integers. 82 | readable_bytes = [10] + list(range(32, 127)) 83 | 84 | # We can discover one byte of the "secret string" with this method: 85 | # 1. Encrypt a plaintext formed by as many A as the block size minus 1. 86 | # 2. The last byte of the block will be filled with the first byte of the text to break. 87 | # 3. We generate all possibilities for the complete block by appending each possible 88 | # readable byte (n below). 89 | # 4. By comparing the encrypted blocks, we can find out the correct plaintext byte. 90 | # 91 | # Let's say the first discovered byte is X. Now we encrypt a plaintext 92 | # formed by as many A as the block size minus 2. The encrypter will add X and another byte 93 | # to complete the block, so we can repeat the procedure and find the second byte. 94 | # 95 | # When the first block has been completely decrypted, we just repeat the same procedure, 96 | # but we compare bytes from the 2nd block, and then the 3rd... since we already know 97 | # the whole plaintext that comes before that. 98 | for k in range(0, num_secret_blocks): 99 | for b in range(block_size - 1, -1, -1): 100 | oneshort = enc.Encrypt(b'A' * b) 101 | # Get only the block we are currently decrypting. 102 | oneshort = oneshort[block_size * k : block_size * (k+1)] 103 | 104 | for n in readable_bytes: 105 | # The generated plaintext here contains the same padding as the 106 | # original one, plus the secret key plaintext we discovered (if any), 107 | # and the current guess for the next byte. 108 | pt = (b'A' * b) + broken_text + bytes([n]) 109 | calculated = enc.Encrypt(pt) 110 | # Get the block we are currently decrypting. 111 | calculated = calculated[block_size * k : block_size * (k+1)] 112 | 113 | if calculated == oneshort: 114 | broken_text += bytes([n]) 115 | break 116 | 117 | # Chop to known size otherwise we also return the random padding at the end. 118 | return broken_text[:num_secret_blocks * block_size] 119 | 120 | 121 | if __name__ == '__main__': 122 | enc = aes_lib.RandomizedCipher() 123 | 124 | block_size = GetBlockSize(enc) 125 | print("[**] Detected block size is " + str(block_size)) 126 | 127 | ciphertext = enc.Encrypt(b'A' * (block_size * 2)) 128 | mode = DetectEncryptionMode(ciphertext, block_size) 129 | 130 | if mode != AES.MODE_ECB: 131 | print("[!!] Wrong encryption mode detected: " + str(mode)) 132 | exit(0) 133 | 134 | print("[**] Detected mode is: " + ModeToString(mode)) 135 | 136 | plaintext = BreakECB(enc, block_size) 137 | print("[**] Uncovered plaintext: " + plaintext.decode('ascii')) 138 | -------------------------------------------------------------------------------- /set2/aes_lib.py: -------------------------------------------------------------------------------- 1 | # Library for AES utilities, used thorough the challenges. 2 | 3 | import base64 4 | from Crypto.Cipher import AES 5 | from Crypto import Random 6 | import struct 7 | from pkcs7 import Pkcs7 8 | from pkcs7 import StripPkcs7 9 | import random 10 | 11 | class AESCipher(object): 12 | def __init__(self, key=None, mode=AES.MODE_ECB, iv=None): 13 | """Initialize a AES cipher with the given mode, and key (as a byte string) 14 | 15 | Parameters: 16 | key: the key, as a byte string (e.g. b'YELLOW SUBMARINE'). If None, 17 | a random one will be generated. 18 | mode: AES mode. Default: AES.MODE_ECB. 19 | iv: IV. If None, a random one will be generated internally. 20 | """ 21 | 22 | if iv is None: 23 | self._iv = self.GenerateRandomBytes(AES.block_size) 24 | else: 25 | self._iv = iv 26 | 27 | if key is None: 28 | self._key = self.GenerateRandomBytes(AES.block_size) 29 | else: 30 | self._key = key 31 | 32 | self.mode = mode 33 | self._cipher = AES.new(key=self._key, mode=self.mode, IV=self._iv) 34 | 35 | def aes_decrypt(self, message): 36 | """Decrypt a message under the cipher. The message should be a byte string.""" 37 | return self._cipher.decrypt(message) 38 | 39 | def aes_encrypt(self, message): 40 | """Encrypt a message under the cipher. The message should be a byte string.""" 41 | return self._cipher.encrypt(message) 42 | 43 | def aes_pad_and_encrypt(self, message): 44 | """Encrypts a message under the cipher, adding PKCS7 padding if required.""" 45 | return self.aes_encrypt(Pkcs7(message, AES.block_size)) 46 | 47 | def aes_decrypt_and_depad(self, message): 48 | """Decrypts a message under the cipher, removing and verifying PKCS7 padding.""" 49 | return StripPkcs7(self.aes_decrypt(message), AES.block_size) 50 | 51 | def GenerateRandomBytes(self, size): 52 | """Random byte string of given size.""" 53 | return Random.new().read(size) 54 | 55 | def _ByteXOR(self, s1, s2): 56 | """Computes the XOR between two byte strings.""" 57 | assert len(s1) == len(s2) 58 | res = b'' 59 | 60 | for i in range(0, len(s1)): 61 | # Each byte is converted to a number, XORed, and then 62 | # converted back. 63 | n1 = s1[i] 64 | n2 = s2[i] 65 | res += bytes([n1 ^ n2]) 66 | 67 | return res 68 | 69 | def SimulateCBCEncryption(self, plaintext): 70 | assert self.mode == AES.MODE_ECB 71 | 72 | prev_ct = self._iv 73 | block_index = 0 74 | ciphertext = b'' 75 | 76 | # The loop simulates decryption through AES in CBC mode. 77 | # In such mode, the ciphertext is divided in blocks the size 78 | # of the key. Each block is decrypted, then the plaintext is XORed 79 | # with the previous ciphertext block. To initialize the algorithm, 80 | # a random IV (initialization vector) is used. 81 | while block_index < len(plaintext): 82 | block = plaintext[block_index : block_index + AES.block_size] 83 | final_block = self._ByteXOR(block, prev_ct) 84 | 85 | cipher_block = self.aes_encrypt(final_block) 86 | prev_ct = cipher_block 87 | ciphertext += cipher_block 88 | 89 | block_index += AES.block_size 90 | 91 | return ciphertext 92 | 93 | def SimulateCBCDecryption(self, ciphertext): 94 | """Implement decryption with CBC mode, without relying on the 95 | underlying library. 96 | 97 | Only works if the current mode is ECB. No padding is applied. 98 | """ 99 | assert self.mode == AES.MODE_ECB 100 | 101 | prev_ct = self._iv 102 | block_index = 0 103 | plaintext = b'' 104 | 105 | # The loop simulates decryption through AES in CBC mode. 106 | # In such mode, the ciphertext is divided in blocks the size 107 | # of the key. Each block is decrypted, then the plaintext is XORed 108 | # with the previous ciphertext block. To initialize the algorithm, 109 | # a random IV (initialization vector) is used. 110 | while block_index < len(ciphertext): 111 | block = ciphertext[block_index : block_index + AES.block_size] 112 | 113 | prep_plaintext = self.aes_decrypt(block) 114 | plaintext += self._ByteXOR(prev_ct, prep_plaintext) 115 | prev_ct = block 116 | 117 | block_index += AES.block_size 118 | return plaintext 119 | 120 | 121 | class RandomizedCipher(AESCipher): 122 | 123 | def __init__(self): 124 | self.key = AESCipher.GenerateRandomBytes(self, AES.block_size) 125 | AESCipher.__init__(self, self.key) 126 | 127 | base64_filler = ('Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg' 128 | 'aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq' 129 | 'dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg' 130 | 'YnkK') 131 | self.filler = base64.b64decode(base64_filler) 132 | 133 | # Intentionally bigger than 1 block to make things a bit more challenging. 134 | randomPrefixLen = random.randint(1, 32) 135 | self.randomPrefix = AESCipher.GenerateRandomBytes(self, randomPrefixLen) 136 | 137 | def Encrypt(self, plaintext): 138 | """Returns the encrypted text.""" 139 | text = plaintext + self.filler 140 | return AESCipher.aes_pad_and_encrypt(self, text) 141 | 142 | def EncryptWithRandomPad(self, plaintext): 143 | """Prepends a random count of random bytes to the plaintext 144 | before encrypting. The number of bytes and the bytes themselves 145 | are generated in the constructor.""" 146 | text = self.randomPrefix + plaintext + self.filler 147 | return AESCipher.aes_pad_and_encrypt(self, text) 148 | -------------------------------------------------------------------------------- /set3/ch19.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from Crypto.Cipher import AES 4 | from Crypto import Random 5 | import ch18 as ctr 6 | import base64 7 | # Note: this is the same module I implemented for set 1. 8 | from plaintext_verifier import PlaintextVerifier 9 | 10 | # Set 3, Challenge 19: break fixed-nonce CTR mode using substitutions. 11 | 12 | def EncryptWithFixedNonce(plaintextes): 13 | """Challenge setup: encrypt all the plaintextes separately with 14 | a fixed nonce (0) and fixed random key.""" 15 | result = [] 16 | key = Random.new().read(AES.block_size) 17 | 18 | for pt in plaintextes: 19 | ct = ctr.DoCTR(base64.b64decode(pt), key, 0) 20 | result.append(ct) 21 | 22 | return result 23 | 24 | def IsReadable(byte): 25 | """Returns True if the character is in readable ASCII range.""" 26 | return byte in list(range(32, 127)) + [10] 27 | 28 | def RecoverByteAllUpper(ciphertextes): 29 | """Return the keystream byte such that the first char 30 | of all ciphertextes decrypt to an upper case letter.""" 31 | resByte = -1 32 | 33 | for keyByte in range(0, 256): 34 | for ct in ciphertextes: 35 | ptByte = ct[0] ^ keyByte 36 | 37 | # Found a counterexample ciphertext, we can stop. 38 | if not IsReadable(ptByte) or chr(ptByte) < 'A' or chr(ptByte) > 'Z': 39 | break 40 | else: 41 | # Found the valid byte. 42 | resByte = keyByte 43 | break 44 | 45 | if resByte == -1: 46 | raise Exception('No byte found') 47 | 48 | return resByte 49 | 50 | def UpdatePlaintextes(cts, charIndex, pts, keyByte): 51 | """Utility function to update all the plaintextes knowing the next byte 52 | of the keystream that was used to encrypt them.""" 53 | index = 0 54 | 55 | for pt in pts: 56 | # Some strings are shorter than others, so this could 57 | # be out of range. Just skip them. 58 | if charIndex >= len(cts[index]): 59 | index += 1 60 | continue 61 | 62 | # Decrypt. The result should be readable or keyByte is 63 | # not valid and we need to try a different guess. 64 | pts[index] += chr(cts[index][charIndex] ^ keyByte) 65 | index += 1 66 | 67 | return pts 68 | 69 | def RecoverByteEqual(cts, index, charIndex, expectedChar): 70 | """Return keyByte such that the character at position charIndex of 71 | the ciphertext with index 'index' decrypts to expectedChar.""" 72 | 73 | for keyByte in range(0, 256): 74 | ptByte = cts[index][charIndex] ^ keyByte 75 | if IsReadable(ptByte) and chr(ptByte) == expectedChar: 76 | return keyByte 77 | 78 | raise Exception('No byte found') 79 | 80 | # The method used here is not orthodox, as the challenge itself suggested. 81 | # There are too many possibilities for an efficient backtracking algorithm, 82 | # even eliminating all non-readable bytes from the possible plaintextes; 83 | # moreover, most heuristics to recognize English plaintext need a few characters 84 | # to work with, reducing early pruning. 85 | # 86 | # This is the approach: I used with the assumption that all textes must 87 | # start with a capital letter, which was correct (other assumptions failed :-)). 88 | # Then at each iteration of the for-loop below, I basically look at all the 89 | # partially recovered plaintextes and try to guess the likely next character 90 | # for one text; I then recover the key byte that would have generated that 91 | # character for that text and apply it to all textes. I print the result, if 92 | # it is sensible, I repeat with the next character, and so on. 93 | # This was very easy: several first characters were 'W', which led me to 94 | # guess 'h'; then two other strings were 'Or', so I guessed a whitespace next, 95 | # and so on. Guessing whitespaces was especially a good idea since at least one 96 | # string was bound to be at the end of a word in most iterations. After a while 97 | # I just googled the first partial text and found the whole poem to recover 98 | # the last characters. 99 | # 100 | # Since not all textes are of the same length, at the end I just 'guess' the 101 | # remaining characters for the longest ones; the shortest ones are ignored. 102 | # 103 | # The guessed characters were collected in guessedNextChars, while the indices 104 | # of the strings for which they were guessed are in guessedStringIndex. 105 | def RecoverPlaintext(ciphertextes): 106 | recoveredKeystream = b'' 107 | plaintextes = [''] * len(ciphertextes) 108 | 109 | keyByte = RecoverByteAllUpper(ciphertextes) 110 | plaintextes = UpdatePlaintextes(ciphertextes, 0, plaintextes, keyByte) 111 | 112 | maxTextLength = len(max(ciphertextes, key=len)) 113 | guessedNextChars = 'h ng tle d mury ss e ng y ay headn' 114 | guessedStringIndex = [20, 5, 8, 1, 1, 1, 27, 39, 39, 39, 1, 4, 4, 0, 3, 3, 115 | 3, 3, 5, 5, 5, 6, 6, 39, 2, 2, 14, 37, 0, 0, 4, 4, 116 | 4, 4, 4, 37] 117 | 118 | for nextTextIndex in range(1, len(guessedNextChars) + 1): 119 | keyByte = RecoverByteEqual( 120 | ciphertextes, guessedStringIndex[nextTextIndex - 1], nextTextIndex, 121 | guessedNextChars[nextTextIndex - 1]) 122 | 123 | plaintextes = UpdatePlaintextes(ciphertextes, nextTextIndex, plaintextes, keyByte) 124 | 125 | for pt in plaintextes: 126 | print(pt) 127 | 128 | if __name__ == '__main__': 129 | plaintextes = [] 130 | 131 | # Read the input. 132 | with open('data/19.txt', 'rb') as data: 133 | plaintextes = data.readlines() 134 | 135 | ciphertextes = EncryptWithFixedNonce(plaintextes) 136 | RecoverPlaintext(ciphertextes) 137 | -------------------------------------------------------------------------------- /set7/ch51.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import codecs 4 | import zlib 5 | import string 6 | import base64 7 | 8 | from Crypto.Cipher import AES 9 | from Crypto import Random 10 | from Crypto.Util import Counter 11 | 12 | # Set 7, challenge 51: compression ratio side-channel attack. 13 | 14 | def padPKCS7(message, k=16): 15 | if len(message) % k == 0: 16 | return message 17 | 18 | ch = k - (len(message) % k) 19 | return message + bytes([ch] * ch) 20 | 21 | def FormatRequest(payload): 22 | """Formats the request according to the protocol.""" 23 | 24 | return (b'POST / HTTP/1.1\n' 25 | b'Host: hapless.com\n' 26 | b'Cookie: sessionid=TmV2ZXIgcmV2ZWFsIHRoZSBXdS1UYW5nIFNlY3JldCE=\n' 27 | b'Content-Length: %d\n%s' % (len(payload), payload)) 28 | 29 | def Compress(request): 30 | # NOTE: the compression algorithm obviously matters for the result. Some "leak" less 31 | # information than others. I have tried LZMA and it only managed to recover the first 32 | # two characters with the CTR oracle. 33 | return zlib.compress(request) 34 | 35 | def EncryptWithStream(plaintext): 36 | """Encrypts using AES in CTR mode with random key and nonce.""" 37 | key = Random.new().read(AES.block_size) 38 | cipher = AES.new(key=key, mode=AES.MODE_CTR, counter=Counter.new(AES.block_size * 8)) 39 | return cipher.encrypt(plaintext) 40 | 41 | def EncryptCBC(plaintext): 42 | """Encrypts using AES in CBC mode with random key and IV.""" 43 | key = Random.new().read(AES.block_size) 44 | iv = Random.new().read(AES.block_size) 45 | cipher = AES.new(key, AES.MODE_CBC, iv) 46 | 47 | # Note that while we have full control over the request, the compressed 48 | # version may still need padding for CBC to work. 49 | return cipher.encrypt(padPKCS7(plaintext)) 50 | 51 | def OracleCTR(payload): 52 | """Compression oracle with stream cipher.""" 53 | req = FormatRequest(payload) 54 | ct = EncryptWithStream(Compress(req)) 55 | 56 | return len(ct) 57 | 58 | def OracleCBC(payload): 59 | """Compression oracle with CBC.""" 60 | req = FormatRequest(payload) 61 | ct = EncryptCBC(Compress(req)) 62 | 63 | return len(ct) 64 | 65 | def GetPadding(payload, oracle): 66 | paddingAlphabet = b'!@#$%^&*()-`~[]{}' 67 | l = oracle(payload) 68 | padding = b'' 69 | 70 | # The idea here is to pad with "unique" characters (note that 71 | # they are not part of the regular alphabet, nor of the rest 72 | # of the request) until we cross a boundary to the "next" 73 | # length of the compressed request. 74 | # 75 | # This allows for more variation in the length of the 76 | # compressed request based on optimizations introduced by the 77 | # algorithm for similarities. 78 | for i in range(len(paddingAlphabet)): 79 | padding += bytearray([paddingAlphabet[i]]) 80 | newLen = oracle(padding + payload) 81 | 82 | if newLen > l: 83 | return padding 84 | 85 | return b'' 86 | 87 | 88 | def BreakWithOracle(sessionIdLength, oracle): 89 | recoveredId = b'' 90 | # All characters part of the base64 encoding. 91 | alphabet = str.encode(string.ascii_letters + string.digits + '+/=') 92 | 93 | for i in range(sessionIdLength): 94 | min_ch = b'' 95 | min_size = 10000 96 | 97 | # These two are only really required for CBC; this is because there 98 | # is naturally less variation in the ciphertext length when using CBC, 99 | # because it is always a multiple of the block size. 100 | # 101 | # I found out about adding "extra_spaces" by trial and error; prepending 102 | # it improved things but did not allow me to recover the whole ID. 103 | extra_spaces = b' ' * 16 104 | # The ~ character is not important, as long as it does not occur in the 105 | # rest of the request; this relies on knowing what characters can go in 106 | # the unknown session ID, or its encoding, but that is not an unreasonable 107 | # assumption given that we usually deal with public protocols. 108 | padding = GetPadding((b'sessionid=' + recoveredId + b'~' + extra_spaces) * 8, oracle) 109 | 110 | for ch in alphabet: 111 | payload = b'sessionid=' + recoveredId + bytearray([ch]) + extra_spaces 112 | 113 | # Repeating the payload multiple time is the key here; if you 114 | # repeat it only once, there is not enough similar characters 115 | # for the compression algorithm to use optimizations. Indeed 116 | # less than 8 repetitions are not enough here; more are enough. 117 | #print('Calling the oracle with', i, sessionIdLength) 118 | size = oracle(padding + (payload * 8)) 119 | 120 | # At every iteration, we simply pick the character than causes 121 | # the minimum length in the compressed (and encrypted) request. 122 | if size < min_size: 123 | min_size = size 124 | min_ch = bytearray([ch]) 125 | 126 | recoveredId += min_ch 127 | 128 | return recoveredId 129 | 130 | if __name__ == '__main__': 131 | trueSessionId = b'TmV2ZXIgcmV2ZWFsIHRoZSBXdS1UYW5nIFNlY3JldCE=' 132 | l = len(trueSessionId) 133 | 134 | recoveredIdCTR = BreakWithOracle(l, OracleCTR) 135 | recoveredIdCBC = BreakWithOracle(l, OracleCBC) 136 | 137 | if recoveredIdCTR == trueSessionId: 138 | print('[**] (CTR) Session ID recovered successfully:', base64.b64decode(recoveredIdCTR)) 139 | else: 140 | print('[**] (CTR) Recovered session ID does not match.') 141 | 142 | if recoveredIdCBC == trueSessionId: 143 | print('[**] (CBC) Session ID recovered successfully:', base64.b64decode(recoveredIdCBC)) 144 | else: 145 | print('[**] (CBC) Recovered session ID does not match: %s vs. %s' 146 | % (recoveredIdCBC, trueSessionId)) 147 | -------------------------------------------------------------------------------- /set7/ch52.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from Crypto.Cipher import AES 4 | import math 5 | import itertools 6 | 7 | # Set 7, challenge 52: iterated hash function multicollisions. 8 | 9 | def padPKCS7(message, k=16): 10 | """Usual PKCS#7 padding.""" 11 | if len(message) % k == 0: 12 | return message 13 | 14 | ch = k - (len(message) % k) 15 | return message + bytes([ch] * ch) 16 | 17 | def GetNumBlocks(message): 18 | return math.ceil(len(message) / AES.block_size) 19 | 20 | def GetBlock(message, index): 21 | # To make it easier, I assume there are always perfectly-sized blocks 22 | # in the input. 23 | return message[index * AES.block_size:(index+1) * AES.block_size] 24 | 25 | def GenerateAllCollisions(perStageCollisions, result, i=0, partial=b''): 26 | if i >= len(perStageCollisions): 27 | result.append(partial) 28 | return 29 | 30 | for piece in perStageCollisions[i]: 31 | newPartial = partial + piece 32 | GenerateAllCollisions(perStageCollisions, result, i+1, newPartial) 33 | 34 | def MerkleDamgard(message, state=b'I\xbf', stateLen=2): 35 | """Applies an arbitrary Merkle-Damgard construction to the message. 36 | 37 | The default state length and initial state are those used all over 38 | this program. 39 | """ 40 | newState = state 41 | # The state length we use is shorter than what AES wants for the keys. 42 | newState = padPKCS7(newState) 43 | 44 | for i in range(GetNumBlocks(message)): 45 | cipher = AES.new(newState, AES.MODE_ECB) 46 | newState = cipher.encrypt(GetBlock(message, i)) 47 | 48 | # This would be a really bad idea to do in practice, if we are 49 | # actually using AES or an algorithm that requires keys of 50 | # a certain size. It's needed here because the hash and 51 | # the key needs to be the same for the challenge to work, and 52 | # the hash we return has 2 bytes. 53 | newState = padPKCS7(newState[:stateLen]) 54 | 55 | return newState[:stateLen] 56 | 57 | def FindCollisionOneStage(allPossibleBlocks, initialState): 58 | """This finds collisions in one stage of the MD construction. 59 | 60 | This means that given all possible blocks of size 16 bytes, and 61 | an initial state, we look for two blocks that are hashed to the 62 | same sequence. The initial state is the actual initial state for 63 | the first block, and the colliding hash of the previous stage 64 | for next ones. 65 | 66 | Given the short size of hashes, we can brute-force this; there 67 | are only (2^8) possible blocks. 68 | """ 69 | hashDict = {} 70 | collisions = [] 71 | 72 | for b in allPossibleBlocks: 73 | b = padPKCS7(b) 74 | h = MerkleDamgard(b, initialState) 75 | 76 | if h in hashDict: 77 | collisions.append(b) 78 | collisions.append(hashDict[h]) 79 | # Return both the hash and the list of colliding blocks. 80 | return (h, collisions) 81 | else: 82 | hashDict[h] = b 83 | 84 | # Nothing found. 85 | return (b'', collisions) 86 | 87 | def FindCollisions(stateLen, n, initialState): 88 | """Finds n collisions for the given hashing functions. 89 | 90 | - statelen is the length of the hashes. 91 | - n is the number of collisions we want to find. It works better when it's 92 | a power of 2. 93 | """ 94 | # Prepare by generating all possible blocks, i.e. all possible 95 | # byte strings of 16 bytes. We use this inside FindCollisionOneStage, but 96 | # we only generate it once here. 97 | allBytes = range(2 ** 8) 98 | allPossibleBlocks = [] 99 | for comb in itertools.combinations(allBytes, stateLen): 100 | byteString = b''.join(x.to_bytes(1, 'little') for x in comb) 101 | allPossibleBlocks.append(byteString) 102 | 103 | perStageCollisions = [] 104 | currentState = initialState 105 | 106 | # The first step is finding collisions at stage 1. Then the shared hash 107 | # of this collision is used at the next stage, and we find two more colliding 108 | # blocks. Etc... until we have n collisions to return (see below). 109 | # 110 | # perStageCollisions is a list of lists: each list contains two colliding 111 | # blocks for that stage. 112 | while (2 ** len(perStageCollisions)) < n: 113 | nextState, collisions = FindCollisionOneStage(allPossibleBlocks, currentState) 114 | 115 | if not collisions: 116 | raise Exception("No collision found at current stage.") 117 | 118 | perStageCollisions.append(collisions) 119 | currentState = nextState 120 | 121 | collisions = [] 122 | # Now we exploit the main principles here: if block X and Y collide at stage 1, 123 | # and block Z and W collide at stage 2, then each possible concatenation 124 | # (X+Z, X+W, Y+Z, Y+W) is a collision too. So if the per-stage list has n lists, 125 | # we are able to produce 2^n collisions. 126 | # 127 | # This functions makes the concatenations for us and returns a list with all the 128 | # colliding strings. 129 | GenerateAllCollisions(perStageCollisions, collisions) 130 | return collisions 131 | 132 | def VerifyCollisions(collisions, initialState): 133 | """Verifies all the collisions in the input list.""" 134 | sharedH = None 135 | 136 | for c in collisions: 137 | h = MerkleDamgard(padPKCS7(c), initialState) 138 | if sharedH is None: 139 | sharedH = h 140 | elif sharedH != h: 141 | return False 142 | 143 | return True 144 | 145 | if __name__ == '__main__': 146 | stateLen = 2 147 | 148 | collisions = FindCollisions(stateLen, 8, b'I\xbf') 149 | if VerifyCollisions(collisions, b'I\xbf'): 150 | print('[**] First scenario of challenge 52: found several collisions.') 151 | print(collisions) 152 | else: 153 | print('[**] Nope, not real.') 154 | -------------------------------------------------------------------------------- /set7/ch53.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from Crypto.Cipher import AES 3 | from Crypto import Random 4 | from Crypto.Util.strxor import strxor 5 | 6 | import math 7 | 8 | # Set 7, challenge 53: Kelsey and Schneier's expandable messages. 9 | 10 | def padPKCS7(message, k=16): 11 | """Apply PKCS7 padding to a message, if required.""" 12 | if len(message) % k == 0: 13 | return message 14 | 15 | ch = k - (len(message) % k) 16 | return message + bytes([ch] * ch) 17 | 18 | def GetNumBlocks(message): 19 | return math.ceil(len(message) / AES.block_size) 20 | 21 | def GetBlock(message, index): 22 | return message[index * AES.block_size:(index+1) * AES.block_size] 23 | 24 | # The choice of algorithm here is not very important. You can do this 25 | # with any hash function in which the hash is computed from a chain of 26 | # intermediate states. 27 | def CBCMac(message, iv): 28 | cipher = AES.new(b'YELLOW SUBMARINE', AES.MODE_CBC, iv) 29 | ct = cipher.encrypt(padPKCS7(message)) 30 | return ct[-AES.block_size:] 31 | 32 | def CBCMacWithStates(message, iv): 33 | """Computes the CBC-MAC of a message given the IV. 34 | 35 | Returns a tuple with the hash and a list of tuples; the first 36 | element is the block index and the second is the intermediate state 37 | associated with that block, as a byte string. 38 | 39 | It is expected that this returns the same hash as CBC-MAC above 40 | given the same inputs. 41 | """ 42 | M_states = [] 43 | 44 | paddedMessage = padPKCS7(message) 45 | intermediateMessage = b'' 46 | currentState = iv 47 | 48 | for bIndex in range(GetNumBlocks(paddedMessage)): 49 | intermediateMessage = GetBlock(paddedMessage, bIndex) 50 | 51 | cipher = AES.new(b'YELLOW SUBMARINE', AES.MODE_CBC, currentState) 52 | currentState = cipher.encrypt(intermediateMessage) 53 | currentState = currentState[-AES.block_size:] 54 | M_states.append( ( bIndex, currentState ) ) 55 | 56 | return currentState, M_states 57 | 58 | 59 | if __name__ == '__main__': 60 | totalK = 4 61 | iv = Random.new().read(AES.block_size) 62 | currentState = iv 63 | expandableMessages = [] 64 | 65 | # Generates the expandable messages. At each stage we produce one 66 | # single block message and a longer one. We store them as a list 67 | # of tuples. 68 | for k in range(totalK, 0, -1): 69 | singleBlockMessage = Random.new().read(AES.block_size) 70 | singleBlockMac = CBCMac(singleBlockMessage, currentState) 71 | 72 | # If our longer block is T blocks long, we "fix" the first 73 | # T-1 blocks to something random. Then given the hash of 74 | # these dummy blocks and the singleBlockMessage we are able 75 | # to compute the last block such that we have a collision with 76 | # singleBlockMessage itself. 77 | longerMessageBlockSize = (2 ** (k-1)) + 1 78 | longerMessage = Random.new().read( AES.block_size * (2**(k-1)) ) 79 | 80 | dummyMac = CBCMac(longerMessage, currentState) 81 | lastBlock = strxor( strxor(dummyMac, singleBlockMessage), currentState ) 82 | longerMessage += lastBlock 83 | secondMac = CBCMac(longerMessage, currentState) 84 | 85 | # Verify the two blocks collide with each other. 86 | if singleBlockMac != secondMac: 87 | raise Exception('Step k=%d failed' % k) 88 | 89 | print('[**] Step k=%d: success' % k) 90 | print(' Generated message of %d blocks' % (len(longerMessage) / AES.block_size)) 91 | 92 | # Update the state for the next step. 93 | currentState = secondMac 94 | expandableMessages.append( ( singleBlockMessage, longerMessage ) ) 95 | 96 | finalState = currentState 97 | 98 | M = Random.new().read( AES.block_size * (2**totalK) ) 99 | print('[**] Generated random M with %d blocks' % (2**totalK)) 100 | M_hash, M_states = CBCMacWithStates(M, iv) 101 | 102 | bridgeIndex = 13 103 | 104 | # Given the index where we want to place our bridge, compute the actual 105 | # bridge block knowing the input and the state we want to get to. Note 106 | # that we use the previous intermediate state for M to do this. 107 | bridge = strxor( strxor(finalState, M_states[bridgeIndex-1][1]), GetBlock(M, bridgeIndex) ) 108 | remainingBlocks = b'' 109 | # Concatenate the blocks of M we need to append to our forgery. 110 | for i in range(bridgeIndex+1, 2**totalK): 111 | remainingBlocks += GetBlock(M, i) 112 | 113 | # Here I fix bridgeIndex and decided which expandable blocks to use manually so that the 114 | # prefix length was as expected. I use the longer block + shorter + shorter + longer to 115 | # make it more varied. It would be possible to automatically compute the set of blocks 116 | # to use here as an optimization problem, if you want to get fancy :) 117 | forgery = (expandableMessages[0][1] + expandableMessages[1][0] + expandableMessages[2][0] + expandableMessages[3][1] + 118 | bridge + remainingBlocks) 119 | print('[**] Length of the forgery is %d blocks' % (len(forgery) / AES.block_size)) 120 | 121 | if len(forgery) != len(M): 122 | print('!! Error. Message and forgery have different lengths.') 123 | elif forgery == M: 124 | print('!! Error. Generated forgery and M are the same') 125 | else: 126 | forgeryHash = CBCMac(forgery, iv) 127 | 128 | if forgeryHash == M_hash: 129 | print('[**] Success') 130 | else: 131 | print('Failure. Forgery hash was %s, expected %s' % (forgeryHash, M_hash)) 132 | -------------------------------------------------------------------------------- /set6/ch42.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import binascii 4 | import math 5 | import decimal 6 | import rsa 7 | import re 8 | import hashlib 9 | 10 | # Set 6, challenge 42: Bleichenbacher's e=3 RSA Attack. 11 | 12 | # The signatures must contain a standard code identifying the hash algorithm 13 | # used. This is the code for sha256. Source: https://www.ietf.org/rfc/rfc3447.txt 14 | ASN1_GOOP = b'\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20' 15 | 16 | # We need to use large keysizes to have some room for padding, which then becomes 17 | # room for garbage in our forged signature. Remember that the length of the signature 18 | # must be equal to that of the RSA modulus. 19 | KEY_BITSIZE = 2048 20 | KEY_BYTESIZE = 2048 // 8 21 | 22 | def IntegerToBytes(x, expectedLen=None): 23 | """Transforms an integer into the corresponding byte string. 24 | 25 | If expectedLen is not None, the string is padded on the left with 0 bytes 26 | until it reaches the expected length; if it was larger to begin with, an 27 | error is raised. 28 | """ 29 | data = x.to_bytes((x.bit_length() + 7) // 8, 'big') 30 | 31 | if expectedLen is None: 32 | return data 33 | 34 | if len(data) > expectedLen: 35 | raise Exception('Integer too large') 36 | 37 | if len(data) < expectedLen: 38 | data = b'\x00' * (expectedLen - len(data)) + data 39 | 40 | return data 41 | 42 | def BytesToInteger(s): 43 | return int(binascii.hexlify(s), 16) 44 | 45 | def sha256(data): 46 | hasher = hashlib.sha256() 47 | hasher.update(data) 48 | return hasher.digest() 49 | 50 | def CheckSignatureWeak(pubKey, message, signature): 51 | """Weak signature checking.""" 52 | e, n = pubKey 53 | k = KEY_BYTESIZE 54 | 55 | if len(signature) != k: 56 | return False 57 | 58 | s = BytesToInteger(signature) 59 | m = rsa.modexp(s, e, n) 60 | 61 | try: 62 | encodedMessage = IntegerToBytes(m, k) 63 | except Exception as ex: 64 | print('Failed conversion to bytes:', str(ex)) 65 | return False 66 | 67 | H = sha256(message) 68 | # This is probably the most common way this bug can manifest: we have a 69 | # regular expression that only verifies there is at least one '\xff' byte 70 | # in the padding, but does not state how many are expected. Moreover, it 71 | # does not check while this regexp covers the full string, allowing for 72 | # signatures, like ours, with garbage appended after the hash. 73 | if re.match(b'\x00\x01' + b'[\xff]+' + b'\x00' + re.escape(ASN1_GOOP + H), encodedMessage): 74 | return True 75 | 76 | return False 77 | 78 | # Source: https://tools.ietf.org/html/rfc3447#section-9.2 79 | def PKCS1_v1_5_ENCODE(M): 80 | H = sha256(M) 81 | T = ASN1_GOOP + H 82 | 83 | # Pad enough to get to the same length in bytes as the public modulo n. 84 | padding = (KEY_BYTESIZE - len(T) - 3) * b'\xff' 85 | EM = b'\x00\x01' + padding + b'\x00' + T 86 | return EM 87 | 88 | def RSASign(message, privKey): 89 | """Compute a proper signature for the message, with the private key.""" 90 | n, d = privKey 91 | 92 | encodedMessage = PKCS1_v1_5_ENCODE(message) 93 | 94 | # Occasionally the string encoded in s turns out to be 257 bytes, instead 95 | # of the 256 we expect, and this throws an error. I suspect the key pair 96 | # generation is a bit wonky... 97 | m = BytesToInteger(encodedMessage) 98 | s = rsa.Decrypt(privKey, m) 99 | signature = IntegerToBytes(s, KEY_BYTESIZE) 100 | return signature 101 | 102 | def cube_root(x): 103 | """Required because the usual x ^ 1/3 does not work with big integers.""" 104 | decimal.getcontext().prec = 2 * len(str(x)) 105 | power = decimal.Decimal(1) / decimal.Decimal(3) 106 | x = decimal.Decimal(str(x)) 107 | root = x ** power 108 | 109 | integer_root = root.quantize(decimal.Decimal('1.'), rounding=decimal.ROUND_DOWN) 110 | return int(integer_root) 111 | 112 | # Taken from Hal Finney's summary at https://www.ietf.org/mail-archive/web/openpgp/current/msg00999.html, 113 | def ForgeSignature(message, pubKey): 114 | e, n = pubKey 115 | 116 | H = sha256(message) 117 | D = BytesToInteger(ASN1_GOOP + H) 118 | Dbits = (len(H) + len(ASN1_GOOP) + 1) * 8 119 | 120 | # Strangely enough, Finney assumes N to be a power of 3, but here it's not 121 | # and it still works. 122 | N = (2 ** Dbits) - D 123 | # The -4 is to eliminate the bytes that need to be there, 00 01 at the 124 | # start of the signature, and FF 00 just before ASN1_GOOP. 125 | X = (KEY_BYTESIZE - len(H) - len(ASN1_GOOP) - 4) * 8 126 | 127 | # We can fit anything into the X bits leftover for garbage, so we pick the 128 | # largest number we can fit. 129 | garbage = 2 ** X - 1 130 | # In the writeup, the key bit size gets 15 bits removed; here I do the same. 131 | maxBlock = 2 ** (KEY_BITSIZE - 15) - N * (2 ** X) + garbage 132 | 133 | sigNum = cube_root(maxBlock) 134 | signature = IntegerToBytes(sigNum, KEY_BYTESIZE) 135 | return signature 136 | 137 | if __name__ == '__main__': 138 | pubKey, privKey = rsa.GenerateRSAPair(KEY_BITSIZE) 139 | message = b'real message' 140 | signature = RSASign(message, privKey) 141 | 142 | assert CheckSignatureWeak(pubKey, message, signature), 'Real signature did not verify!' 143 | print('[**] Real signature generated and verified correctly.') 144 | 145 | newMessage = b'hi mom' 146 | forgedSig = ForgeSignature(newMessage, pubKey) 147 | assert CheckSignatureWeak(pubKey, newMessage, forgedSig), 'Forged signature did not verify!' 148 | print('[**] Forged signature verified.') 149 | --------------------------------------------------------------------------------