IrisCTF 2024
题面:
题目:
# 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
#核心加密操作 对状态数组x的四个部分进行加密
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)
#对输入的16位状态数组inp执行20轮加密操作
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:
#rjust字符串向右对齐,确保每个字节串都有4个字节 不足的部分用b"\x00"填充
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加密流程 异或
解题:
首先看到题目第一行的注释提示# https://en.wikipedia.org/wiki/Salsa20#ChaCha20_adoption
得知这是一个基于Chacha算法的加密流程,那么先对该算法进行学习
该算法相比于AES的流程简单很多,其效率也会提高很多,类似于对称的流密码,每一个数据位单独进行加密操作
对于该算法流程的学习,强烈推荐这个视频
针对这个题目中给出的代码,主要分为三步走操作
一、初始化状态矩阵
Conest | Conest | Conest | Conest |
---|---|---|---|
Key0 | Key1 | Key2 | Key3 |
Key4 | Key5 | Key6 | Key7 |
Counter | Counter | nonce | nonce |
二、状态矩阵变换
对矩阵中指定位置进行变换 每一次转两轮 一共十次二十轮
每一次中分别进行Row Transform
和 Diagonal Transform
三、旋转变换流程

而把加密过程封装 对于明文进行加密的流程如下:
下面这个图是整个chacha算法的加密流程 通过密钥生成 获得一系列的密钥 与明文异或 获得密文
其中:
numbver used once
: nonce
block number
: counter
该题中最终的结果是异或产生,所以不需要对于原始的加密程序进行逆操作,而是利用其对称性
因为题目可以获得任意输入的加密密文
output.append(b ^ buffer[0])
buffer = buffer[1:]
缓冲区每次清除一位 总长为64位
下面这个图便于理解:
因为initstate中存在随机数 我们没法直接获得 所以进行逆推
第一步: 恢复init_state
传入64个a
之所以选择64首先是避免buffer对下一次的影响 其次是因为每次的状态生成的buffer正好是64位
mes : 自己可控输入的64个a
enc : 服务器自己进行加密返回的密文
第二步: 按照流程跑对称加密 从而实现解密
拿到init_state后面就全是固定的了,这套加密体系就相当于已经被破解了,在本地放上init_state跑一次获得state1,或者直接跑一次chacha_block
获得state1效果都是相同的,然后把flag的enc作为明文放进encrypt
函数即可解密成功获得flag!
exp1: 手工nc
#恢复初始状态
hex = '001119045241050f18034c530a41041547708dd0eb535ac585e17dbcd9d399717f671bfa9593c1b14524575aa3f8ff916161616161616161040095799828fac9'
flag = []
new = bytes.fromhex(hex)
print('new',new)
# j = 0
# print(new)
data = "a" * 64
data = data.encode().hex()
data = bytes.fromhex(data)
for i in new:
flag.append(i ^ data[0])
print(bytes(flag))
print('init_state = ',flag)
buffer_init = flag
buffer_init = bytes(buffer_init)
#bytes_to_long(buffer[i:i+4])
# state = [print(buffer[i:i+4]) for i in range(0,64,4)]
state = [bytes_to_long(buffer_init[i:i+4]) for i in range(0,64,4)]
print(state)
#初始state
state = [1634760805, 857760878, 2036477234, 1797285236, 638708913, 2318547876, 3833601245, 3098736656, 503741083, 4109541584, 608515643, 3264847600, 0, 0, 1700918296, 4182350760]
#旋转 获得下一次的state1
state = chacha_block(state)
print('s1=',state)
# encrypt(b'a'*64) 或这一句 效果一样 都是为了旋转init_state得到state1
flag_enc = '505d7afaa6844f5fc77d00881ad51a5a8a32594171efb706e8d13e7aee6ad91846677ef934'
new = bytes.fromhex(flag_enc)
print(len(data),encrypt(new))
#64 b'irisctf{initialization_is_no_problem}'
exp2: 跟三顺七师傅学到的手法!直接用pwn包里的函数进行交互
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)
# io = process(['python3','chal.py'])
#远程连接
io = remote('babycha.chal.irisc.tf',10100)
def xor(a, b):
return bytes(x^y for x,y in zip(a,b))
payload = b'1'*64
#在>后面输入1
io.sendlineafter(b'>',b'1')
io.sendlineafter(b'?',payload)
tmp = bytes.fromhex(io.recvline().decode())
t = xor(b'1'*64,tmp)
state = [bytes_to_long(t[i:i+4]) for i in range(0,64,4)]
print(state)
io.sendlineafter(b'>',b'2')
enc = bytes.fromhex(io.recvline().decode())
#本地使用的state是刚刚恢复的!
encrypt(b'1'*64)
flag = encrypt(enc)
print(flag)
io.close()
#result
[+] Opening connection to babycha.chal.irisc.tf on port 10100: Done
[1634760805, 857760878, 2036477234, 1797285236, 2714170026, 2437710413, 1144321761, 2270814278, 1203596994, 1549984785, 4218075128, 4047158956, 0, 0, 477636988, 2609084528]
b'irisctf{initialization_is_no_problem}'
[*] Closed connection to babycha.chal.irisc.tf port 10100
避坑复盘:起初自己的exp没有打通的原因如下
在根据buffer对state进行复原的时候 语句存在问题 有点问题 不是特别清楚
#题目
#rjust字符串向右对齐,确保每个字节串都有4个字节 不足的部分用b"\x00"填充
buffer = b"".join(long_to_bytes(x).rjust(4, b"\x00") for x in state)
#old
init_state = []
for i in range(0, len(buffer), 4):
#这里相当于对ascii操作
flag = chr(buffer[i]) + chr(buffer[i+1]) + chr(buffer[i+2]) + chr(buffer[i+3])
print(bytes(flag.encode()))
init_state.append(bytes_to_long(flag.encode()))
#new
buffer = bytes(buffer) #对字节操作
state = [bytes_to_long(buffer[i:i+4]) for i in range(0,64,4)]
总结 上面的原因与编码知识息息相关 需要补习一下,对于后续的bytes拼接操作就不要转为chr进行拼接了 而是直接取值[a:b]
进行截取!
我是哈皮,祝您每天嗨皮!我们下期再见~