babycha | IrisCTF 2024

#IrisCTF2024

# 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))