SM4加密分析

参考:密码学基础——SM4算法-CSDN博客

SM4是中华人民共和国政府]采用的一种[分组密码标准,由国家密码管理局于2012年3月21日发布。相关标准为“GM/T 0002-2012《SM4分组密码算法》(原SMS4分组密码算法)。

SM4涉及异或、移位以及盒变换等操作,它分为加解密以及密钥扩展两个模块

分组长度和密钥长度:SM4算法的分组长度和密钥长度均为128位(16字节)。

加密流程(左)和密钥扩展(右)如下图所示

image-20250906202903145

S盒:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const uint8 Sbox[256] = {
0xd6,0x90,0xe9,0xfe,0xcc,0xe1,0x3d,0xb7,0x16,0xb6,0x14,0xc2,0x28,0xfb,0x2c,0x05,
0x2b,0x67,0x9a,0x76,0x2a,0xbe,0x04,0xc3,0xaa,0x44,0x13,0x26,0x49,0x86,0x06,0x99,
0x9c,0x42,0x50,0xf4,0x91,0xef,0x98,0x7a,0x33,0x54,0x0b,0x43,0xed,0xcf,0xac,0x62,
0xe4,0xb3,0x1c,0xa9,0xc9,0x08,0xe8,0x95,0x80,0xdf,0x94,0xfa,0x75,0x8f,0x3f,0xa6,
0x47,0x07,0xa7,0xfc,0xf3,0x73,0x17,0xba,0x83,0x59,0x3c,0x19,0xe6,0x85,0x4f,0xa8,
0x68,0x6b,0x81,0xb2,0x71,0x64,0xda,0x8b,0xf8,0xeb,0x0f,0x4b,0x70,0x56,0x9d,0x35,
0x1e,0x24,0x0e,0x5e,0x63,0x58,0xd1,0xa2,0x25,0x22,0x7c,0x3b,0x01,0x21,0x78,0x87,
0xd4,0x00,0x46,0x57,0x9f,0xd3,0x27,0x52,0x4c,0x36,0x02,0xe7,0xa0,0xc4,0xc8,0x9e,
0xea,0xbf,0x8a,0xd2,0x40,0xc7,0x38,0xb5,0xa3,0xf7,0xf2,0xce,0xf9,0x61,0x15,0xa1,
0xe0,0xae,0x5d,0xa4,0x9b,0x34,0x1a,0x55,0xad,0x93,0x32,0x30,0xf5,0x8c,0xb1,0xe3,
0x1d,0xf6,0xe2,0x2e,0x82,0x66,0xca,0x60,0xc0,0x29,0x23,0xab,0x0d,0x53,0x4e,0x6f,
0xd5,0xdb,0x37,0x45,0xde,0xfd,0x8e,0x2f,0x03,0xff,0x6a,0x72,0x6d,0x6c,0x5b,0x51,
0x8d,0x1b,0xaf,0x92,0xbb,0xdd,0xbc,0x7f,0x11,0xd9,0x5c,0x41,0x1f,0x10,0x5a,0xd8,
0x0a,0xc1,0x31,0x88,0xa5,0xcd,0x7b,0xbd,0x2d,0x74,0xd0,0x12,0xb8,0xe5,0xb4,0xb0,
0x89,0x69,0x97,0x4a,0x0c,0x96,0x77,0x7e,0x65,0xb9,0xf1,0x09,0xc5,0x6e,0xc6,0x84,
0x18,0xf0,0x7d,0xec,0x3a,0xdc,0x4d,0x20,0x79,0xee,0x5f,0x3e,0xd7,0xcb,0x39,0x48
};

非线性变换

非线性变换 img 是以字为单位的非线性替换,它由4个S盒并置构成。设输入为 img (4个32位的字),输出为 img (4个32位的字),则

线性变换部件L

线性变换部件L是以字为处理单位的线性变换,其输入输出都是32位的字,它的密码学作用是扩散。 设 L的输入为字B,输出为字C,则

img

合成变换 T

合成变换T由非线性变换和线性变换L复合而成,数据处理的单位是字。设输入为字 X,则先对 X进行非线性变换,再进行线性L变换。记为

img

轮函数

轮函数由上述基本密码部件构成。设轮函数的输入为4个32位字img共128位,轮密钥为一个32位的字。输出也是一个32位的字,由下式给出:

img

即是:

img

img,有

img

轮函数的结构如图所示

image-20250907190633135

加密算法

加密算法采用32轮迭代结构,每轮使用一个轮密钥。

设输入的明文为四个字(128比特长),输入的轮密钥为,共32个字。输出的密文为四个字(128比特长)。加密算法可描述如下:

为了与解密算法需要的顺序一致,同时也与人们的习惯顺序一致,在加密算法之后还需要一个反序处理:

imgimg

img

解密算法

解密算法与加密算法相同,只是轮密钥的使用顺序相反,解密轮密钥是加密轮密钥的逆序。

算法的输入为密文img 和轮密钥img,输出为明文img。根据式img。为了便于与加密算法对照,解密算法中仍然用img表示密文。于是可得到如下的解密算法。

解密算法:

img img

与加密算法之后需要一个反序处理同样的道理,在解密算法之后也需要一个反序处理

密钥扩展算法

SM4算法加密时输入128位的密钥,采用32轮迭代结构,每一轮使用一个32位的轮密钥,共使用32个轮密钥。使用密钥扩展算法,从加密密钥产生出32个轮密钥。

常数FK, 在密钥扩展中使用如下的常数

1
2
# 轮常数,用于密钥扩展
FK = [0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc]

固定参数

1
2
3
4
5
6
7
8
9
10
11
# 固定参数,用于轮密钥生成
CK = [
0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269,
0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9,
0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249,
0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9,
0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229,
0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299,
0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209,
0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279
]

MK:加密密钥

设输入的加密密钥为 img ,输出轮密钥为 img ,密钥扩展算法可描述如下(Ki)

中间数据:

image-20250907200522795
imgimg

其中的变换img与加密算法轮函数中的img基本相同,只将其中的线性变化 修改为以下的 :

img

密钥扩展算法的结构与加密算法的结构类似,也是采用了32轮的迭代处理。

算法流程

密钥扩展

  • 将128位的初始密钥通过密钥扩展算法生成32个32位的轮密钥。
  • 密钥扩展过程中使用了固定参数(CK)和系统参数(FK),确保密钥与轮函数之间的强关联性。

轮函数(F函数):

每轮迭代使用一个轮密钥,通过非线性变换(S盒)和线性变换(L函数)对数据进行处理。

S盒替换:将8位输入通过复合域S盒进行非线性替换,增强抗差分攻击能力。

线性变换:包括循环左移和异或操作,实现数据的高分支数扩散

加密过程

将128位的明文分组分为4个32位的字(X₀, X₁, X₂, X₃)。

通过32轮迭代,每轮使用一个轮密钥,生成新的中间状态。

最后一轮后,将4个字逆序拼接,得到128位的密文。

解密过程:

解密过程与加密过程相同,只是轮密钥的使用顺序相反。

其它

SM4算法支持多种工作模式,常见的有:

  1. ECB(电子密码本模式):每个分组独立加密,安全性较低,不推荐用于加密大量数据。
  2. CBC(密码分组链接模式):使用初始化向量(IV),每个分组的加密依赖于前一个分组的密文,安全性较高。
  3. CTR(计数器模式):将块加密算法转换为流加密算法,适合并行加密,安全性高。

加密代码

1
2
3
4
5
6
7
8
def encode(plaintext, rk):
X = [plaintext >> (128 - (i + 1) * 32) & 0xffffffff for i in range(4)]
for i in range(32):
t = T(X[1], X[2], X[3], rk[i])
c = (t ^ X[0])
X = X[1:] + [c]
ciphertext = R(X[0], X[1], X[2], X[3])
return ciphertext

加密首先是密钥扩展

密钥扩展

1
2
3
4
5
6
7
8
9
10
def key_extend(main_key):
MK = [(main_key >> (128 - (i + 1) * 32)) & 0xffffffff for i in range(4)]
keys = [FK[i] ^ MK[i] for i in range(4)]
RK = []
for i in range(32):
t = Tx(keys[i + 1], keys[i + 2], keys[i + 3], CK[i])
k = keys[i] ^ t
keys.append(k)
RK.append(k)
return RK

首先异或生成4个初始中间密钥

最后的轮密钥为rk

TX

1
2
3
4
def Tx(k1, k2, k3, ck):
xor = k1 ^ k2 ^ k3 ^ ck
t = s_box(xor)
return t ^ rotate_left(t, 13) ^ rotate_left(t, 23)

sbox

就是字节代换

简单的来说比如

0x17

就是找sbox的第二行第八列

1
2
3
4
5
6
7
8
9
def s_box(wd):
ret = []
for i in range(0, 4):
byte = (wd >> (24 - i * 8)) & 0xff
row = byte >> 4
col = byte & 0x0f
index = (row * 16 + col)
ret.append(S_BOX[index])
return bys_to_wd(ret)

rotate_left

左移,没什么好讲的

1
2
def rotate_left(wd, bit):
return (wd << bit & 0xffffffff) | (wd >> (32 - bit))

之后是对明文加密

T

1
2
3
4
def T(x1, x2, x3, rk):
t = x1 ^ x2 ^ x3 ^ rk
t = s_box(t)
return t ^ rotate_left(t, 2) ^ rotate_left(t, 10) ^ rotate_left(t, 18) ^ rotate_left(t, 24)

解密代码

我们结合python代码进行分析

1
2
3
4
5
6
7
8
9
def decode(ciphertext, rk):
ciphertext = int(ciphertext, 16)
X = [ciphertext >> (128 - (i + 1) * 32) & 0xffffffff for i in range(4)]
for i in range(32):
t = T(X[1], X[2], X[3], rk[31 - i])
c = (t ^ X[0])
X = X[1:] + [c]
m = R(X[0], X[1], X[2], X[3])
return m

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# 魔改S盒
S_BOX = [
0xD1, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7, 0x16, 0xB6, 0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x5, 0x2B, 0x67, 0x9A,
0x76, 0x2A, 0xBE, 0x4, 0xC3, 0xAA, 0x44, 0x13, 0x26, 0x49, 0x86, 0x6, 0x99, 0x9C, 0x42, 0x50, 0xF4, 0x91, 0xEF,
0x98, 0x7A, 0x33, 0x54, 0xB, 0x43, 0xED, 0xCF, 0xAC, 0x62, 0xE4, 0xB3, 0x17, 0xA9, 0x1C, 0x8, 0xE8, 0x95, 0x80,
0xDF, 0x94, 0xFA, 0x75, 0x8F, 0x3F, 0xA6, 0x47, 0x7, 0xA7, 0x4F, 0xF3, 0x73, 0x71, 0xBA, 0x83, 0x59, 0x3C, 0x19,
0xE6, 0x85, 0xD6, 0xA8, 0x68, 0x6B, 0x81, 0xB2, 0xFC, 0x64, 0xDA, 0x8B, 0xF8, 0xEB, 0xF, 0x4B, 0x70, 0x56, 0x9D,
0x35, 0x1E, 0x24, 0xE, 0x78, 0x63, 0x58, 0x9F, 0xA2, 0x25, 0x22, 0x7C, 0x3B, 0x1, 0x21, 0xC9, 0x87, 0xD4, 0x0, 0x46,
0x57, 0x5E, 0xD3, 0x27, 0x52, 0x4C, 0x36, 0x2, 0xE7, 0xA0, 0xC4, 0xC8, 0x9E, 0xEA, 0xBF, 0x8A, 0xD2, 0x40, 0xC7,
0x38, 0xB5, 0xA3, 0xF7, 0xF2, 0xCE, 0xF9, 0x61, 0x15, 0xA1, 0xE0, 0xAE, 0x5D, 0xA4, 0x9B, 0x34, 0x1A, 0x55, 0xAD,
0x93, 0x32, 0x30, 0xF5, 0x8C, 0xB1, 0xE3, 0x1D, 0xF6, 0xE2, 0x2E, 0x82, 0x66, 0xCA, 0x60, 0xC0, 0x29, 0x23, 0xAB,
0xD, 0x53, 0x4E, 0x6F, 0xD5, 0xDB, 0x37, 0x45, 0xDE, 0xFD, 0x8E, 0x2F, 0x3, 0xFF, 0x6A, 0x72, 0x6D, 0x6C, 0x5B,
0x51, 0x8D, 0x1B, 0xAF, 0x92, 0xBB, 0xDD, 0xBC, 0x7F, 0x11, 0xD9, 0x5C, 0x41, 0x1F, 0x10, 0x5A, 0xD8, 0xA, 0xC1,
0x31, 0x88, 0xA5, 0xCD, 0x7B, 0xBD, 0x2D, 0x74, 0xD0, 0x12, 0xB8, 0xE5, 0xB4, 0xB0, 0x89, 0x69, 0x97, 0x4A, 0xC,
0x96, 0x77, 0x7E, 0x65, 0xB9, 0xF1, 0x9, 0xC5, 0x6E, 0xC6, 0x84, 0x18, 0xF0, 0x7D, 0xEC, 0x3A, 0xDC, 0x4D, 0x20,
0x79, 0xEE, 0x5F, 0x3E, 0xD7, 0xCB, 0x39, 0x48
]

FK = [0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc]
CK = [
0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269,
0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9,
0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249,
0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9,
0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229,
0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299,
0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209,
0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279
]


def wd_to_byte(wd, bys):
bys.extend([(wd >> i) & 0xff for i in range(24, -1, -8)])


def bys_to_wd(bys):
ret = 0
for i in range(4):
bits = 24 - i * 8
ret |= (bys[i] << bits)
return ret

def s_box(wd):
ret = []
for i in range(0, 4):
byte = (wd >> (24 - i * 8)) & 0xff
row = byte >> 4
col = byte & 0x0f
index = (row * 16 + col)
ret.append(S_BOX[index])
return bys_to_wd(ret)


def rotate_left(wd, bit):
return (wd << bit & 0xffffffff) | (wd >> (32 - bit))


def Linear_transformation(wd):
return wd ^ rotate_left(wd, 2) ^ rotate_left(wd, 10) ^ rotate_left(wd, 18) ^ rotate_left(wd, 24)


def Tx(k1, k2, k3, ck):
xor = k1 ^ k2 ^ k3 ^ ck
t = s_box(xor)
return t ^ rotate_left(t, 13) ^ rotate_left(t, 23)


def T(x1, x2, x3, rk):
t = x1 ^ x2 ^ x3 ^ rk
t = s_box(t)
return t ^ rotate_left(t, 2) ^ rotate_left(t, 10) ^ rotate_left(t, 18) ^ rotate_left(t, 24)

def key_extend(main_key):
MK = [(main_key >> (128 - (i + 1) * 32)) & 0xffffffff for i in range(4)]
keys = [FK[i] ^ MK[i] for i in range(4)]
RK = []
for i in range(32):
t = Tx(keys[i + 1], keys[i + 2], keys[i + 3], CK[i])
k = keys[i] ^ t
keys.append(k)
RK.append(k)
return RK


def R(x0, x1, x2, x3):
x0 &= 0xffffffff
x1 &= 0xffffffff
x2 &= 0xffffffff
x3 &= 0xffffffff
s = f"{x3:08x}{x2:08x}{x1:08x}{x0:08x}"
return s


def encode(plaintext, rk):
X = [plaintext >> (128 - (i + 1) * 32) & 0xffffffff for i in range(4)]
for i in range(32):
t = T(X[1], X[2], X[3], rk[i])
c = (t ^ X[0])
X = X[1:] + [c]
ciphertext = R(X[0], X[1], X[2], X[3])
return ciphertext


def decode(ciphertext, rk):
ciphertext = int(ciphertext, 16)
X = [ciphertext >> (128 - (i + 1) * 32) & 0xffffffff for i in range(4)]
for i in range(32):
t = T(X[1], X[2], X[3], rk[31 - i])
c = (t ^ X[0])
X = X[1:] + [c]
m = R(X[0], X[1], X[2], X[3])
return m


if __name__ == '__main__':
key_str = 'NCTF24nctfNCTF24'
main_key = int.from_bytes(key_str.encode('utf-8'), byteorder='big')
rk = key_extend(main_key)

enc = bytes([
0xFB, 0x97, 0x3C, 0x3B, 0xF1, 0x99, 0x12, 0xDF, 0x13, 0x30,
0xF7, 0xD8, 0x7F, 0xEB, 0xA0, 0x6C, 0x14, 0x5B, 0xA6, 0x2A,
0xA8, 0x05, 0xA5, 0xF3, 0x76, 0xBE, 0xC9, 0x01, 0xF9, 0x36,
0x7B, 0x46
])

# Split ciphertext into two 16-byte blocks
block1 = enc[:16].hex()
block2 = enc[16:].hex()

# Decrypt each block
part1 = decode(block1, rk)
part2 = decode(block2, rk)

# Combine and decode to get flag
combined = bytes.fromhex(part1 + part2).decode('utf-8', errors='replace')
print(f"NCTF{{{combined}}}")

sbox也是可以魔改的,加密解密中间也可以魔改添加异或逻辑

1
2
3
4
5
from gmssl.sm4 import CryptSM4, SM4_DECRYPT

sm4_dec = CryptSM4()
sm4_dec.set_key(b'Pyu0Z8#bC5vqUFgt', SM4_DECRYPT)
print(sm4_dec.crypt_ecb(bytes.fromhex('6A61EF281A7473D6B1B431D0351F7E2242CFB9D6EC4E01EF656D6CF520F142821C7061EB843D5ABE378B394C4DC1298B')))

实际运用的时候,还需考虑输入分组长度不是 128bit 的整数倍时需要添加的填充(例如 PKCS #7)。此处的代码仅用于展示 SM4 加解密过程的原理,输入的加密数据长度仅支持 128bit(长度为 16 的 byte 数组)