import flag import hashcash import secrets from Crypto.Cipher import AES def gf128(a, b): a = int.from_bytes(a, byteorder="big") b = int.from_bytes(b, byteorder="big") R = 128 P = sum(1 << x for x in [R, 7, 2, 1, 0]) r = 0 for i in range(R): if a & (1 << i): r ^= b << i for i in range(R)[::-1]: if r & (1 << (i+R)): r ^= P << i return r.to_bytes(16, byteorder="big") def xor(a, b): return bytes(x^y for x,y in zip(a,b)) class LeakyBlockCipher: def __init__(self, key = None): if key is None: key = secrets.token_bytes(16) self.key = key self.aes = AES.new(key, AES.MODE_ECB) self.H = self.aes.encrypt(bytes(16)) def encrypt(self, iv, data): assert len(iv) == 16 assert len(data) % 16 == 0 ivi = int.from_bytes(iv, "big") cip = bytes() tag = bytes(16) for i in range(0,len(data),16): cntr = ((ivi + i // 16 + 1) % 2**128).to_bytes(16, byteorder="big") block = data[i:i+16] enced = self.aes.encrypt(xor(cntr, block)) cip += enced tag = xor(tag, enced) tag = gf128(tag, self.H) tag = xor(tag, self.aes.encrypt(iv)) return cip, tag def main(): resource = secrets.token_hex(8) print(resource) token = input() assert hashcash.check(token.strip(), resource, bits=21) print("Thanks for helping me try to find this leak.") print("Here's a few rounds of the cipher for you to investigate.") print("") for _ in range(20): G = LeakyBlockCipher() iv = secrets.token_bytes(16) print("iv =", iv.hex()) plaintext = bytes.fromhex(input("plaintext = ")) assert len(plaintext) > 100 cip, tag = G.encrypt(iv, plaintext) print("secure auth tag =", tag.hex()) print("") enc_iv = G.aes.encrypt(iv).hex() print("Have you caught the drip?") print("It looks like ", enc_iv[:-1] + "X") guess = input("So what is X? ").strip() if guess == enc_iv[-1:]: print("Good. Now just to check, do it again for me.") else: print("Sorry, the answer was", enc_iv[-1:]) break else: print(flag.flag) if __name__ == "__main__": main()
overview
20回クエリに答えることができればフラグがもらえる
クエリ
16byteの
iv
が毎回ランダムに生成される(もらえる)100文字以上の
plaintext
を贈り、(iv, plaintext)
が暗号化され、tag
がもらえるiv
をAESで暗号化したenc_iv
の、最後の1バイト4bitであるX
以外
がもらえるので
X
を当てる
ブロック の暗号化( GCM に近いけど違う)
(where )
observation
から、 を求めればよく、 は既知なので、 がわかれば良い?
なので、そうなるように、 を工夫して送れば になる?
- より、 としたければ
[! わからないのでダメだった]
たとえば、 とすると
例えば…… とすると
の末尾4bitだけを求められているのはなぜ? 全体は求められないが4bit程度なら求められるのだろうか……?
冷静に考えて4bitわかるなら5bitもわかりそうだしそれなら8bit 16bitくらいは全探索できるのではという気分になる
solution
として
なので
の正しい値を得られれば が成り立つ
であるはず
上では、 という式が成り立つ
嘘でした
方針
の末尾4bitを探索し、得られた が 乗根を持っていれば( 次剰余であれば?) それは正しい であろう、と判断できる
ただし、 の値が小さい場合には false positive な が探索されうる
の multiplicative order は で、これは などの素因数を持つので を選んで を考えると、この multiplicative order は になる
ので、 を適当に選んだ時、 となるような が存在する確率は
- というのも という射を考えると、位数が と減っているわけだから、写った先に重複がある。ランダムに を選んで、それが の写る先である割合は という話
方針
はできるだけ大きくして、 false positive をなくしたい
は、いつでも必ず 乗根をもつ
一方、誤った の探索結果から得られる は、 のランダムな要素とみなせるので、 のとき、 の確率で 乗根を持つ
implementation
以下を20回やる
iv
をもらえるのでcntr
を計算して、iv + cntr
を 255ブロック送るtag
とenc_iv'
がもらえる4bit 全探索して が 255 乗根を持っているような を見つけ、それを返すと次のステージにいけるかも
[PR.
とintの変換にはinteger_representation / fetch intを使う](</entry/PR.\<x>とintの変換にはinteger_representation / fetch intを使う>)
from ptrlib import Socket sock = Socket("localhost", 19999) R.<x> = GF(2)[] F = GF(2^128, name='a', modulus=x^128+x^7+x^2+x+1) for _ in range(20): iv = int(sock.recvlineafter("iv = "), 16) plaintext = b"" for i in range(255): cntr = (iv + i + 1) % (2**128) plaintext += int(iv ^^ cntr).to_bytes(16, "big") sock.sendlineafter("plaintext = ", plaintext.hex()) tag = F.fetch_int(int(sock.recvlineafter("tag ="), 16)) sock.recvuntil("like ") enc_iv_ = sock.recvuntil("X")[:-1].strip().decode() for x in range(2**4): enc_iv = F.fetch_int(int(enc_iv_ + "{:x}".format(x), 16)) Hn1 = tag / enc_iv try: Hn1.nth_root(255) sock.sendlineafter("So what is X? ", "{:x}".format(x)) print(sock.recvline()) break except ValueError: pass else: raise ValueError("not found") sock.interactive()