[CTF Writeup / Crypto] IrisCTF 2024 - Baby Charge

Baby Charge

For your first assignment, break ChaCha20!


nc 서버가 제공되고, 해당 서버의 로직이 담긴 chal.py가 제공된다. 다음은 chal.py 이다.

# 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 알고리즘이라고 하는데, 자세한건 잘 모르겠다. 여기서 주의깊게 보아야 할 부분은 chacha_init() 함수다. 처음 서버에 요청하게되면 state 초반부가 고정이다. 그리고 암호화 시 생성되는 buffer를 출력해보면 중간에 0이 고정이다.


[ # first run
1634760805, 857760878, 2036477234, 1797285236,
3991586989, 2077829313, 4049756097, 2298079012,
4113014344, 3103725978, 1652622192, 1727789229,
0, 0, 3902470312, 325314252
]

[ # second run
1634760805, 857760878, 2036477234, 1797285236,
3096644661, 3090121645, 1525883431, 1596624440,
252041209, 1265235606, 721901306, 1094568409,
0, 0, 959071436, 1213286989
]


위 배열은 xor 암호화에 사용되는 buffer의 요소를 출력해봤다. 초반 배열도 고정이지만, 뒷부분 0으로 고정되는 부분이 두 군데 존재한다. xor 연산이므로 0을 이용하면 편하다. 각 요소는 4byte씩 변환되며, 부족한 부분은 0x00으로 padding 된다.



def get_flag_8bytes(index):
x = remote('babycha.chal.irisc.tf', 10100)

dummy = "*" * (4 * 12 - index)
x.sendlineafter(b'> ', b'1')
x.sendlineafter(b'? ', dummy.encode())

x.sendlineafter(b'> ', b'2')
enc_flag = x.recvline()

base = index*2
enc_flag = enc_flag[base:base+16]
flag = int(enc_flag, 16)

x.close()
return long_to_bytes(flag).decode()


위와 같이 원하는 위치부터 8bytes를 가져오는 코드를 작성했다. buffer가 0이 되는 곳까지는 padding을 encrypt 해서 buffer 앞 부분을 치우고, 적절한 위치에서 flag를 encrypt 하면 xor 0 연산으로 평문 그대로 출력된다.



flag = ''
for i in range(0, 40, 8):
flag += get_flag_8bytes(i)
print(flag)


앞서 만든 함수를 이용해서 8bytes 씩 leak 하면 flag를 구할 수 있다.




irisctf{initialization_is_no_problem}



댓글