1.European ePassport
MTC3 AES key — encoded in the machine readable zone of a European ePassport (link )
题目需要破解一个欧洲护照,图片中给出了护照的一部分内容,和一个未知字符。已知初始化矢量即IV为零,填充为01-00。所以首先我们需要找到缺失的字符是什么。根据规则定义了unknown_number()
函数。
1 2 3 4 5 6 7 def unknown_number (): number = "111116" weight = "731" total = 0 for i in range (len (number)): total += int (number[i]) * int (weight[i % 3 ]) return total % 10
经过调用函数我们找到缺失的字符是7
所以我们能够补齐护照得到12345678<8<<<1110182<1111167<<<<<<<<<<<<<<<4
。然后我们利用这个护照序列
计算出k_seed。
1 2 3 4 def calculate_kseed (): MRZ_information = "12345678<8<<<1110182<1111167<<<<<<<<<<<<<<<4" H_information = hashlib.sha1((MRZ_information[:10 ] + MRZ_information[13 :20 ] + MRZ_information[21 :28 ]).encode()).hexdigest() return H_information[:32 ]
从 K_seed 计算出 Ka 和 Kb
1 2 3 4 def calculate_ka_kb (K_seed ): d = K_seed + "00000001" H_d = hashlib.sha1(binascii.unhexlify(d)).hexdigest() return H_d[:16 ], H_d[16 :32 ]
分别对Ka和Kb进行奇偶校验
1 2 3 4 def parity_check (hex_str ): binary_str = bin (int (hex_str, 16 ))[2 :].zfill(64 ) k_list = [(byte := binary_str[i:i + 7 ]) + ('1' if byte.count('1' ) % 2 == 0 else '0' ) for i in range (0 , len (binary_str), 8 )] return hex (int ('' .join(k_list), 2 ))[2 :].zfill(16 )
得到key之后解密密文删掉填充得到最终答案
1 2 Key: ea8645d97ff725a898942aa280c43179 Decrypted message: Herzlichen Glueckwunsch. Sie haben die Nuss geknackt. Das Codewort lautet: Kryptographie!
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import hashlibimport base64from Crypto.Cipher import AESimport binasciidef pad (text ): padding_len = AES.block_size - len (text) % AES.block_size padding = b'\x01' + b'\x00' * (padding_len - 1 ) return text + padding def unpad (text ): return text.rstrip(b'\x00' ).rstrip(b'\x01' ) def unknown_number (): number = "111116" weight = "731" total = sum (int (number[i]) * int (weight[i % 3 ]) for i in range (len (number))) return total % 10 def calculate_kseed (): MRZ_information = "12345678<8<<<1110182<1111167<<<<<<<<<<<<<<<4" H_information = hashlib.sha1((MRZ_information[:10 ] + MRZ_information[13 :20 ] + MRZ_information[21 :28 ]).encode()).hexdigest() return H_information[:32 ] def calculate_ka_kb (K_seed ): d = K_seed + "00000001" H_d = hashlib.sha1(binascii.unhexlify(d)).hexdigest() return H_d[:16 ], H_d[16 :32 ] def parity_check (hex_str ): binary_str = bin (int (hex_str, 16 ))[2 :].zfill(64 ) k_list = [(byte := binary_str[i:i + 7 ]) + ('1' if byte.count('1' ) % 2 == 0 else '0' ) for i in range (0 , len (binary_str), 8 )] return hex (int ('' .join(k_list), 2 ))[2 :].zfill(16 ) def decrypt_message (encrypted_text ): K_seed = calculate_kseed() ka, kb = calculate_ka_kb(K_seed) key = parity_check(ka) + parity_check(kb) print (f"Key: {key} " ) ciphertext = base64.b64decode(encrypted_text) IV = '0' * 32 cipher = AES.new(binascii.unhexlify(key), AES.MODE_CBC, binascii.unhexlify(IV)) decrypted_padded = cipher.decrypt(ciphertext) decrypted_message = unpad(decrypted_padded).decode('utf-8' , errors='ignore' ) print (f"Decrypted message: {decrypted_message} " ) if __name__ == "__main__" : print (unknown_number()) encrypted_text = '9MgYwmuPrjiecPMx61O6zIuy3MtIXQQ0E59T3xB6u0Gyf1gYs2i3K9Jxaa0zj4gTMazJuApwd6+jdyeI5iGHvhQyDHGVlAuYTgJrbFDrfB22Fpil2NfNnWFBTXyf7SDI' decrypt_message(encrypted_text)
2. Crypto Challenge Set 2
(1) Implement PKCS#7 padding
如题目所述就是应用PKCS#7这种填充方法
1 2 3 4 def pkcs7_pad (text, block_size ): padding_len = block_size - (len (text) % block_size) padding = bytes ([padding_len] * padding_len) return text + padding
(2) Implement CBC mode
首先需要按照题目所述写一个ECB函数如下
1 2 3 def ecb_decrypt (cipher_text, key ): cipher = AES.new(key, AES.MODE_ECB) return cipher.decrypt(cipher_text)
因为这个是基于CBC的密码所以说要写一个解密CBC的函数并且输出去除PKCS#7填充的明文
1 2 3 4 5 6 7 8 9 10 11 12 def cbc_decrypt (cipher_text, key, iv ): block_size = len (key) plain_text = b'' previous_block = iv for i in range (0 , len (cipher_text), block_size): block = cipher_text[i:i + block_size] decrypted_block = ecb_decrypt(block, key) decrypted_block = xor_bytes(decrypted_block, previous_block) plain_text += decrypted_block previous_block = block return pkcs7_unpad(plain_text)
最后写出我们的 main
函数运算,输入已知的key'YELLOW SUBMARINE'
和16位空IV
以及读取密文文件。
1 2 3 4 5 6 7 if __name__ == "__main__" : key = b'YELLOW SUBMARINE' iv = b'\x00' * 16 with open ("2_2.txt" ,'r' ) as f: ciphertext = base64.b64decode(f.read()) plain_text = cbc_decrypt(ciphertext, key, iv) print (f"plaintext: {plain_text.decode()} " )
通过调用函数能够得到最终答案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 I'm back and I'm ringin' the bell A rockin' on the mike while the fly girls yell In ecstasy in the back of me Well that's my DJ Deshay cuttin' all them Z's Hittin' hard and the girlies goin' crazy Vanilla's on the mike, man I'm not lazy. I'm lettin' my drug kick in It controls my mouth and I begin To just let it flow, let my concepts go My posse's to the side yellin', Go Vanilla Go! Smooth 'cause that's the way I will be And if you don't give a damn, then Why you starin' at me So get off 'cause I control the stage There's no dissin' allowed I'm in my own phase The girlies sa y they love me and that is ok And I can dance better than any kid n' play Stage 2 -- Yea the one ya' wanna listen to It's off my head so let the beat play through So I can funk it up and make it sound good 1-2-3 Yo -- Knock on some wood For good luck, I like my rhymes atrocious Supercalafragilisticexpialidocious I'm an effect and that you can bet I can take a fly girl and make her wet. I'm like Samson -- Samson to Delilah There's no denyin', You can try to hang But you'll keep tryin' to get my style Over and over, practice makes perfect But not if you're a loafer. You'll get nowhere, no place, no time, no girls Soon -- Oh my God, homebody, you probably eat Spaghetti with a spoon! Come on and say it! VIP. Vanilla Ice yep, yep, I'm comin' hard like a rhino Intoxicating so you stagger like a wino So punks stop trying and girl stop cryin' Vanilla Ice is sellin' and you people are buyin' 'Cause why the freaks are jockin' like Crazy Glue Movin' and groovin' trying to sing along All through the ghetto groovin' this here song Now you're amazed by the VIP posse. Steppin' so hard like a German Nazi Startled by the bases hittin' ground There's no trippin' on mine, I'm just gettin' down Sparkamatic, I'm hangin' tight like a fanatic You trapped me once and I thought that You might have it So step down and lend me your ear '89 in my time! You, '90 is my year. You're weakenin' fast, YO! and I can tell it Your body's gettin' hot, so, so I can smell it So don't be mad and don't be sad 'Cause the lyrics belong to ICE, You can call me Dad You're pitchin' a fit, so step back and endure Let the witch doctor, Ice, do the dance to cure So come up close and don't be square You wanna battle me -- Anytime, anywhere You thought that I was weak, Boy, you're dead wrong So come on, everybody and sing this song Say -- Play that funky music Say, go white boy, go white boy go play that funky music Go white boy, go white boy, go Lay down and boogie and play that funky music till you die. Play that funky music Come on, Come on, let me hear Play that funky music white boy you say it, say it Play that funky music A little louder now Play that funky music, white boy Come on, Come on, Come on Play that funky music
(((貌似是一段歌词
(3) An ECB/CBC detection oracle
按照题目意思首先生成随机AES密钥。
1 2 def generate_random_aes_key (): return os.urandom(16 )
之后创建一个加密oracle,在明文之前和之后附加随机字节,然后使用ECB或CBC模式随机加密。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def encryption_oracle (input_data ): key = generate_random_aes_key() prepend = os.urandom(random.randint(5 , 10 )) append = os.urandom(random.randint(5 , 10 )) plain_text = prepend + input_data + append if random.randint(0 , 1 ) == 0 : cipher = AES.new(key, AES.MODE_ECB) padded_text = pkcs7_pad(plain_text, AES.block_size) encrypted = cipher.encrypt(padded_text) mode = "ECB" else : iv = os.urandom(AES.block_size) encrypted = cbc_encrypt(plain_text, key, iv) mode = "CBC" return encrypted, mode
最后编写一个函数来检测使用的加密模式是ECB还是CBC,这里用在ECB模式下,相同的明文块会生成相同的密文块而CBC模式下,每个明文块在加密前会与前一个密文块进行异或操作,使得相同的明文块生成不同的密文块。所以运用这个不同来判断是什么模式。
1 2 3 4 5 6 7 def detect_encryption_mode (encrypted_data ): block_size = AES.block_size blocks = [encrypted_data[i:i + block_size] for i in range (0 , len (encrypted_data), block_size)] if len (set (blocks)) != len (blocks): return "ECB" else : return "CBC"
最后调用main
函数,这里的明文是三块一样的就可以用上吗的方法判断了。
1 2 3 4 5 6 7 8 if __name__ == "__main__" : key = b'YELLOW SUBMARINE' iv = b'\x00' * 16 * 3 plain_text = b"\xFF" * 16 * 3 encrypted, mode = encryption_oracle(plain_text) detected_mode = detect_encryption_mode(encrypted) print (f"Actual mode: {mode} " ) print (f"Detected mode: {detected_mode} " )
(4) Byte-at-a-time ECB decryption (Simple)
就是攻击函数就是写一堆A然后把不知道的放在最后,像这样“AAAT”A已知T未知的数据块进行加密,并将得到的密文存储。然后,你向oracle请求加密“AAAi”的数据块识别第一个目标字节,当返回的密文与之前存储密文相同时,知道字节i就是目标字节T。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import base64from Crypto import Randomfrom Crypto.Cipher import AESUNKNOWN_STRING = base64.b64decode( b"Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg" b"aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq" b"dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg" b"YnkK" ) KEY = Random.new().read(16 ) def pad (data, block_size=16 ): padding_len = block_size - len (data) % block_size return data + bytes ([padding_len] * padding_len) def encryption_oracle (your_string ): plaintext = pad(your_string + UNKNOWN_STRING) cipher = AES.new(KEY, AES.MODE_ECB) return cipher.encrypt(plaintext) def detect_block_size (): initial_len = len (encryption_oracle(b"" )) for i in range (1 , 256 ): data = b"A" * i new_len = len (encryption_oracle(data)) if new_len != initial_len: return new_len - initial_len def detect_mode (cipher ): block_size = 16 blocks = [cipher[i:i + block_size] for i in range (0 , len (cipher), block_size)] return "ECB" if len (blocks) > len (set (blocks)) else "not ECB" def ecb_decrypt (block_size ): known_bytes = b"" while True : block_index = len (known_bytes) // block_size block_offset = block_size - 1 - (len (known_bytes) % block_size) prefix = b"A" * block_offset target_block = encryption_oracle(prefix)[: (block_index + 1 ) * block_size] found = False for i in range (256 ): guess = prefix + known_bytes + bytes ([i]) if encryption_oracle(guess)[: (block_index + 1 ) * block_size] == target_block: known_bytes += bytes ([i]) found = True break if not found: print (f"Decrypted text: {known_bytes.decode('ascii' , errors='ignore' )} " ) return def main (): block_size = detect_block_size() print (f"Detected block size: {block_size} " ) cipher = encryption_oracle(b"A" * 50 ) mode = detect_mode(cipher) print (f"Detected mode: {mode} " ) ecb_decrypt(block_size) if __name__ == "__main__" : main()
运行结果
1 2 3 4 Rollin' in my 5.0 With my rag-top down so my hair can blow The girlies on standby waving just to say hi Did you stop? No, I just drove by
(5) ECB cut-and-paste
这个题就是读取这个信息然后能够把有用的信息提出来比如email、uid等信息
我在这里设置了自动生成的密钥,在给定信息后加密、解密、然后打印识别的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import refrom Crypto.Cipher import AESfrom Crypto.Random import get_random_bytesdef parse_kv (kv_string ): return dict (pair.split('=' ) for pair in kv_string.split('&' )) def profile_for (email ): email = re.sub(r'[&=]' , '' , email) profile = { 'email' : email, 'uid' : 10 , 'role' : 'user' } return f"email={profile['email' ]} &uid={profile['uid' ]} &role={profile['role' ]} " def pad (data ): block_size = 16 padding = block_size - len (data) % block_size return data + bytes ([padding] * padding) def unpad (data ): padding = data[-1 ] return data[:-padding] def generate_key (): return get_random_bytes(16 ) def encrypt_profile (profile, key ): cipher = AES.new(key, AES.MODE_ECB) padded_profile = pad(profile.encode()) return cipher.encrypt(padded_profile) def decrypt_profile (ciphertext, key ): cipher = AES.new(key, AES.MODE_ECB) decrypted = unpad(cipher.decrypt(ciphertext)) return decrypted.decode() def create_admin_profile (): key = generate_key() email1 = "foo@bar.com" email2 = "foo@bar.comadmin" + "\x0b" * 11 encrypted1 = encrypt_profile(profile_for(email1), key) encrypted2 = encrypt_profile(profile_for(email2), key) crafted_ciphertext = encrypted1[:32 ] + encrypted2[16 :32 ] decrypted_profile = decrypt_profile(crafted_ciphertext, key) return parse_kv(decrypted_profile) if __name__ == "__main__" : print (parse_kv("foo=bar&baz=qux&zap=zazzle" )) print (profile_for("foo@bar.com" )) key = generate_key() encrypted = encrypt_profile(profile_for("foo@bar.com" ), key) print ('---------------------------------' ) print (decrypt_profile(encrypted, key)) print (create_admin_profile())
输出
1 2 3 4 5 {'foo': 'bar', 'baz': 'qux', 'zap': 'zazzle'} email=foo@bar.com&uid=10&role=user --------------------------------- email=foo@bar.com&uid=10&role=user {'email': 'foo@bar.com', 'uid': '10', 'role': 'usmadmi'}
(6) Byte-at-a-time ECB decryption (Harder)
随机前缀的长度是不确定的,所以说我们首先需要确定前缀的长度。
由于有一个随机前缀,在逐渐增加输入的过程中,某一段的密文会变得稳定,这表示该块已完全由前缀和我们构造的输入组成。然后其他的和前面那个simple是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 import osimport randomfrom random import randintimport base64from Crypto.Cipher import AESfrom Crypto.Util import Paddingkey = os.urandom(16 ) prefix = os.urandom(randint(1 , 15 )) target = base64.b64decode( "Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg" "aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq" "dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg" "YnkK" ) def encrypt (message ): plaintext = Padding.pad(prefix + message + target, 16 ) cipher = AES.new(key, AES.MODE_ECB) return cipher.encrypt(plaintext) previous_length = len (encrypt(b'' )) for i in range (20 ): length = len (encrypt(b'X' * i)) if length != previous_length: block_size = length - previous_length size_prefix_plus_target_aligned = previous_length min_known_ptxt_size_to_align = i break else : raise Exception('did not detect any change in ciphertext length' ) assert block_size == 16 def split_bytes_in_blocks (data, block_size ): return [data[i:i + block_size] for i in range (0 , len (data), block_size)] previous_blocks = None for i in range (1 , block_size + 1 ): blocks = split_bytes_in_blocks(encrypt(b'X' * i), block_size) if previous_blocks is not None and blocks[0 ] == previous_blocks[0 ]: prefix_size = block_size - i + 1 break previous_blocks = blocks else : raise Exception('did not detect constant ciphertext block' ) assert prefix_size == len (prefix)target_size = size_prefix_plus_target_aligned - min_known_ptxt_size_to_align - prefix_size assert target_size == len (target)known_target_bytes = b"" for _ in range (target_size): r = prefix_size k = len (known_target_bytes) padding_length = (-k - 1 - r) % block_size padding = b"X" * padding_length target_block_number = (k + r) // block_size target_slice = slice (target_block_number * block_size, (target_block_number + 1 ) * block_size) target_block = encrypt(padding)[target_slice] for i in range (256 ): message = padding + known_target_bytes + bytes ([i]) block = encrypt(message)[target_slice] if block == target_block: known_target_bytes += bytes ([i]) break print (known_target_bytes.decode())
答案是
1 2 3 4 Rollin' in my 5.0 With my rag-top down so my hair can blow The girlies on standby waving just to say hi Did you stop? No, I just drove by
(7) PKCS#7 padding validation
这个就写几个PKCS#7的测试就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def validate_and_strip_pkcs7_padding (plaintext ): if not plaintext: raise ValueError("The plaintext is empty" ) padding_value = plaintext[-1 ] if padding_value < 1 or padding_value > 16 : raise ValueError("Invalid padding value" ) if plaintext[-padding_value:] != bytes ([padding_value]) * padding_value: raise ValueError("Invalid PKCS#7 padding" ) return plaintext[:-padding_value] try : result = validate_and_strip_pkcs7_padding(b"ICE ICE BABY\x04\x04\x04\x04" ) print (result.decode()) except ValueError as e: print (e) try : result = validate_and_strip_pkcs7_padding(b"ICE ICE BABY\x05\x05\x05\x05" ) print (result.decode()) except ValueError as e: print (e) try : result = validate_and_strip_pkcs7_padding(b"ICE ICE BABY\x01\x02\x03\x04" ) print (result.decode()) except ValueError as e: print (e)
(8) CBC bit flipping attacks
我们要让;admin=true;
出现在消息中但是不可以用";“或”=",所以说设置sanitize_input
函数将输入中的 ‘;’ 和 ‘=’ 字符转义成对应的16进制表示,防止用户直接输入;admin=true;
。
1 2 def sanitize_input (userdata ): return re.sub(r'[;=]' , lambda x: f"%{ord (x.group(0 )):02x} " , userdata)
然后接受用户的输入数据,首先将其转义,然后将它嵌入到一个固定的前缀和后缀之间,组成完整的明文。最后,该明文经过AES CBC
模式加密后返回密文。
1 2 3 4 5 6 7 8 9 10 def encrypt (userdata ): prefix = b"comment1=cooking%20MCs;userdata=" suffix = b";comment2=%20like%20a%20pound%20of%20bacon" sanitized_userdata = sanitize_input(userdata.decode()).encode() plaintext = prefix + sanitized_userdata + suffix padded_plaintext = pad(plaintext) cipher = AES.new(KEY, AES.MODE_CBC, IV) ciphertext = cipher.encrypt(padded_plaintext) print (f"Plaintext: {plaintext} " ) return ciphertext
生成一个初始的密文16个字母A。定位我们希望进行位翻转的区域,通过亦或运算操作修改密文的第二个块这将影响解密出的第三个块的明文。而我们希望将明文中的16个字母A变成 ;admin=true;
所以编写翻转攻击函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def bitflipping_attack (): userdata = b"A" * 16 ciphertext = encrypt(userdata) print (f"Ciphertext: {ciphertext} " ) block_size = 16 modified_ciphertext = bytearray (ciphertext) target = b";admin=true;" for i in range (len (target)): modified_ciphertext[block_size + i] ^= ord ('A' ) ^ target[i] return decrypt(bytes (modified_ciphertext))
最后运行出来的答案
1 2 3 4 Plaintext: b'comment1=cooking%20MCs;userdata=AAAAAAAAAAAAAAAA;comment2=%20like%20a%20pound%20of%20bacon' Ciphertext: b"\x1e^H\xec\xfen\xa8\x90\xf0\xae&j\x9a<\x1a\x03\xc7!4\x7f[\xce\xf5\xcc~GC)\xaeA\x15\xd2\xd0\xefz\xdf##\x11\xaf\xdaOP\x1albKi\xc4\x80#\x00\xa1\x18s\xf2\xec%\x11\x0f\xba'\xf6\xca\x16e\xd6\xddxJU\xf1\x1c\xda\x8b\xado\xe5\n\xfa\x83'1\x01\xe8;\x0e\n\x84/\xbf\x14\xf9\x93\x1f\xac" Decrypted: b'comment1=cooking\x19yD\xf4*p\xd9B\xd1\xde\x08#\x8cb\x9b\xaf;admin=true;AAAA;comment2=%20like%20a%20pound%20of%20bacon' Bitflipping attack successful: True
实验总结
在这次密码实验中,我实现了多个重要的加密功能和攻击,包括 PKCS#7 填充 、ECB/CBC 检测 、逐字节 ECB 解密(简单与复杂版) 、以及 CBC 位翻转攻击 等多个实验。通过这些实验,我对加密算法AES运用更加熟练,也了解到CBC模式下的位翻转攻击以及ECB模式的可预测性也就是漏洞。我知道了,加密不仅要保证数据的机密性,还必须结合完整性验证等手段,才能有效防御攻击者的篡改和攻击。
源代码链接
https://github.com/cool-chicken/cryptography-exp/tree/main/密码学实验二