【IrisCTF2024】记录一道关于chacha20算法的密码题

本文详细解析了一个基于Chacha20算法的加密题目,涉及加密流程、状态数组初始化、异或操作和解密方法。作者通过逆向分析,展示了如何利用控制输入和对称加密原理解密旗标。
摘要由CSDN通过智能技术生成

IrisCTF 2024

题面:

image-20240108115433233

题目:

# 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的流程简单很多,其效率也会提高很多,类似于对称的流密码,每一个数据位单独进行加密操作
对于该算法流程的学习,强烈推荐这个视频

针对这个题目中给出的代码,主要分为三步走操作
一、初始化状态矩阵

ConestConestConestConest
Key0Key1Key2Key3
Key4Key5Key6Key7
CounterCounternoncenonce

二、状态矩阵变换

对矩阵中指定位置进行变换 每一次转两轮 一共十次二十轮

每一次中分别进行Row TransformDiagonal Transform

三、旋转变换流程

image-20240108115448834

而把加密过程封装 对于明文进行加密的流程如下:

下面这个图是整个chacha算法的加密流程 通过密钥生成 获得一系列的密钥 与明文异或 获得密文
其中:
numbver used once : nonce
block number : counter
image-20240108115457713

该题中最终的结果是异或产生,所以不需要对于原始的加密程序进行逆操作,而是利用其对称性

因为题目可以获得任意输入的加密密文

output.append(b ^ buffer[0])
buffer = buffer[1:]

缓冲区每次清除一位 总长为64位

下面这个图便于理解:

_636227282__ea4f2548077aaaf2c9e2f2929d8fa896_-2131926282_IMG_20240107_210023_0_xg_0

因为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

image-20240108151530034

#恢复初始状态
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] 进行截取!


我是哈皮,祝您每天嗨皮!我们下期再见~

  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值