ctf-writeups

stream-cipher-fans-when

Written by virchau13

They don't call it a CHF for nothing

Attachment:

import numpy as np, itertools, random

CHUNK_SIZE = 256
shared_key = np.random.permutation(np.arange(CHUNK_SIZE))

def apply_perm(chunk):
    global shared_key
    assert len(chunk) == CHUNK_SIZE
    return np.array(list(chunk), dtype=np.uint8)[shared_key]

def chf(data):
    state = np.zeros(CHUNK_SIZE, dtype=np.uint8)
    for i in range(0, len(data), CHUNK_SIZE):
        chunk = data[i:i+CHUNK_SIZE]
        chunk += b'\0'*(CHUNK_SIZE-len(chunk))
        state ^= apply_perm(chunk)
    return bytes(state.tolist())

def csprng():
    counter = 0
    while True:
        block = chf((1337*str(counter)).encode())
        yield block
        counter += 1

def encrypt(data):
    for enc_block in csprng():
        plain_block = data[:CHUNK_SIZE]
        if len(plain_block) == 0:
            break
        plain_block += b'\0'*(CHUNK_SIZE - len(plain_block))
        cipher_block = bytes([x^y for x,y in zip(plain_block, enc_block)])
        yield cipher_block
        data = data[CHUNK_SIZE:]

with open('AIW.txt', 'rb') as f:
    aiw = f.read()[random.randint(0, 1000):]

with open('encrypted.bin', 'wb') as f:
    for block in encrypt(aiw):
        f.write(block)

The program generates key using chf and xors with data in AIW.txt, starting from random offset. Since the first key chf(0) is constant regardless of the private key, we can find the correct offset:

CHUNK_SIZE = 256
f = open("encrypted.bin", "rb")
data = f.read()
aiw = open("AIW-truncated.txt", "rb").read()
for offset in range(0, 1000):
    plain_block = aiw[offset : offset + CHUNK_SIZE]
    cipher_block = data[0:CHUNK_SIZE]
    enc_block = bytes([x ^ y for x, y in zip(plain_block, cipher_block)])
    if b"00000000" in enc_block:
        # found correct offset
        print(enc_block, offset)
        break

Then for chf(1) to chf(9), we know that the bytes of the keys can only have two possibilities and their location is the same. Since we know the plaintext, we can deduce chf(1) is:

b'1111111111111\x001111\x001111\x001111\x0011\x001\x001111\x0011\x00\x00\x00\x0011111111\x00111111\x00111111111\x001\x00\x0011111111111111\x00\x00\x
001\x0011111\x00\x0011111\x0011\x001\x00\x001111111111\x00\x00111\x001\x001\x00\x00\x00111111\x00111111111111111111\x001111111\x001\x00\x00\x0011\x0
0111111111\x0011\x00\x00111111\x001111\x00111\x001\x00\x00\x001111111111111\x0011111111\x001111111111\x00111\x0011\x0011\x00\x00\x001'

For chf(10), the key may contain \x00, "1" or "0". So we use known plaintext to recover the key:

j = CHUNK_SIZE * 10
cipher_block = data[j : j + CHUNK_SIZE]
plain_block = b"his time?\xe2\x80\x9d she said aloud. \xe2\x80\x9cI must be\r\ngetting somewhere near the centre of the earth. Let me see: that would\r\nbe four thousand miles down, I think\xe2\x80\x94\xe2\x80\x95 (for, you see, Alice had learnt\r\nseveral things of this sort in her lessons in the schoolroom, and though this"
print(len(plain_block))
recons.write(plain_block)
temp_enc_block = bytes([x ^ y for x, y in zip(plain_block, cipher_block)])
for i in range(len(temp_enc_block)):
    if (
        temp_enc_block[i] != 0
        and temp_enc_block[i] != ord("0")
        and temp_enc_block[i] != ord("1")
    ):
        print(
            i,
            temp_enc_block[i],
            chr(cipher_block[i]),
            chr(cipher_block[i] ^ ord("1")),
            chr(cipher_block[i] ^ ord("0")),
        )
        print(plain_block[i:])
        break
print(temp_enc_block)

By replacing 1 and 0 to other digits, we can find all keys of chf(11) to chf(99):

for i in range(11, 100):
    # 10 -> xy
    last_enc_block = (
        temp_enc_block.replace(b"0", b"x")
        .replace(b"1", b"y")
        .replace(b"x", str(i % 10).encode())
        .replace(b"y", str(i // 10).encode())
    )
    j = CHUNK_SIZE * i
    cipher_block = data[j : j + CHUNK_SIZE]
    plain_block = bytes([x ^ y for x, y in zip(last_enc_block, cipher_block)])
    print(plain_block)
    recons.write(plain_block)

Then, we manually recover chf(100), chf(101) and chf(102) using known plaintext:

j = CHUNK_SIZE * 100
cipher_block = data[j : j + CHUNK_SIZE]
plain_block = b" advisable to go with Edgar Atheling to meet William and offer him\r\nthe crown. William\xe2\x80\x99s conduct at first was moderate. But the insolence\r\nof his Normans\xe2\x80\x94\xe2\x80\x99 How are you getting on now, my dear?\xe2\x80\x9d it continued,\r\nturning to Alice as it spoke.\r\n\r\n\xe2\x80\x9cAs "
print(len(plain_block))
recons.write(plain_block)
temp_enc_block = bytes([x ^ y for x, y in zip(plain_block, cipher_block)])
for i in range(len(temp_enc_block)):
    if (
        temp_enc_block[i] != 0
        and temp_enc_block[i] != 1
        and temp_enc_block[i] != ord("0")
        and temp_enc_block[i] != ord("1")
    ):
        print(
            i,
            temp_enc_block[i],
            repr(chr(cipher_block[i])),
            repr(chr(cipher_block[i] ^ 1)),
            repr(chr(cipher_block[i] ^ ord("1"))),
            repr(chr(cipher_block[i] ^ ord("0"))),
        )
        print(plain_block[i:])
        break
print(temp_enc_block)
enc_block_100 = temp_enc_block

j = CHUNK_SIZE * 101
cipher_block = data[j : j + CHUNK_SIZE]
plain_block = b"wet as ever,\xe2\x80\x9d said Alice in a melancholy tone: \xe2\x80\x9cit doesn\xe2\x80\x99t seem to\r\ndry me at all.\xe2\x80\x9d\r\n\r\n\xe2\x80\x9cIn that case,\xe2\x80\x9d said the Dodo solemnly, rising to its feet, \xe2\x80\x9cI move\r\nthat the meeting adjourn, for the immediate adoption of more energetic\r\nremedies\xe2\x80\x94\xe2\x80\x9d"
print(len(plain_block))
recons.write(plain_block)
temp_enc_block = bytes([x ^ y for x, y in zip(plain_block, cipher_block)])
for i in range(len(temp_enc_block)):
    if (
        temp_enc_block[i] != 0
        and temp_enc_block[i] != 1
        and temp_enc_block[i] != ord("0")
        and temp_enc_block[i] != ord("1")
    ):
        print(
            i,
            cipher_block[i],
            repr(chr(cipher_block[i])),
            repr(chr(cipher_block[i] ^ 1)),
            repr(chr(cipher_block[i] ^ ord("1"))),
            repr(chr(cipher_block[i] ^ ord("0"))),
        )
        print(plain_block[i:])
        break
print(temp_enc_block)
enc_block_101 = temp_enc_block

j = CHUNK_SIZE * 102
cipher_block = data[j : j + CHUNK_SIZE]
plain_block = b"\r\n\r\n\xe2\x80\x9cSpeak English!\xe2\x80\x9d said the Eaglet. \xe2\x80\x9cI don\xe2\x80\x99t know the meaning of half\r\nthose long words, and, what\xe2\x80\x99s more, I don\xe2\x80\x99t believe you do either!\xe2\x80\x9d And\r\nthe Eaglet bent down its head to hide a smile: some of the other birds\r\ntittered audibly.\r\n\r\n\xe2\x80\x9c"
print(len(plain_block))
recons.write(plain_block)
temp_enc_block = bytes([x ^ y for x, y in zip(plain_block, cipher_block)])
for i in range(len(temp_enc_block)):
    if (
        temp_enc_block[i] != 0
        and temp_enc_block[i] != 1
        and temp_enc_block[i] != 2
        and temp_enc_block[i] != 3
        and temp_enc_block[i] != ord("0")
        and temp_enc_block[i] != ord("1")
        and temp_enc_block[i] != ord("2")
        and temp_enc_block[i] != ord("3")
    ):
        print(
            i,
            cipher_block[i],
            repr(chr(cipher_block[i])),
            repr(chr(cipher_block[i] ^ 1)),
            repr(chr(cipher_block[i] ^ 2)),
            repr(chr(cipher_block[i] ^ 3)),
            repr(chr(cipher_block[i] ^ ord("1"))),
            repr(chr(cipher_block[i] ^ ord("0"))),
            repr(chr(cipher_block[i] ^ ord("2"))),
            repr(chr(cipher_block[i] ^ ord("3"))),
        )
        print(plain_block[i:])
        break
print(temp_enc_block)
enc_block_102 = temp_enc_block

The key is a xor-combination of three bytes of str(counter), so we classify each byte of key using chf(100), chf(101) and chf(102):

# classify each byte, 100/101/102 -> xyz, xor between xyz and \x00
res = []
for a, b, c, index in zip(enc_block_100, enc_block_101, enc_block_102, range(256)):
    if a == 0 and b == 1 and c == 2:
        res.append("y xor z")
    elif a == 1 and b == 1 and c == 1:
        res.append("x xor y")
    elif a == 1 and b == 0 and c == 3:
        res.append("x xor z")
    elif a == 49 and b == 48 and c == 51: # 103
        res.append("x xor y xor z")
    else:
        res.append("unknown")
        print(index, ":", a, b, c)

At last, we can recover all chf(103) to chf(999) keys using the information:

for i in range(103, 1000):
    x = str(i // 100).encode()
    y = str((i / 10) % 10).encode()
    z = str(i % 10).encode()
    # 100/101 -> xyz
    last_enc_block = bytearray()
    for type in res:
        if type == "y xor z":
            last_enc_block.append(y[0] ^ z[0])
        elif type == "x xor y":
            last_enc_block.append(x[0] ^ y[0])
        elif type == "x xor z":
            last_enc_block.append(x[0] ^ z[0])
        elif type == "x xor y xor z":
            last_enc_block.append(x[0] ^ y[0] ^ z[0])
        else:
            last_enc_block.append(0)
    j = CHUNK_SIZE * i
    cipher_block = data[j : j + CHUNK_SIZE]
    if len(cipher_block) == 0:
        break
    plain_block = bytes([x ^ y for x, y in zip(last_enc_block, cipher_block)])
    # print(plain_block)
    recons.write(plain_block)

In the reconstructed file, at 2249th row:

“Of course it is,” said the Duchess, who seemed ready to agree to
everything that Alice said; “there’s a large mustard-mine near here.
And the moral of that is—‘The more there is of mine, the less there is
of yours.’”

watctf{https://graydon2.dreamwidth.org/319755.html}

“Oh, I know!” exclaimed Alice, who had not attended to this last
remark, “it’s a vegetable. It doesn’t look like one, but it is.”

The flag is watctf{https://graydon2.dreamwidth.org/319755.html}.

Full code:

CHUNK_SIZE = 256
f = open("encrypted.bin", "rb")
data = f.read()
aiw = open("AIW-truncated.txt", "rb").read()
for offset in range(0, 1000):
    plain_block = aiw[offset : offset + CHUNK_SIZE]
    cipher_block = data[0:CHUNK_SIZE]
    enc_block = bytes([x ^ y for x, y in zip(plain_block, cipher_block)])
    if b"00000000" in enc_block:
        # found correct offset
        print(enc_block, offset)
        break

recons = open("AIW-reconstructed.txt", "wb")
for j in range(0, CHUNK_SIZE * 9, CHUNK_SIZE):
    plain_block = aiw[offset + j : offset + j + CHUNK_SIZE]
    cipher_block = data[j : j + CHUNK_SIZE]
    enc_block = bytes([x ^ y for x, y in zip(plain_block, cipher_block)])
    print(enc_block, plain_block.decode())
    recons.write(plain_block)

j = CHUNK_SIZE * 7
plain_block = aiw[offset + j : offset + j + CHUNK_SIZE]
cipher_block = data[j : j + CHUNK_SIZE]
enc_block = bytes([x ^ y for x, y in zip(plain_block, cipher_block)])
print(enc_block)

last_enc_block = enc_block.replace(b"7", b"8")
j = CHUNK_SIZE * 8
cipher_block = data[j : j + CHUNK_SIZE]
plain_block = bytes([x ^ y for x, y in zip(last_enc_block, cipher_block)])
print(plain_block)

last_enc_block = enc_block.replace(b"7", b"9")
j = CHUNK_SIZE * 9
cipher_block = data[j : j + CHUNK_SIZE]
plain_block = bytes([x ^ y for x, y in zip(last_enc_block, cipher_block)])
print(plain_block)
recons.write(plain_block)

j = CHUNK_SIZE * 10
cipher_block = data[j : j + CHUNK_SIZE]
plain_block = b"his time?\xe2\x80\x9d she said aloud. \xe2\x80\x9cI must be\r\ngetting somewhere near the centre of the earth. Let me see: that would\r\nbe four thousand miles down, I think\xe2\x80\x94\xe2\x80\x95 (for, you see, Alice had learnt\r\nseveral things of this sort in her lessons in the schoolroom, and though this"
print(len(plain_block))
recons.write(plain_block)
temp_enc_block = bytes([x ^ y for x, y in zip(plain_block, cipher_block)])
for i in range(len(temp_enc_block)):
    if (
        temp_enc_block[i] != 0
        and temp_enc_block[i] != ord("0")
        and temp_enc_block[i] != ord("1")
    ):
        print(
            i,
            temp_enc_block[i],
            chr(cipher_block[i]),
            chr(cipher_block[i] ^ ord("1")),
            chr(cipher_block[i] ^ ord("0")),
        )
        print(plain_block[i:])
        break
print(temp_enc_block)

for i in range(11, 100):
    # 10 -> xy
    last_enc_block = (
        temp_enc_block.replace(b"0", b"x")
        .replace(b"1", b"y")
        .replace(b"x", str(i % 10).encode())
        .replace(b"y", str(i // 10).encode())
    )
    j = CHUNK_SIZE * i
    cipher_block = data[j : j + CHUNK_SIZE]
    plain_block = bytes([x ^ y for x, y in zip(last_enc_block, cipher_block)])
    print(plain_block)
    recons.write(plain_block)

j = CHUNK_SIZE * 100
cipher_block = data[j : j + CHUNK_SIZE]
plain_block = b" advisable to go with Edgar Atheling to meet William and offer him\r\nthe crown. William\xe2\x80\x99s conduct at first was moderate. But the insolence\r\nof his Normans\xe2\x80\x94\xe2\x80\x99 How are you getting on now, my dear?\xe2\x80\x9d it continued,\r\nturning to Alice as it spoke.\r\n\r\n\xe2\x80\x9cAs "
print(len(plain_block))
recons.write(plain_block)
temp_enc_block = bytes([x ^ y for x, y in zip(plain_block, cipher_block)])
for i in range(len(temp_enc_block)):
    if (
        temp_enc_block[i] != 0
        and temp_enc_block[i] != 1
        and temp_enc_block[i] != ord("0")
        and temp_enc_block[i] != ord("1")
    ):
        print(
            i,
            temp_enc_block[i],
            repr(chr(cipher_block[i])),
            repr(chr(cipher_block[i] ^ 1)),
            repr(chr(cipher_block[i] ^ ord("1"))),
            repr(chr(cipher_block[i] ^ ord("0"))),
        )
        print(plain_block[i:])
        break
print(temp_enc_block)
enc_block_100 = temp_enc_block

j = CHUNK_SIZE * 101
cipher_block = data[j : j + CHUNK_SIZE]
plain_block = b"wet as ever,\xe2\x80\x9d said Alice in a melancholy tone: \xe2\x80\x9cit doesn\xe2\x80\x99t seem to\r\ndry me at all.\xe2\x80\x9d\r\n\r\n\xe2\x80\x9cIn that case,\xe2\x80\x9d said the Dodo solemnly, rising to its feet, \xe2\x80\x9cI move\r\nthat the meeting adjourn, for the immediate adoption of more energetic\r\nremedies\xe2\x80\x94\xe2\x80\x9d"
print(len(plain_block))
recons.write(plain_block)
temp_enc_block = bytes([x ^ y for x, y in zip(plain_block, cipher_block)])
for i in range(len(temp_enc_block)):
    if (
        temp_enc_block[i] != 0
        and temp_enc_block[i] != 1
        and temp_enc_block[i] != ord("0")
        and temp_enc_block[i] != ord("1")
    ):
        print(
            i,
            cipher_block[i],
            repr(chr(cipher_block[i])),
            repr(chr(cipher_block[i] ^ 1)),
            repr(chr(cipher_block[i] ^ ord("1"))),
            repr(chr(cipher_block[i] ^ ord("0"))),
        )
        print(plain_block[i:])
        break
print(temp_enc_block)
enc_block_101 = temp_enc_block

j = CHUNK_SIZE * 102
cipher_block = data[j : j + CHUNK_SIZE]
plain_block = b"\r\n\r\n\xe2\x80\x9cSpeak English!\xe2\x80\x9d said the Eaglet. \xe2\x80\x9cI don\xe2\x80\x99t know the meaning of half\r\nthose long words, and, what\xe2\x80\x99s more, I don\xe2\x80\x99t believe you do either!\xe2\x80\x9d And\r\nthe Eaglet bent down its head to hide a smile: some of the other birds\r\ntittered audibly.\r\n\r\n\xe2\x80\x9c"
print(len(plain_block))
recons.write(plain_block)
temp_enc_block = bytes([x ^ y for x, y in zip(plain_block, cipher_block)])
for i in range(len(temp_enc_block)):
    if (
        temp_enc_block[i] != 0
        and temp_enc_block[i] != 1
        and temp_enc_block[i] != 2
        and temp_enc_block[i] != 3
        and temp_enc_block[i] != ord("0")
        and temp_enc_block[i] != ord("1")
        and temp_enc_block[i] != ord("2")
        and temp_enc_block[i] != ord("3")
    ):
        print(
            i,
            cipher_block[i],
            repr(chr(cipher_block[i])),
            repr(chr(cipher_block[i] ^ 1)),
            repr(chr(cipher_block[i] ^ 2)),
            repr(chr(cipher_block[i] ^ 3)),
            repr(chr(cipher_block[i] ^ ord("1"))),
            repr(chr(cipher_block[i] ^ ord("0"))),
            repr(chr(cipher_block[i] ^ ord("2"))),
            repr(chr(cipher_block[i] ^ ord("3"))),
        )
        print(plain_block[i:])
        break
print(temp_enc_block)
enc_block_102 = temp_enc_block

# classify each byte, 100/101/102 -> xyz, xor between xyz and \x00
res = []
for a, b, c, index in zip(enc_block_100, enc_block_101, enc_block_102, range(256)):
    if a == 0 and b == 1 and c == 2:
        res.append("y xor z")
    elif a == 1 and b == 1 and c == 1:
        res.append("x xor y")
    elif a == 1 and b == 0 and c == 3:
        res.append("x xor z")
    elif a == 49 and b == 48 and c == 51: # 103
        res.append("x xor y xor z")
    else:
        res.append("unknown")
        print(index, ":", a, b, c)

for i in range(103, 1000):
    x = str(i // 100).encode()
    y = str((i / 10) % 10).encode()
    z = str(i % 10).encode()
    # 100/101 -> xyz
    last_enc_block = bytearray()
    for type in res:
        if type == "y xor z":
            last_enc_block.append(y[0] ^ z[0])
        elif type == "x xor y":
            last_enc_block.append(x[0] ^ y[0])
        elif type == "x xor z":
            last_enc_block.append(x[0] ^ z[0])
        elif type == "x xor y xor z":
            last_enc_block.append(x[0] ^ y[0] ^ z[0])
        else:
            last_enc_block.append(0)
    j = CHUNK_SIZE * i
    cipher_block = data[j : j + CHUNK_SIZE]
    if len(cipher_block) == 0:
        break
    plain_block = bytes([x ^ y for x, y in zip(last_enc_block, cipher_block)])
    # print(plain_block)
    recons.write(plain_block)