# https://en.wikipedia.org/wiki/Salsa20#ChaCha20_adoption from Crypto.Util.number import long_to_bytes, bytes_to_long import secrets def ROTL(a, b): return (((a) << (b)) | ((a % 2**32) >> (32 - (b)))) % 2**32 def qr(x, a, b, c, d): x[a] += x[b]; x[d] ^= x[a]; x[d] = ROTL(x[d],16) x[c] += x[d]; x[b] ^= x[c]; x[b] = ROTL(x[b],12) x[a] += x[b]; x[d] ^= x[a]; x[d] = ROTL(x[d], 8) x[c] += x[d]; x[b] ^= x[c]; x[b] = ROTL(x[b], 7) ROUNDS = 20 def chacha_block(inp): x = list(inp) for i in range(0, ROUNDS, 2): qr(x, 0, 4, 8, 12) qr(x, 1, 5, 9, 13) qr(x, 2, 6, 10, 14) qr(x, 3, 7, 11, 15) qr(x, 0, 5, 10, 15) qr(x, 1, 6, 11, 12) qr(x, 2, 7, 8, 13) qr(x, 3, 4, 9, 14) return [(a+b) % 2**32 for a, b in zip(x, inp)] def chacha_init(key, nonce, counter): assert len(key) == 32 assert len(nonce) == 8 state = [0 for _ in range(16)] state[0] = bytes_to_long(b"expa"[::-1]) state[1] = bytes_to_long(b"nd 3"[::-1]) state[2] = bytes_to_long(b"2-by"[::-1]) state[3] = bytes_to_long(b"te k"[::-1]) key = bytes_to_long(key) nonce = bytes_to_long(nonce) for i in range(8): state[i+4] = key & 0xffffffff key >>= 32 state[12] = (counter >> 32) & 0xffffffff state[13] = counter & 0xffffffff state[14] = (nonce >> 32) & 0xffffffff state[15] = nonce & 0xffffffff return state state = chacha_init(secrets.token_bytes(32), secrets.token_bytes(8), 0) buffer = b"" def encrypt(data): global state, buffer output = [] for b in data: if len(buffer) == 0: buffer = b"".join(long_to_bytes(x).rjust(4, b"\x00") for x in state) state = chacha_block(state) output.append(b ^ buffer[0]) buffer = buffer[1:] return bytes(output) flag = b"fake_flag{FAKE_FLAG}" if __name__ == "__main__": print("""This cipher is approved by Disk Jockey B. 1. Encrypt input 2. Encrypt flag """) while True: inp = input("> ") match inp: case '1': print(encrypt(input("? ").encode()).hex()) case '2': print(encrypt(flag).hex()) case _: print("Bye!") exit()
ChaCha20 が実装されている
無限回次のことができる
任意のテキストを暗号化してもらう
フラグを暗号化してもらう
ChaCha20のことは全然しらないのでコードを読んでわかったことをメモ(したがって、ChaCha20の設定とこの問題の設定が混ざってそう)
- ストリーム暗号
である。
buffer
という変数がストリーム鍵になっている。buffer
が尽きたらstate
を新たなbufferとし、state
自体はchacha_block(state)
で更新する
- ストリーム暗号
である。
明らかに
buffer = state
となっているのが脆弱で、知っている平文を暗号化してもらうことで、state
を知ることができる
from ptrlib import Socket, xor, chunks from Crypto.Util.number import long_to_bytes from chal import chacha_block sock = Socket("nc babycha.chal.irisc.tf 10100") # Encrypt 64 bytes of known plaintext to get the buffer known_plaintext = b"A" * 64 sock.sendlineafter("> ", "1") sock.sendlineafter("? ", known_plaintext) ciphertext = bytes.fromhex(sock.recvline().strip().decode()) # Calculate current state from buffer buffer = xor(known_plaintext, ciphertext) prev_state = [int.from_bytes(x, "big") for x in chunks(buffer, 4)] state = chacha_block(prev_state) next_buffer = b"".join(long_to_bytes(x).rjust(4, b"\x00") for x in state) # Get the encrypted flag and decrypt it (because we know the state) sock.sendlineafter("> ", "2") flag = bytes.fromhex(sock.recvline().strip().decode()) print(xor(flag, next_buffer))