注:本blog参考《SM4分组密码算法》(GB/T 32907-2016)与python开源库gmssl中的sm4算法实现

国家标准全文公开系统:https://openstd.samr.gov.cn
作者个人博客:https://baymax-fools.github.io

算法基本信息

SM4一般用于数据加密,属于分组密码(SM1、SM7也是分组密码,不过算法未公开)
分组长度:128bit,密钥长度:128bit
加密算法和密钥扩展算法均采用非线性迭代结构,轮数:32轮
轮密钥的使用情况和DES类似:解密轮密钥就是加密轮密钥的逆序,且解密和加密的算法结构相同

合成置换 T

合成置换T 是在密钥扩展算法、加密算法和解密算法中使用的函数,因此在这里先说一下这个函数的实现:
合成置换T 是一个可逆变换,由非线性变换函数 τ \tau τ 和线性变换函数 L L L 复合成
T ( , ) = L ( τ ( , ) ) T(,) = L(\tau (,)) T(,)=L(τ(,))
其中 τ 函数就是一个简单的S盒映射函数(非线性变换),没什么好说的
L 函数就是线性变换的,输入输出都是32位,实现:
L ( B ) = B ⊕ ( B < < < 2 ) ⊕ ( B < < < 10 ) ⊕ ( B < < < 18 ) ⊕ ( B < < < 24 ) L(B) = B \oplus (B <<< 2)\oplus (B <<< 10)\oplus (B <<< 18)\oplus (B <<< 24) L(B)=B(B<<<2)(B<<<10)(B<<<18)(B<<<24)
注: <<< 是循环左移

# Expanded SM4 box table  
SM4_BOXES_TABLE = []

# _f 是 T函数,在轮函数f中会被使用   
def _sm4_l_t(ka):  
    b = [0, 0, 0, 0]  
    a = put_uint32_be(ka)  # 将32位的数据转换成大端字节序的4个字节列表
	# τ函数  
	b[0] = SM4_BOXES_TABLE[a[0]]  
	b[1] = SM4_BOXES_TABLE[a[1]]  
	b[2] = SM4_BOXES_TABLE[a[2]]  
	b[3] = SM4_BOXES_TABLE[a[3]]  
	bb = get_uint32_be(b[0:4])  # put_unit32_be()的逆函数  
	# 下行是 L 函数  
	c = bb ^ (rotl(bb,2)) ^ (rotl(bb,10)) ^ (rotl(bb,18)) ^ (rotl(bb,24))  
	return c  

密钥

密钥长度为128bit
密钥表示: M K = ( M K 0 , M K 1 , M K 2 . M K 3 ) MK=(MK_0,MK_1,MK_2.MK_3) MK=(MK0,MK1,MK2.MK3)(每个32bit)
轮密钥表示: ( r k 0 , r k 1 , . . . , r k 31 ) (rk_0,rk_1,...,rk_{31}) (rk0,rk1,...,rk31)

密钥扩展算法

注:该算法用到了系统参数 F K = ( F K 0 , F K 1 , F K 2 , F K 3 ) FK = (FK_0,FK_1,FK_2,FK_3) FK=(FK0,FK1,FK2,FK3)和固定参数 C K = ( C K 0 , C K 1 , . . . , C K 31 ) CK = (CK_0,CK_1,...,CK_{31}) CK=(CK0,CK1,...,CK31)
轮密钥是密钥用密钥扩展算法得到
在这里插入图片描述

# Expanded SM4 box table  
SM4_BOXES_TABLE = []

# System parameter  
SM4_FK = []  
  
# fixed parameter  
SM4_CK = []

# 相当于 T'函数  
def _round_key(cls, ka):  # ka是T'函数的参数(已经计算好的)  
    b = [0, 0, 0, 0]  
    a = put_uint32_be(ka)  # 将32位的数据转换成大端字节序的4个字节列表  
    # 下面4行就是在实现 τ 函数  
    b[0] = SM4_BOXES_TABLE[a[0]]  
    b[1] = SM4_BOXES_TABLE[a[1]]  
    b[2] = SM4_BOXES_TABLE[a[2]]  
    b[3] = SM4_BOXES_TABLE[a[3]]  
    bb = get_uint32_be(b[0:4])  # put_unit32_be()的逆函数  
    rk = bb ^ (rotl(bb, 13)) ^ (rotl(bb, 23))   # 这是 L'函数,对应原文式(8)
    return rk
    
# 密钥扩展算法  
def set_key(self, key, mode):  
    key = bytes_to_list(key)  
    MK = [0, 0, 0, 0]   # 加密密钥分块  
    k = [0] * 36    # 用来暂时存放轮密钥,最后会被拷到 self.sk 中  
    MK[0] = get_uint32_be(key[0:4])  
    MK[1] = get_uint32_be(key[4:8])  
    MK[2] = get_uint32_be(key[8:12])  
    MK[3] = get_uint32_be(key[12:16])  
    k[0:4] = xor(MK[0:4], SM4_FK[0:4]) # 先计算 k0、k1、k2、k3 ,对应原文式(6)  
    for i in range(32):     # 整个for循环就是轮密钥生成算法,生成的轮密钥暂存在k中,对应原文式(7)  
        k[i + 4] = k[i] ^ (  
            self._round_key(k[i + 1] ^ k[i + 2] ^ k[i + 3] ^ SM4_CK[i]))  
        self.sk[i] = k[i + 4]  
    self.mode = mode  
    if mode == SM4_DECRYPT:     # 如果是解密模式,就将加密的轮密钥逆序  
        for idx in range(16):  
            t = self.sk[idx]  
            self.sk[idx] = self.sk[31 - idx]  
            self.sk[31 - idx] = t

轮函数 F

主要使用于加密函数中32次迭代运算,rk是轮密钥
结构: F ( X 0 , X 1 , X 2 , X 3 , r k ) = X 0 ⊕ T ( X 1 ⊕ X 2 ⊕ X 3 ⊕ r k ) F(X_0,X_1,X_2,X_3,rk) = X_0 \oplus T(X_1 \oplus X_2 \oplus X_3 \oplus rk) F(X0,X1,X2,X3,rk)=X0T(X1X2X3rk)

# _f 是 f函数,在轮函数f中会被使用  
def _f(cls, x0, x1, x2, x3, rk):  
    # _sm4_l_t是 T函数(就是上文的T函数代码实现)  
    def _sm4_l_t(ka):  
        b = [0, 0, 0, 0]  
        a = put_uint32_be(ka)  
        # τ函数  
        b[0] = SM4_BOXES_TABLE[a[0]]  
        b[1] = SM4_BOXES_TABLE[a[1]]  
        b[2] = SM4_BOXES_TABLE[a[2]]  
        b[3] = SM4_BOXES_TABLE[a[3]]  
        bb = get_uint32_be(b[0:4])  
        # 下行是 L 函数  
        c = bb ^ (rotl(bb,2)) ^ (rotl(bb,10)) ^ (rotl(bb,18)) ^ (rotl(bb,24))  
        return c  
    return (x0 ^ _sm4_l_t(x1 ^ x2 ^ x3 ^ rk))

加密算法

由32次迭代运算盒1次反序变换 R 组成
每组明文为128bit,分成4组,一组32bit( X 0 , X 1 , X 2 , X 3 X_0,X_1,X_2,X_3 X0,X1,X2,X3),输出密文 ( Y 0 , Y 1 , Y 2 , Y 3 ) (Y_0,Y_1,Y_2,Y_3) (Y0,Y1,Y2,Y3)

32次迭代:
X i + 4 = F ( X i , X i + 1 , X i + 2 , X i + 3 , r k i ) , i = 0 , 1 , . . . , 31 X_{i+4} = F(X_i,X_{i+1},X_{i+2},X_{i+3},rk_i),i = 0,1,...,31 Xi+4=F(Xi,Xi+1,Xi+2,Xi+3,rki),i=0,1,...,31

反序变换R:

( Y 0 , Y 1 , Y 2 , Y 3 ) = R ( X 32 , X 33 , X 34 , X 35 ) = ( X 35 , X 34 , X 33 , X 32 ) (Y_0,Y_1,Y_2,Y_3) = R(X_{32},X_{33},X_{34},X_{35}) = (X_{35},X_{34},X_{33},X_{32}) (Y0,Y1,Y2,Y3)=R(X32,X33,X34,X35)=(X35,X34,X33,X32)

代码实现

# 加密算法  
def one_round(self, sk, in_put): # 明文分组,一次128bit  
    out_put = []  
    ulbuf = [0] * 36  
    ulbuf[0] = get_uint32_be(in_put[0:4])  
    ulbuf[1] = get_uint32_be(in_put[4:8])  
    ulbuf[2] = get_uint32_be(in_put[8:12])  
    ulbuf[3] = get_uint32_be(in_put[12:16])  
    # 32轮迭代  
    for idx in range(32):  
        ulbuf[idx + 4] = self._f(ulbuf[idx],  
                                 ulbuf[idx + 1],  
                                 ulbuf[idx + 2],  
                                 ulbuf[idx + 3],  
                                 sk[idx])  
  
    # 反序变换R  
    out_put += put_uint32_be(ulbuf[35])  
    out_put += put_uint32_be(ulbuf[34])  
    out_put += put_uint32_be(ulbuf[33])  
    out_put += put_uint32_be(ulbuf[32])  
    return out_put

解密算法

本blog看到这解密算法应该也是很清晰的,将加密轮密钥逆序后同加密过程就能解密了
GB/T中写:本算法的解密变换与加密变换结构相同,不同的仅是轮密钥的使用顺序。解密时,使用轮密钥序 ( r k 31 , r k 30 , … , r k 0 rk_{31},rk_{30},…,rk_0 rk31,rk30,,rk0)

算法ecb、cbc模块

gmssl中还实现加密(解密)的ecb和cbc)

def crypt_ecb(self, input_data):  
    # SM4-ECB block encryption/decryption  
    input_data = bytes_to_list(input_data)  # 将输入字节转成列表  
    if self.mode == SM4_ENCRYPT:    # 判断是否要填充,PKCS7是经典的分组密码填充模式  
        if self.padding_mode == PKCS7:  
            input_data = pkcs7_padding(input_data)  
        elif self.padding_mode == ZERO:  
            input_data = zero_padding(input_data)  
  
    length = len(input_data)  
    i = 0  
    output_data = []  
    while length > 0:   # ecb的加密  
        output_data += self.one_round(self.sk, input_data[i:i + 16])  
        i += 16  
        length -= 16  
    if self.mode == SM4_DECRYPT:    # 解密的话要先去填充  
        if self.padding_mode == PKCS7:  
            return list_to_bytes(pkcs7_unpadding(output_data))  
        elif self.padding_mode == ZERO:  
            return list_to_bytes(zero_unpadding(output_data))  
    return list_to_bytes(output_data)  
  
def crypt_cbc(self, iv, input_data):  
    # SM4-CBC buffer encryption/decryption  
    i = 0  
    output_data = []  
    tmp_input = [0] * 16  
    iv = bytes_to_list(iv)  # cbc模式的特征,引入一个初始向量iv  
    if self.mode == SM4_ENCRYPT:  
        input_data = pkcs7_padding(bytes_to_list(input_data))   # 加密填充  
        length = len(input_data)  
        while length > 0:   # cbc模式加密,很经典的实现方式  
            tmp_input[0:16] = xor(input_data[i:i + 16], iv[0:16])  
            output_data += self.one_round(self.sk, tmp_input[0:16])  
            iv = copy.deepcopy(output_data[i:i + 16])  
            i += 16  
            length -= 16  
        return list_to_bytes(output_data)  
    else:   # 解密流程  
        length = len(input_data)      
        while length > 0:  
            output_data += self.one_round(self.sk, input_data[i:i + 16])  
            output_data[i:i + 16] = xor(output_data[i:i + 16], iv[0:16])  
            iv = copy.deepcopy(input_data[i:i + 16])  
            i += 16  
            length -= 16  
        return list_to_bytes(pkcs7_unpadding(output_data))

题目:

UNICTF2026-Subgroup-Scribe

题目:

from os import urandom
from random import randint
from secret import flag

sbox = [147, 138, 104,  87,   5, 201, 249, 141, 243,  72,  71, 221,  97, 174,  48, 155,
        114, 225, 117, 105, 224,  70,   7, 108, 190, 146, 145, 130,  46, 209, 229, 226,
         15, 112, 103,  27,  91, 181, 253, 183, 152, 165, 110,  44, 160,  66, 116,   0,
         75,  26,  61,  96, 127, 157, 197, 164, 172,  20,  37,  68, 202, 101,   9,   3,
        109,  31, 208,  98,  11, 144,  79,  25, 239, 231,  43,  36,  10,   2, 170, 251,
        161, 135, 134, 166, 136, 177, 215,  82, 244, 218,  47, 137, 242,  76, 233, 115,
        182, 153, 214,  84,  13, 159,  60,  74,  65,  54, 163,  56, 180,  30, 139, 236,
         67,  64,  80, 119,  40, 206, 148,  93, 217,  81, 126, 162, 185, 186,  77, 234,
         45, 142, 230, 179,  34, 193, 124, 107, 125, 198,  90,  23,  12, 232, 100,  16,
        120,  59,   1,   6, 102,  24, 133, 176, 150, 187,  28,  51, 195,  85, 196, 219,
        167, 227,  38,  55, 248, 241, 204, 235, 192, 194,  52, 252, 247,   4, 212,  58,
         78, 245, 240,  21,  14,  29, 169,   8, 121,  86, 118, 184, 143, 129,  69, 205,
        132, 213, 246, 238,  73,  53, 122,  62,  35, 210, 250, 149,  17, 203, 111,  18,
        158,  33, 151,  50,  83,  57,  92, 123,  95,  63, 216, 189, 173, 175, 220,  94,
        106,  41, 222, 154,  89, 156, 171,  32, 200,  88, 254,  99, 140, 228, 188, 207,
         19, 113, 255,  49, 237, 223, 191, 168,  42, 211,  22, 199, 128,  39, 178, 131]


FK = [0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC]
CK = [0x00070E15, 0x8C939AA1, 0x181F262D, 0xA4ABB2B9, 0xAC019832, 0XD8DFE6ED, 0X2C333A41]


def rotl(x, n):
    return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))

def tau(a):
    return ((sbox[(a >> 24) & 0xFF] << 24) |
            (sbox[(a >> 16) & 0xFF] << 16) |
            (sbox[(a >> 8) & 0xFF] << 8) |
             sbox[a & 0xFF])

def L(b):
    return b ^ rotl(b, 2) ^ rotl(b, 10) ^ rotl(b, 18) ^ rotl(b, 24)

def T(x):
    return L(tau(x))

def T_prime(x):
    b = tau(x)
    return b ^ rotl(b, 13) ^ rotl(b, 23)

def gen_key(mk_int):
    k = [(mk_int >> 96) & 0xFFFFFFFF, (mk_int >> 64) & 0xFFFFFFFF, 
         (mk_int >> 32) & 0xFFFFFFFF, mk_int & 0xFFFFFFFF]
    k = [k[0] ^ FK[0], k[1] ^ FK[1], k[2] ^ FK[2], k[3] ^ FK[3]]
    rk = []
    for i in range(7):
        k_next = k[i] ^ T_prime(k[i+1] ^ k[i+2] ^ k[i+3] ^ CK[i])
        k.append(k_next)
        rk.append(k_next)
    return rk

def encrypt(msg, key):
    assert len(msg) == len(key) == 16

    msg = int.from_bytes(msg)
    key = int.from_bytes(key)
    rk = gen_key(key)
    x = [(msg >> 96) & 0xFFFFFFFF, (msg >> 64) & 0xFFFFFFFF, 
         (msg >> 32) & 0xFFFFFFFF, msg & 0xFFFFFFFF]
    
    for i in range(7):
        x_next = x[0] ^ T(x[1] ^ x[2] ^ x[3] ^ rk[i])
        x = x[1:] + [x_next]
    
    y = x[::-1]
    return int.to_bytes((y[0] << 96) | (y[1] << 64) | (y[2] << 32) | y[3], 16)

def enc_ecb(msg, key):
    assert len(msg) % 16 == len(key) % 16 == 0
    msg = [msg[16 * i : 16 * i + 16] for i in range(len(msg) // 16)]
    enc = []
    for i in msg:
        enc += [encrypt(i, key)]
        key = bytes([sbox[i] for i in key]) # 
    return b''.join(enc)

ROUNDS = 128
for r in range(ROUNDS):
    print(f"--- Round {r + 1}/{ROUNDS} ---")
    key = urandom(16)
    coin = randint(0, 1)
    msg = bytes.fromhex(input('msg > '))
    if len({*zip(*[iter(msg)] * 16)}) * 16 != len(msg): print('🤡'); exit()
    enc = enc_ecb(msg, key)
    print(f'hint: {[enc.hex(), urandom(len(enc)).hex()][coin]}')
    if int(input('give me coin > ')) != coin: print('🤬'); exit()
print(f'😊: {flag}')

官方wp:https://my.feishu.cn/docx/IB4Ad9hP3o0HWFxRxVtcBIKNnJb?from=from_copylink
wp中说:
首先 sbox 的循环节太短了,只有 128,所以加密 129 块即可得到两个 key 完全相同的块。

下面就是要想办法使得两个不同的明文加密后能看出来关联。这里使用的两个初始状态为 [0, 0, 0, 0] 和 [1, 1, 1, 0]。

[0, 0, 0, 0] 的密文为: [ C 1 = T ( k 1 ) , C 2 = T ( C 1 ⊕ k 2 ) , C 3 = T ( C 1 ⊕ C 2 ⊕ k 3 ) , C 4 = T ( C 1 ⊕ C 2 ⊕ C 3 ⊕ C 4 ⊕ k 4 ) ] [C_1=T(k_1), C_2 = T(C_1 \oplus k_2), C_3 = T(C_1 \oplus C_2 \oplus k_3), C_4 = T(C_1 \oplus C_2 \oplus C_3 \oplus C_4 \oplus k_4)] [C1=T(k1),C2=T(C1k2),C3=T(C1C2k3),C4=T(C1C2C3C4k4)]

[1, 1, 1, 0] 的密文为: [ 1 ⊕ C 1 , 1 ⊕ C 2 , 1 ⊕ C 3 , T ( C 1 ⊕ C 2 ⊕ C 3 ⊕ C 4 ⊕ k 4 ⊕ 1 ) ] [1 \oplus C_1, 1 \oplus C_2, 1 \oplus C_3, T(C_1 \oplus C_2 \oplus C_3 \oplus C_4 \oplus k_4 \oplus 1)] [1C1,1C2,1C3,T(C1C2C3C4k41)]

这两者的密文最后只差 T 中的一个 ⊕ 1 \oplus 1 1,而由于 T 对于 sbox 后的内容是线性的,sbox 只有低 1 byte 会改变,所以只有可能有 256 种变化,直接将这 256 种变化存到一个 list 里,将两个密文异或看是不是在这里面就可以了。

脚本:

#!/usr/bin/env python3
from pwn import *
from os import urandom

sbox = [147, 138, 104,  87,   5, 201, 249, 141, 243,  72,  71, 221,  97, 174,  48, 155,
        114, 225, 117, 105, 224,  70,   7, 108, 190, 146, 145, 130,  46, 209, 229, 226,
         15, 112, 103,  27,  91, 181, 253, 183, 152, 165, 110,  44, 160,  66, 116,   0,
         75,  26,  61,  96, 127, 157, 197, 164, 172,  20,  37,  68, 202, 101,   9,   3,
        109,  31, 208,  98,  11, 144,  79,  25, 239, 231,  43,  36,  10,   2, 170, 251,
        161, 135, 134, 166, 136, 177, 215,  82, 244, 218,  47, 137, 242,  76, 233, 115,
        182, 153, 214,  84,  13, 159,  60,  74,  65,  54, 163,  56, 180,  30, 139, 236,
         67,  64,  80, 119,  40, 206, 148,  93, 217,  81, 126, 162, 185, 186,  77, 234,
         45, 142, 230, 179,  34, 193, 124, 107, 125, 198,  90,  23,  12, 232, 100,  16,
        120,  59,   1,   6, 102,  24, 133, 176, 150, 187,  28,  51, 195,  85, 196, 219,
        167, 227,  38,  55, 248, 241, 204, 235, 192, 194,  52, 252, 247,   4, 212,  58,
         78, 245, 240,  21,  14,  29, 169,   8, 121,  86, 118, 184, 143, 129,  69, 205,
        132, 213, 246, 238,  73,  53, 122,  62,  35, 210, 250, 149,  17, 203, 111,  18,
        158,  33, 151,  50,  83,  57,  92, 123,  95,  63, 216, 189, 173, 175, 220,  94,
        106,  41, 222, 154,  89, 156, 171,  32, 200,  88, 254,  99, 140, 228, 188, 207,
         19, 113, 255,  49, 237, 223, 191, 168,  42, 211,  22, 199, 128,  39, 178, 131]


FK = [0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC]
CK = [0x00070E15, 0x8C939AA1, 0x181F262D, 0xA4ABB2B9, 0xAC019832, 0XD8DFE6ED, 0X2C333A41]


def rotl(x, n):
    return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))

def tau(a):
    return ((sbox[(a >> 24) & 0xFF] << 24) |
            (sbox[(a >> 16) & 0xFF] << 16) |
            (sbox[(a >> 8) & 0xFF] << 8) |
             sbox[a & 0xFF])

def L(b):
    return b ^ rotl(b, 2) ^ rotl(b, 10) ^ rotl(b, 18) ^ rotl(b, 24)

def T(x):
    return L(tau(x))

T_maps = set()
for i in range(256):
    idx = sbox[i] ^ sbox[i ^ 1]
    T_maps.add(L(idx))


p = remote("nc1.ctfplus.cn", 42974)
# scontext(log_level='debug')

for i in range(128):
    msg = [b'\x00' * 16] 

    for _ in range(127):
        msg.append(urandom(16))
        
    # 第 129 个块:[1, 1, 1, 0]
    msg.append(b'\x00\x00\x00\x01' * 3 + b'\x00\x00\x00\x00')

    msg_hex = b''.join(msg).hex()

    p.recvuntil(b'msg > ')
    p.sendline(msg_hex.encode())

    p.recvuntil(b'hint: ')
    hint = bytes.fromhex(p.recvline()[:-1].decode())
    #print(hint)

    enc1 = hint[0:16]
    enc2 = hint[-16:]
    # 取第四个字节
    enc1 = int.from_bytes(enc1[12:16],'big')
    enc2 = int.from_bytes(enc2[12:16],'big')

    x = enc1 ^ enc2
    if x in T_maps:
        coin = 0
    else:
        coin = 1

    p.recvuntil(b'give me coin > ')

    p.sendline(str(coin).encode())
    print(i)

p.interactive()
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐