Crypto challenges Writeups
Thu Sep 26 2024
记录一下现代密码学课程第二次实验的题解,(主要还是反转课堂演示用
# Part 1
纯纯的读文档题,烂活
根据文档,计算校验位,补全机读区的缺失部分
# %%
import base64
import hashlib
from Crypto.Cipher.AES import new as aes
# %%
encoded = "9MgYwmuPrjiecPMx61O6zIuy3MtIXQQ0E59T3xB6u0Gyf1gYs2i3K9Jxaa0zj4gTMazJuApwd6+jdyeI5iGHvhQyDHGVlAuYTgJrbFDrfB22Fpil2NfNnWFBTXyf7SDI"
ciphertext = base64.b64decode(encoded)
# mrz = "12345678<8<<<1110182<111116?<<<<<<<<<<<<<<<4"
def checksum(ks):
def mapped():
for i, v in enumerate(ks):
o = ord(v)
if 48 <= o <= 57:
yield i, o - 48
if 65 <= o <= 90:
yield i, o - 55
w = [7, 3, 1]
return sum(k * w[i % len(w)] for i, k in mapped()) % 10
assert checksum("111116") == 7
mrz = "12345678<8<<<1110182<1111167<<<<<<<<<<<<<<<4"
根据文档,从机读区计算k_seed
k_seed = hashlib.sha1((mrz[:10] + mrz[13:20] + mrz[21:28]).encode()).digest()[:16]
根据文档,从k_seed
计算 AES key
def gen_key(k_seed, c):
return bytes(
((v & 0b11111110) | ((v & 0b11111110).bit_count() & 1) ^ 1)
for v in hashlib.sha1(k_seed + bytes([0, 0, 0, c])).digest()[:16]
)
k = gen_key(k_seed, 1)
ctx = aes(k, 2, b"\x00" * 16)
print(ctx.decrypt(ciphertext))
# Part 2
## Implement PKCS#7 padding
根据 PKCS#7 标准,直接写
def pad(raw:bytes, block_size:int) -> bytes:
padding = block_size - len(raw) % block_size
return raw + bytes([padding] * padding)
## Implement CBC mode
这里要先实现一个 AES ECB 算法
根据 CBC Mode 的定义,令 AES ECB 加密一个 block 的算法为 transform
,可以得到 CBC Mode 的实现如下
def strxor(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))
def cbc_encrypt(
blocks: list[bytes], iv: bytes, transform: Callable[[bytes], bytes]
) -> list[bytes]:
result = [iv]
for block in blocks:
result.append(transform(strxor(block, result[-1])))
return result
def cbc_decrypt(
blocks: list[bytes], iv: bytes, transform: Callable[[bytes], bytes]
) -> list[bytes]:
result = [strxor(transform(blocks[0]), iv)]
for index, block in enumerate(blocks[1:]):
result.append(strxor(transform(block), blocks[index]))
return result
我们还需要把密文分块
def split_blocks(message, block_size=16):
assert len(message) % block_size == 0
return [message[i:i+16] for i in range(0, len(message), block_size)]
已知 iv 为 b"\x00"*16
,key 为 b"YELLOW SUBMARINE"
,可进行解密:
import base64
from aes import context as aes_context
ciphertext = base64.b64decode("""
/* omitted */
""")
key = b"YELLOW SUBMARINE"
ctx = aes_context(key)
print(b"".join(cbc_decrypt(split_blocks(ciphertext), b"\x00"*16, ctx.decrypt_block)))
## An ECB/CBC detection oracle
先生成随机的 key 和 padding,可以直接用urandom
import os
import random
def random_key():
return os.urandom(16)
def random_padding():
return os.urandom(random.randint(5, 10))
编写一个函数,根据给定的 key 和 msg,在进行随机 padding 之后,随机采用 ECB 或者 CBC 加密
import Crypto.Cipher.AES as AES
import Crypto.Util.Padding as padding
def encryption_oracle(key, msg):
mode = random.choice([AES.MODE_ECB, AES.MODE_CBC])
plaintext = random_padding() + msg + random_padding()
plaintext = padding.pad(plaintext, 16)
match mode:
case AES.MODE_ECB:
return AES.new(key, mode).encrypt(plaintext), mode
case AES.MODE_CBC:
iv = random_key()
return AES.new(key, mode, iv).encrypt(plaintext), mode
assert False, "unreachable"
随机选择一个密钥,再选择一个连续三个块内容一致的明文,用上面的函数加密
key = random_key()
msg = b"\x00" * 16 * 3
encrypted = [encryption_oracle(key, msg) for _ in range(100)]
因为选择的明文块有相同的内容,如果使用 ECB 模式得到的密文也是相同的,而 CBC 模式不会出现该问题,因此可以考虑统计有几个不同的密文块,如果存在相同密文块,则可以断定为 ECB 模式
def detect_mode(ciphertext):
blocks = [ciphertext[i : i + 16] for i in range(0, len(ciphertext), 16)]
if len(blocks) != len(set(blocks)):
return AES.MODE_ECB
return AES.MODE_CBC
accr = sum(detect_mode(ciphertext) == mode for ciphertext, mode in encrypted)
print(f"{accr / len(encrypted):.2%}")
## Byte-at-a-time ECB decryption (Simple)
因为能在未知的明文前面进行填充,那就想办法将第一个未知的字符放在每个块的最后,这样我们就可以构造一个块进行碰撞(由15个已知的字符,和一个位置的字符)
先导入函数,并构造encryption_oracle
import base64
import os
import Crypto.Cipher.AES as AES
import Crypto.Util.Padding as padding
import string
def encryption_oracle(oracle):
key = os.urandom(16)
return AES.new(key, AES.MODE_ECB).encrypt(
padding.pad(
oracle
+ base64.b64decode("""
Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg
aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq
dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg
YnkK"""),
16,
)
)
因为我们不知道未知明文的长度,因此需要爆破明文长度
init_unk_strlen = len(encryption_oracle(b""))
unk_strlen = init_unk_strlen
assert unk_strlen % 16 == 0
for i in range(16):
if len(encryption_oracle(b"A" * i)) != init_unk_strlen:
unk_strlen = init_unk_strlen - i
break
构造明文空间,构造dfs进行搜索,这里不能全部递归,可能会爆栈
plain_space = string.printable.encode()
def search(known):
while True:
partial = known[-15:] # 取明文的最后15字节(可能不足)
partial = b"\x00" * (15 - len(partial)) + partial # 补齐到15字节
current = []
for i in plain_space:
oracle = partial + bytes([i]) + b"\x00" * (15 - len(known) % 16) # 构造 oracle,让第一个未知明文在块的最后一个字节
enc = encryption_oracle(oracle)
if enc[15] == enc[len(known) // 16 * 16 + 31]: # 成功碰撞
current.append(i)
if len(current) == 1:
known += bytes(current) # 不递归,用循环,防止爆栈
print(known)
if len(known) == unk_strlen: # 达到预期长度,成功退出
return True
continue
elif len(current) == 0: # 没有成功碰撞,失败退出
return False
else:
for c in current: # 对每一个碰撞
if search(known + bytes([c])): # 递归搜索
return True # 找到结果就返回
search(b'')
## ECB cut-and-paste
先随便模拟一下题目的环境
import Crypto.Cipher.AES as AES
import Crypto.Util.Padding as padding
key = b"\xc6\xfe\xe2/\x97r|/\xeaY\xc5C\xbfi\x99\x97"
def encrypt(userdata: bytes):
data = (
b"userdata="
+ userdata.replace(b"&", b"").replace(b"=", b"")
+ b"&uid=10&role=user"
)
return AES.new(key, AES.MODE_ECB).encrypt(padding.pad((b"\x00" * 16) + data, 16))
def decrypt(data: bytes):
data = AES.new(key, AES.MODE_ECB).decrypt(data)
data = padding.unpad(data, 16)[16:]
return {
(kv := item.split(b"=", maxsplit=1))[0].decode(): kv[1]
for item in data.split(b"&")
}
def is_admin(data: bytes):
decrypted = decrypt(data)
print(decrypted)
return decrypted.get("role") == b"admin"
因为是 ECB 模式,所以只要复制整个块过去就行,但是要注意 padding
userdata = b"A" * 7 + b"admin" + b"\x0b" * 14
enc = encrypt(userdata)
enc = enc[:64] + enc[32:48]
assert is_admin(enc)
## Byte-at-a-time ECB decryption (Harder)
我们暂时假定这题的意思是,每次实验的prefix_len
随机但固定(不然真没法做了),所以直接求出prefix_len
,然后把prefix
补齐到整块,整个丢掉就好
修改 12 题代码
__MY_SUPER_SECRET_PREFIX_LENGTH__ = random.randint(0, 64)
def encryption_oracle(oracle):
key = os.urandom(16)
rand_pad = os.urandom(__MY_SUPER_SECRET_PREFIX_LENGTH__)
return AES.new(key, AES.MODE_ECB).encrypt(
padding.pad(
rand_pad
+ oracle
+ base64.b64decode("""
Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg
aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq
dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg
YnkK"""),
16,
)
)
求未知部分长度,以及前缀导致的偏移量和需要的补齐长度
def get_unklen():
init_unk_strlen = len(encryption_oracle(b""))
unk_strlen = init_unk_strlen
assert unk_strlen % 16 == 0
for i in range(16):
if len(encryption_oracle(b"A" * i)) != init_unk_strlen:
unk_strlen = init_unk_strlen - i
break
leftlen = 0
while True:
leftlen += 1
enc = encryption_oracle(b"A" * leftlen)
blocks = [enc[i : i + 16] for i in range(0, len(enc), 16)]
for i in range(len(blocks) - 1):
if blocks[i] == blocks[i + 1]:
return unk_strlen - i * 16 + leftlen % 16, i * 16, leftlen % 16
unk_strlen, offset, leftpad = get_unklen()
leftpad = b"\x00" * leftpad
最后把补齐和移除前缀的操作加到search
里面,直接搜就好
plain_space = string.printable.encode()
def search(known):
while True:
partial = known[-15:]
partial = b"\x00" * (15 - len(partial)) + partial
current = []
for i in plain_space:
oracle = leftpad + partial + bytes([i]) + b"\x00" * (15 - len(known) % 16)
enc = encryption_oracle(oracle)[offset:]
if enc[15] == enc[len(known) // 16 * 16 + 31]:
current.append(i)
if len(current) == 1:
known += bytes(current)
print(known)
if len(known) == unk_strlen:
return True
continue
elif len(current) == 0:
return False
else:
for c in current:
if search(known + bytes([c])):
return True
search(b"")
## PKCS#7 padding validation
根据标准实现 PKCS#7 padding
def pad(raw: bytes, block_size: int) -> bytes:
padding = block_size - len(raw) % block_size
return raw + bytes([padding] * padding)
def unpad(data):
padding_len = data[-1]
payload, padding = data[:-padding_len], data[-padding_len:]
assert all(x == padding_len for x in padding)
return payload
随便写一个测试
try:
unpad(pad(b"ICE ICE BABY", 16))
print("Assert passed")
except AssertionError:
print("Assert failed")
try:
unpad(b"ICE ICE BABY\x05\x05\x05\x05")
print("Assert passed")
except AssertionError:
print("Assert failed")
try:
unpad(b"ICE ICE BABY\x01\x02\x03\x04")
print("Assert passed")
except AssertionError:
print("Assert failed")
## CBC bitflipping attacks
经典的比特反转攻击,这里的 key 是写死的,不然没法进行解密
# %%
import Crypto.Cipher.AES as AES
import Crypto.Util.Padding as padding
import os
key = b"\xc6\xfe\xe2/\x97r|/\xeaY\xc5C\xbfi\x99\x97"
def encrypt(userdata: bytes):
data = (
b"comment1=cooking MCs;userdata="
+ userdata.replace(b";", b"%3B").replace(b"=", b"%3D")
+ b";comment2= like a pound of bacon"
)
return AES.new(key, AES.MODE_CBC, os.urandom(16)).encrypt(
padding.pad((b"\x00" * 16) + data, 16)
)
def decrypt(data: bytes):
data = padding.unpad(AES.new(key, AES.MODE_CBC, os.urandom(16)).decrypt(data), 16)[
16:
]
return {
(kv := item.split(b"=", maxsplit=1))[0].decode(): kv[1]
for item in data.split(b";")
}
def is_admin(data: bytes):
decrypted = decrypt(data)
return decrypted.get("admin") == b"true"
由于已知加密后的结构,所以直接对 userdata 进行 padding,然后对 :
(0b111010
) 和 <
(0b111100
) 的最低位进行反转即可
padlen = 18
userdata = b"A" * padlen + b":admin<true"
enc = bytearray(encrypt(userdata))
enc[padlen + 30] ^= 1
enc[padlen + 36] ^= 1
assert is_admin(enc)