[Crypto]SM4算法分析
注:本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)=X0⊕T(X1⊕X2⊕X3⊕rk)
# _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(C1⊕k2),C3=T(C1⊕C2⊕k3),C4=T(C1⊕C2⊕C3⊕C4⊕k4)]
[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)] [1⊕C1,1⊕C2,1⊕C3,T(C1⊕C2⊕C3⊕C4⊕k4⊕1)]
这两者的密文最后只差 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()
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)