第二届熵密杯复现

第二届熵密杯复现

备赛ing 尝试不用AI看看往年题

赛题结构如下:

赛题结构

初始谜题1

请下载题目附件,并获取在线场景,双击运行附件中的客户端,输入IP地址和端口号连接服务端。请认真分析题目附件中的代码,根据服务端所给出的密文,解密出所对应的明文消息,并提交至服务端。

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
from sympy import Mod, Integer
from sympy.core.numbers import mod_inverse

# 模数
N_HEX = "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123"
MODULUS = Integer(int(N_HEX, 16))
MSG_PREFIX = "CryptoCup message:"

# 加密函数
def encrypt_message(message, key):
# 添加前缀
message_with_prefix = MSG_PREFIX + message
message_bytes = message_with_prefix.encode('utf-8')
message_len = len(message_bytes)
num_blocks = (message_len + 15) // 16
blocks = [message_bytes[i * 16:(i + 1) * 16] for i in range(num_blocks)]
# 进行0填充
blocks[-1] = blocks[-1].ljust(16, b'\x00')
encrypted_blocks = []
k = key
# 加密每个分组
for block in blocks:
block_int = int.from_bytes(block, byteorder='big')
encrypted_block_int = Mod(block_int * k, MODULUS)
encrypted_blocks.append(encrypted_block_int)
k += 1 # 密钥自增1
# 将加密后的分组连接成最终的密文
encrypted_message = b''.join(
int(block_int).to_bytes(32, byteorder='big') for block_int in encrypted_blocks
)
return encrypted_message

# 解密函数
def decrypt_message(encrypted_message, key):
num_blocks = len(encrypted_message) // 32
blocks = [encrypted_message[i * 32:(i + 1) * 32] for i in range(num_blocks)]
decrypted_blocks = []
k = key

# 解密每个分组
for block in blocks:
block_int = int.from_bytes(block, byteorder='big')
key_inv = mod_inverse(k, MODULUS)
decrypted_block_int = Mod(block_int * key_inv, MODULUS)
decrypted_blocks.append(decrypted_block_int)
k += 1 # 密钥自增1
# 将解密后的分组连接成最终的明文
decrypted_message = b''.join(
int(block_int).to_bytes(16, byteorder='big') for block_int in decrypted_blocks
)
# 去除前缀
if decrypted_message.startswith(MSG_PREFIX.encode('utf-8')):
decrypted_message = decrypted_message[len(MSG_PREFIX):]

return decrypted_message.rstrip(b'\x00').decode('utf-8')

# 测试
initial_key = Integer(0x123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0)
message = "Hello, this is a test message."
print("Original Message:", message)

# 加密
encrypted_message = encrypt_message(message, initial_key)
print("Encrypted Message (hex):", encrypted_message.hex())

# 解密
decrypted_message = decrypt_message(encrypted_message, initial_key)
print("Decrypted Message:", decrypted_message)

ljust()rjust() 是 Python 字符串(str)的方法,用于对齐字符串

  • ljust = left justify(左对齐)
  • rjust = right justify(右对齐)

本题应该算作签到题,核心加密逻辑在于

其中的k在每一块加密完后自加1

考虑到题目在消息前面加的初始前缀已经超过16字节了 而我们又能够获得加密后的密文 因此首先可以将初始密钥恢复出来,进而恢复完整密文(解密函数已经给出,做一点简单的边界处理即可)

因为没有具体的数据,这里直接copy的特等奖队伍的wp中交互拿到的数据,然后再自己做处理

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
from Crypto.Util.number import *

from sympy import Mod, Integer
from sympy.core.numbers import mod_inverse

# 模数
N_HEX = "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123"
MODULUS = Integer(int(N_HEX, 16))

# 解密函数
def decrypt_message(encrypted_message, key=-1):
num_blocks = len(encrypted_message) // 32
blocks = [encrypted_message[i * 32:(i + 1) * 32] for i in range(num_blocks)]
decrypted_blocks = []
k = key

# 解密每个分组
for block in blocks:
if k == -1:
msg_inv=inverse(int.from_bytes(msg[:16], byteorder='big'),MODULUS)
block_int = int.from_bytes(block, byteorder='big')
keyy=block_int*msg_inv%MODULUS
k=keyy
block_int = int.from_bytes(block, byteorder='big')
key_inv = mod_inverse(k, MODULUS)
decrypted_block_int = Mod(block_int * key_inv, MODULUS)
decrypted_blocks.append(decrypted_block_int)
k += 1 # 密钥自增1
# 将解密后的分组连接成最终的明文
decrypted_message = b''.join(
long_to_bytes(block_int)for block_int in decrypted_blocks
)
# 去除前缀
if decrypted_message.startswith(MSG_PREFIX.encode('utf-8')):
decrypted_message = decrypted_message[len(MSG_PREFIX):]

return decrypted_message.rstrip(b'\x00').decode('utf-8')

MSG_PREFIX = "CryptoCup message:"
msg=MSG_PREFIX.encode('utf-8')

c="9780b05ea8decefb932468a5e95202c055003062d7ced47b2bc83396bf535c9679ffe947e9eea132752f057f2c3efa9b5ddc364907ecd5d5a1c6c92c8e33927612b58f9fbd1a1039fd35c51b65961f551862c2ce7aa5096fb67185cc5a19260948f190a5379f57181883a615fabae29bf4cfa26e0614062a4e5c64501540fc38"
cc=bytes.fromhex(c)
print(decrypt_message(cc))

#TXxlMozyF6FQVmZSMxtr5VWjDy3eLLLY

初始谜题2

利用本题的情景对SM3算法进行简单的学习(实则是之前学过但是忘了 悲

SM3

SM3 是一个杂凑函数,输入任意长度消息,输出固定长度 256 bit,也就是 64 个十六进制字符

gmssl 里,SM3 的主入口是:

1
sm3.sm3_hash(msg)

其中 msg 是字节列表,不是 bytes

SM3 的整体结构

SM3 的执行过程可以分成四步:

  1. 消息填充
  2. 分组
  3. 每组进入压缩函数
  4. 输出最终 256 bit 状态值

这和 SHA 家族很像,属于“迭代压缩型哈希”。

gmssl/sm3.py 中的主要函数分工如下:

  • sm3_hash(msg):主流程
  • sm3_cf(v_i, b_i):压缩函数
  • sm3_ff_j(x, y, z, j):布尔函数 FF
  • sm3_gg_j(x, y, z, j):布尔函数 GG
  • sm3_p_0(x)sm3_p_1(x):置换函数

1.初始向量 IV

SM3 从固定的 8 个 32 位整数开始:

1
2
3
4
IV = [
1937774191, 1226093241, 388252375, 3666478592,
2842636476, 372324522, 3817729613, 2969243214,
]

这 8 个数构成 256 bit 内部状态。

之后每处理一个 512 bit 分组,就把旧状态和当前分组一起送进压缩函数,算出新状态。

2.消息填充

sm3_hash(msg) 中,第一件事是填充消息,使得总长度满足:

  • 最终消息长度对 64 字节取模等于 56
  • 然后再追加 8 字节消息原始比特长度的二进制表示

3.消息分组

填充完后,消息会被切成 64 字节一组,也就是 512 bit 一组:

1
2
3
group_count = round(len(msg) / 64)
...
B.append(msg[i*64:(i+1)*64])

4.消息扩展

一个512bits的块先分成16个32bits字,然后扩展成68个字W;随后再计算后64个字W‘

  • W[0..67]
  • W'[0..63]

源码:

1
2
3
4
5
6
7
8
9
10
11
for i in range(16):
...
w.append(data)

for j in range(16, 68):
w.append(0)
w[j] = sm3_p_1(w[j-16] ^ w[j-9] ^ (rotl(w[j-3], 15 % 32))) ^ (rotl(w[j-13], 7 % 32)) ^ w[j-6]

for j in range(0, 64):
w_1.append(0)
w_1[j] = w[j] ^ w[j+4]

这一步可以理解为:

  • 先把原始消息分组拆成 16 个字
  • 再通过异或、循环左移、置换函数不断“搅开”
  • 让后续轮函数能看到更广范围的信息

消息扩展是密码算法里常见的设计,用于提升扩散性。

5.64轮压缩函数 sm3_cf

这是 SM3 的核心。

它的任务是:把

  • 旧状态 v_i
  • 当前消息块 b_i

混合成新的状态。

每一轮都会更新寄存器中的值

具体到每一轮,会包含下面的操作:(了解即可)

SM3 有两个轮函数:

  • sm3_ff_j
  • sm3_gg_j

它们在前 16 轮和后 48 轮使用不同表达式。

FF

前 16 轮:

1
x ^ y ^ z

后 48 轮:

1
(x & y) | (x & z) | (y & z)
GG

前 16 轮:

1
x ^ y ^ z

后 48 轮:

1
(x & y) | ((~x) & z)
置换函数 P0 和 P1

源码:

1
2
3
4
5
def sm3_p_0(x):
return x ^ (rotl(x, 9 % 32)) ^ (rotl(x, 17 % 32))

def sm3_p_1(x):
return x ^ (rotl(x, 15 % 32)) ^ (rotl(x, 23 % 32))

这两个函数本质上都是:

  • 原值
  • 若干次循环左移后的值
  • 再异或起来

这种结构的意义在于:

  • 计算快
  • 容易在不同 bit 之间传播影响
  • 有利于抵抗某些线性分析

P1 用在消息扩展里,P0 用在压缩轮内部。

sm3_cf 里有 8 个工作寄存器:

1
a, b, c, d, e, f, g, h = v_i

然后做 64 轮迭代:

1
2
3
4
5
6
for j in range(0, 64):
ss_1 = ...
ss_2 = ...
tt_1 = ...
tt_2 = ...
...

每一轮都会:

  • 混入常量 T_j[j]
  • 混入消息扩展字 W[j]W'[j]
  • 混入当前状态字
  • 再通过循环移位和布尔函数更新状态

这和很多哈希算法类似,核心就是反复做:

  • 非线性变换
  • 位置置换
  • 状态搅拌

最终得到全新的 8 个状态字。

6.链式迭代

一组处理完后,SM3 不是直接把轮输出当成结果,而是和旧状态再异或。也就是将原来寄存器中的值和压缩函数处理后的值进行对应位置上的异或

1
return [v_j[i] ^ v_i[i] for i in range(8)]

这个“前后状态异或”的设计,使得每轮结果和历史状态强耦合。

最后所有分组处理完,把 8 个 32 位整数拼起来,就是最终摘要。

SM3 KDF 原理

sm3_kdf(z, klen) 是密钥派生函数,经常在 SM2 加密中使用。

它的思路是:

  1. 准备共享材料 Z
  2. 拼上计数器 ct = 1, 2, 3, ...
  3. 每次算一个 SM3
  4. 拼接多个哈希结果
  5. 截断到所需长度

源码里:

1
2
3
4
for i in range(rcnt):
msg = zin + [i for i in binascii.a2b_hex(('%08x' % ct).encode('utf8'))]
ha = ha + sm3_hash(msg)
ct += 1

为什么要加计数器?

因为一次 SM3 只有 256 bit,如果要导出更长密钥,就需要连续生成多个块。

题目分析

请下载题目附件,并获取在线场景,双击运行附件中的客户端,输入IP地址和端口号连接服务端。请认真分析题目附件中的代码,根据服务端所给出的counter和token,伪造出一个可以通过验证的counter和token,并提交至服务端。

题目代码如下:

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
import binascii
from gmssl import sm3

# 读取HMAC key文件
def read_hmac_key(file_path):
with open(file_path, 'rb') as f:
hmac_key = f.read().strip()
return hmac_key

# 生成token
def generate_token(hmac_key, counter):
# 如果HMAC_KEY长度不足32字节,则在末尾补0,超过64字节则截断
if len(hmac_key) < 32:
hmac_key = hmac_key.ljust(32, b'\x00')
elif len(hmac_key) > 32:
hmac_key = hmac_key[:32]
# 将计数器转换为字节表示
counter_bytes = counter.to_bytes((counter.bit_length() + 7) // 8, 'big')
# print("counter_bytes:", binascii.hexlify(counter_bytes))
tobe_hashed = bytearray(hmac_key + counter_bytes)
# print("tobe_hashed:", binascii.hexlify(tobe_hashed))
# 使用SM3算法计算哈希值
sm3_hash = sm3.sm3_hash(tobe_hashed)
# 将SM3的哈希值转换为十六进制字符串作为token
token = sm3_hash
return token
current_counter = 0
def verify_token(hmac_key, counter, token):
# 生成token
generated_token = generate_token(hmac_key, counter)
global current_counter
# 比较生成的token和输入的token是否相同
if generated_token == token:
if counter & 0xFFFFFFFF > current_counter:
current_counter = counter & 0xFFFFFFFF
print("current_counter: ", hex(current_counter))
return "Success"
else:
return "Error: counter must be increasing"
else:
return "Error: token not match"

# 假设HMAC key文件路径
hmac_key_file = 'hmac_key.txt'

# 假设计数器值
counter = 0x12345678

# 读取HMAC key
hmac_key = read_hmac_key(hmac_key_file)

# 生成token
token = generate_token(hmac_key, counter)
print("Generated token:", token)
print(verify_token(hmac_key, counter, token))

题目需要我们输入一个token,使其和根据服务器的hmac_key以及我们输入的counter计算出来的token相同

current_counter中存储着当前计数器状态,当我们输入时,需要让输入的counter的低32bits比当前计数器中的数字大

根据wp分享,本题会给出服务器的token和当前的counter 需要我们伪造出不同的token和counter

因此本题考察的是SM3长度扩展攻击

由于hmac_key=32bits,counter=4bits 因此给出的token是第一个分组处理后的结果

我们只需要对counter用SM3长度扩展攻击的方法去更新即可,使其前半段和本来的第一部分完全相同,然后后面再填上很多0xff(保证counter更大即可)

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
from gmssl import sm3 , func
from Crypto.Util.number import *
counter = 0x7501e6ea
token = 0xf4ce927c79b616e8e8f7223828794eedf9b16591ae572172572d51e135e0d21a

counter_bytes = counter.to_bytes((counter.bit_length() + 7) // 8, 'big')

counter_bytes+=b'\x80'+b'\x00'*19
counter_bytes+=b'\x00'*6+b'\x01'+b'\x20'
counter_bytes+=b'\xff'*4

last_block = b'\xff' * 4
last_block += b'\x80' + b'\x00' * 51
last_block += b'\x00' * 6 + b'\x02\x20'

counter_bytes+=b'\x00'*6+b'\x02\x20'
print(counter_bytes)

value=long_to_bytes(token).hex()
register=[]
for i in range(0,64,8):
register.append(bytes_to_long((bytes.fromhex(value[i:i+8]))))

newvalue=sm3.sm3_cf(register, func.bytes_to_list(last_block))

newtoken=b''
for i in range(8):
newtoken+=long_to_bytes(newvalue[i])
print(newtoken.hex())

这里counter_bytes用于扩展counter last_block表示最后一个分组填充后的内容(注意我们构造的消息长度是超过一个字节的,因此last_block最后的长度是512+4*8)

这里关键是使用到了gmssl库中的函数:sm3.sm3_cf

该函数根据我们上面的学习,起到压缩函数的功能,将当前寄存器状态和消息块传入后能够得到新的寄存器状态

此外,gmssl库中的func模块有一些辅助函数可以使用(详见说明文档),这里用到了bytes_to_list(虽然实测直接传字节串也没问题)

初始谜题3

请下载题目附件,并获取在线场景,双击运行附件中的客户端,输入IP地址和端口号连接服务端。请认真分析题目附件中的代码,根据服务端所给出的公钥和密文,解密出所对应的明文消息,并提交至服务端。

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
import sympy as sp
import random

# 设置参数
n = 16 # 向量长度
q = 251 # 模数

# 生成随机噪声向量e
e = sp.Matrix(sp.randMatrix(n, 1, min=0, max=1)) # 噪声向量
# 生成随机n维私钥向量s和n*n矩阵A
s = sp.Matrix(sp.randMatrix(n, 1, min=0, max=q - 1)) # 私钥向量
Temp = sp.Matrix(sp.randMatrix(n, n, min=0, max=q - 1)) # 中间变量矩阵Temp
A = Temp.inv_mod(q) # 计算矩阵Temp在模 q 下的逆矩阵作为A

# 计算n维公钥向量b
b = (A * s + e) % q # 公钥向量b = A * s + e

# 加密函数
def encrypt(message, A, b):
m_bin = bin(message)[2:].zfill(n) # 将消息转换为16比特的二进制字符串
m = sp.Matrix([int(bit) for bit in m_bin]) # 转换为SymPy矩阵
x = sp.Matrix(sp.randMatrix(n, n, min=0, max=q // (n * 4))) # 随机产生一个n*n的矩阵x
e1 = sp.Matrix(sp.randMatrix(n, 1, min=0, max=1)) # 随机产生一个n维噪声向量e
c1 = (x * A) % q # 密文部分c1 = x * A
c2 = (x * b + e1 + m * (q // 2)) % q # 密文部分c2 = x * b + e1 + m * q/2
return c1, c2

# 解密函数
def decrypt(c1, c2, s):
m_dec = (c2 - c1 * s) % q
m_rec = m_dec.applyfunc(lambda x: round(2 * x / q) % 2) # 还原消息
m_bin = ''.join([str(bit) for bit in m_rec]) # 将SymPy矩阵转换为二进制字符串
m_rec_int = int(m_bin, 2) # 将二进制字符串转换为整数
return m_rec_int

# 测试加解密
message = random.randint(0, 2 ** n - 1) # 要加密的消息,随机生成一个16比特整数
c1, c2 = encrypt(message, A, b) # 加密
print("原始消息: ", message)
print("公钥A=sp.", A)
print("公钥b=sp.", b)
print("密文c1=sp.", c1)
print("密文c2=sp.", c2)
decrypted_message = decrypt(c1, c2, s)
print("解密后的消息: ", decrypted_message) # 输出解密后的消息

核心公式:

我们能够首先将x求出,进而将第二个式子进行变化:

噪声很小,只需要判断每个元素是否大于等于125即可判断m的当前位是0还是1了

这里借用鸡块师傅的数据做一个复现,顺便手搓熟悉下sage的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from Crypto.Util.number import *
A = Matrix(ZZ,[[139, 63, 18, 202, 166, 185, 85, 108, 58, 90, 211, 248, 240, 44, 137, 39], [5, 230, 89, 226, 139, 24, 233, 20, 12, 108, 127, 11, 52, 64, 188, 156], [80, 61, 105, 3, 165, 96, 154, 40, 62, 103, 157, 75, 190, 101, 31, 239], [193, 100, 124, 216, 248, 95, 241, 196, 67, 192, 217, 114, 171, 248, 219, 169], [116, 71, 221, 105, 167, 153, 22, 124, 178, 45, 7, 183, 125, 8, 127, 123], [182, 162, 164, 184, 27, 148, 206, 73, 217, 86, 187, 137, 82, 150, 99, 65], [106, 60, 153, 91, 213, 41, 188, 92, 121, 246, 164, 223, 199, 85, 161, 25], [93, 97, 145, 31, 48, 36, 7, 110, 56, 47, 108, 79, 233, 186, 93, 181], [195, 98, 47, 147, 49, 40, 158, 89, 218, 8, 23, 118, 170, 19, 50, 17], [127, 95, 37, 48, 230, 244, 130, 37, 75, 125, 103, 154, 148, 218, 227, 178], [162, 235, 129, 44, 204, 228, 221, 130, 239, 36, 57, 38, 41, 74, 61, 155], [246, 11, 11, 97, 218, 57, 209, 72, 229, 27, 250, 73, 19, 64, 25, 62], [60, 162, 1, 110, 191, 130, 120, 227, 214, 98, 165, 245, 28, 55, 94, 190], [129, 212, 185, 156, 119, 239, 83, 221, 4, 174, 65, 218, 32, 211, 213, 223], [80, 218, 135, 245, 238, 127, 55, 68, 113, 145, 110, 59, 50, 177, 159, 146], [68, 239, 36, 166, 206, 23, 59, 126, 67, 152, 99, 189, 133, 113, 243, 198]])
b = vector(ZZ,[88, 74, 219, 244, 81, 109, 81, 216, 125, 218, 170, 56, 152, 229, 204, 45])
c1 = Matrix(ZZ,[[173, 2, 67, 11, 40, 80, 187, 38, 16, 226, 243, 79, 117, 127, 100, 113], [208, 231, 211, 196, 2, 146, 35, 2, 221, 119, 12, 25, 208, 152, 83, 201], [154, 43, 180, 76, 235, 5, 179, 196, 206, 171, 98, 145, 92, 144, 247, 98], [121, 145, 123, 232, 87, 78, 181, 145, 79, 166, 112, 169, 208, 102, 201, 63], [204, 141, 165, 225, 213, 137, 40, 43, 229, 151, 72, 237, 58, 15, 2, 31], [35, 114, 241, 31, 122, 123, 164, 231, 197, 89, 41, 236, 128, 22, 152, 82], [141, 133, 235, 79, 43, 120, 209, 231, 58, 85, 3, 44, 73, 245, 227, 62], [28, 158, 71, 41, 152, 32, 91, 200, 163, 46, 19, 121, 23, 209, 25, 55], [156, 17, 218, 146, 231, 242, 91, 76, 217, 57, 100, 212, 243, 87, 62, 159], [100, 111, 107, 62, 106, 72, 51, 79, 223, 93, 86, 145, 192, 21, 218, 243], [196, 250, 248, 166, 155, 39, 7, 93, 103, 54, 168, 188, 190, 104, 183, 64], [16, 131, 148, 193, 19, 149, 179, 212, 109, 170, 201, 168, 165, 167, 68, 25], [30, 222, 171, 32, 141, 105, 232, 104, 198, 53, 50, 157, 206, 165, 200, 42], [90, 149, 148, 112, 142, 228, 231, 119, 235, 248, 233, 9, 242, 102, 241, 93], [150, 32, 78, 183, 68, 249, 80, 165, 95, 229, 211, 0, 75, 14, 172, 139], [175, 69, 15, 100, 113, 63, 123, 71, 24, 250, 135, 232, 53, 32, 81, 117]])
c2 = vector(ZZ,[18, 67, 187, 237, 99, 127, 128, 23, 83, 66, 64, 69, 7, 214, 43, 156])
p = 251
A = Matrix(Zmod(p), A)
b = vector(Zmod(p), b)
c1 = Matrix(Zmod(p), c1)
c2 = vector(Zmod(p), c2)
A_inv=A.inverse()
x=c1*A_inv
print(x)
m=b''
tmp=c2-x*b
for i in tmp:
if i >=125:
m+=b'1'
else :
m+=b'0'
print(long_to_bytes(int(m,2)).hex())
#21c4

Flag1: GITEA 服务器

登录GITEA服务器,分析口令加密代码,解密口令密文,并使用口令明文解压协同签名源码文件。

题目代码如下:

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
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>

#define ROUND 16

//S-Box 16x16
int sBox[16] =
{
2, 10, 4, 12,
1, 3, 9, 14,
7, 11, 8, 6,
5, 0, 15, 13
};

// 将十六进制字符串转换为 unsigned char 数组
void hex_to_bytes(const char* hex_str, unsigned char* bytes, size_t bytes_len) {
size_t hex_len = strlen(hex_str);
if (hex_len % 2 != 0 || hex_len / 2 > bytes_len) {
fprintf(stderr, "Invalid hex string length.\n");
return;
}

for (size_t i = 0; i < hex_len / 2; i++) {
sscanf(hex_str + 2 * i, "%2hhx", &bytes[i]);
}
}

// 派生轮密钥
void derive_round_key(unsigned int key, unsigned char *round_key, int length) {

unsigned int tmp = key;
for(int i = 0; i < length / 16; i++)
{
memcpy(round_key + i * 16, &tmp, 4); tmp++;
memcpy(round_key + i * 16 + 4, &tmp, 4); tmp++;
memcpy(round_key + i * 16 + 8, &tmp, 4); tmp++;
memcpy(round_key + i * 16 + 12, &tmp, 4); tmp++;
}
}

// 比特逆序
void reverseBits(unsigned char* state) {
unsigned char temp[16];
for (int i = 0; i < 16; i++) {
unsigned char byte = 0;
for (int j = 0; j < 8; j++) {
byte |= ((state[i] >> j) & 1) << (7 - j);
}
temp[15 - i] = byte;
}
for (int i = 0; i < 16; i++) {
state[i] = temp[i];
}
}
void sBoxTransform(unsigned char* state) {
for (int i = 0; i < 16; i++) {
int lo = sBox[state[i] & 0xF];
int hi = sBox[state[i] >> 4];
state[i] = (hi << 4) | lo;
}
}
void leftShiftBytes(unsigned char* state) {
unsigned char temp[16];
for (int i = 0; i < 16; i += 4) {
temp[i + 0] = state[i + 2] >> 5 | (state[i + 1] << 3);
temp[i + 1] = state[i + 3] >> 5 | (state[i + 2] << 3);
temp[i + 2] = state[i + 0] >> 5 | (state[i + 3] << 3);
temp[i + 3] = state[i + 1] >> 5 | (state[i + 0] << 3);
}
for (int i = 0; i < 16; i++)
{
state[i] = temp[i];
}
}

// 轮密钥加
void addRoundKey(unsigned char* state, unsigned char* roundKey, unsigned int round) {
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 8; j++) {
state[i] ^= ((roundKey[i + round * 16] >> j) & 1) << j;
}
}
}

// 加密函数
void encrypt(unsigned char* password, unsigned int key, unsigned char* ciphertext) {
unsigned char roundKeys[16 * ROUND] = {}; //

// 生成轮密钥
derive_round_key(key, roundKeys, 16 * ROUND);

// 初始状态为16字节的口令
unsigned char state[16]; // 初始状态为16字节的密码
memcpy(state, password, 16); // 初始状态为密码的初始值

// 迭代加密过程
for (int round = 0; round < ROUND; round++)
{
reverseBits(state);
sBoxTransform(state);
leftShiftBytes(state);
addRoundKey(state, roundKeys, round);
}

memcpy(ciphertext, state, 16);
}

void main() {
unsigned char password[] = "pwd:0123456789ab"; // 口令明文固定以pwd:开头,16字节的口令
unsigned int key = 0xF0FFFFFF; // 4字节的密钥
unsigned char ciphertext[16]; // 16字节的状态

printf("Password: \n");
printf("%s\n", password);

encrypt(password, key, ciphertext);

// 输出加密后的结果
printf("Encrypted password:\n");
for (int i = 0; i < 16; i++) {
printf("%02X", ciphertext[i]);
}
printf("\n");
}

该题实现了一个自定义的对称分组密码

为了与其进行对比,同时对后续sm4的学习打一下基础,这里首先回顾一下AES-ECB模式的流程

对于AES-128

共有10轮操作


Step1 Key Expansion(密钥扩展)

例如

1
128 bit Key

扩展成

1
2
3
4
5
6
7
8
9
11个Round Key

Round0

Round1

...

Round10

因为

1
2
3
4
5
初始轮

+

10轮

共需要

1
11个Key

Step2 初始轮(AddRoundKey)

状态矩阵state(即明文按列优先规则放置的一个4*4的块)

1
2
3
4
5
State



RoundKey0

得到

1
State

Step3 第1~9轮

每轮执行四个步骤:

(1)SubBytes

S盒替换

1
2
3
4
5
6
7
8
9
每个Byte



SBox查表



新的Byte

主要提供非线性


(2)ShiftRows

第二行

左移1

第三行

左移2

第四行

左移3

例如

原来

1
2
3
4
a b c d
e f g h
i j k l
m n o p

变成

1
2
3
4
a b c d
f g h e
k l i j
p m n o

实现字节扩散。


(3)MixColumns

每列看作

GF(2^8)

上的四维向量

乘固定矩阵

得到新的列。

作用:

增加扩散性。


(4)AddRoundKey

再与本轮Key异或

1
2
3
4
5
State



RoundKey_i

Step4 最后一轮

最后一轮

1
2
3
4
5
6
7
8
9
SubBytes



ShiftRows



AddRoundKey

没有MixColumns。

了解完AES的标准流程后,我们来看一下这道题的核心流程:

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
void encrypt(unsigned char* password, unsigned int key, unsigned char* ciphertext) {
unsigned char roundKeys[16 * ROUND] = {}; //

// 生成轮密钥
derive_round_key(key, roundKeys, 16 * ROUND);

// 初始状态为16字节的口令
unsigned char state[16]; // 初始状态为16字节的密码
memcpy(state, password, 16); // 初始状态为密码的初始值

// 迭代加密过程
for (int round = 0; round < ROUND; round++)
{
reverseBits(state);
sBoxTransform(state);
leftShiftBytes(state);
addRoundKey(state, roundKeys, round);
}

memcpy(ciphertext, state, 16);
}
int main() {
unsigned char password[] = "pwd:0123456789ab"; // 口令明文固定以pwd:开头,16字节的口令
unsigned int key = 0xF0FFFFFF; // 4字节的密钥
unsigned char ciphertext[16]; // 16字节的状态

printf("Password: \n");
printf("%s\n", password);

encrypt(password, key, ciphertext);

// 输出加密后的结果
printf("Encrypted password:\n");
for (int i = 0; i < 16; i++) {
printf("%02X", ciphertext[i]);
}
printf("\n");

return 0;
}

与AES相比,这题的key只有32bits(可以爆破,具体来说,搓一个解密脚本 然后对每一种key,解出来的明文是否是pwd:开头即可)

对于核心加密函数,内部进行了16轮操作,每轮操作按顺序分别进行:

比特逆序

1
2
3
4
5
6
7
8
9
10
11
12
13
void reverseBits(unsigned char* state) {
unsigned char temp[16];
for (int i = 0; i < 16; i++) {
unsigned char byte = 0;
for (int j = 0; j < 8; j++) {
byte |= ((state[i] >> j) & 1) << (7 - j);
}
temp[15 - i] = byte;
}
for (int i = 0; i < 16; i++) {
state[i] = temp[i];
}
}

实现了整个数组的前后翻转和每个字节内部的bit反转,逆向时只需要直接copy即可

S盒替换

1
2
3
4
5
6
7
void sBoxTransform(unsigned char* state) {
for (int i = 0; i < 16; i++) {
int lo = sBox[state[i] & 0xF];
int hi = sBox[state[i] >> 4];
state[i] = (hi << 4) | lo;
}
}

因为本题的S盒只有4*4,所以这里采用了这样的方法进行处理,即对每一个字节将其拆成高4bits和低4bits,分别S盒替换

字节位移

1
2
3
4
5
6
7
8
9
10
11
12
13
void leftShiftBytes(unsigned char* state) {
unsigned char temp[16];
for (int i = 0; i < 16; i += 4) {
temp[i + 0] = state[i + 2] >> 5 | (state[i + 1] << 3);
temp[i + 1] = state[i + 3] >> 5 | (state[i + 2] << 3);
temp[i + 2] = state[i + 0] >> 5 | (state[i + 3] << 3);
temp[i + 3] = state[i + 1] >> 5 | (state[i + 0] << 3);
}
for (int i = 0; i < 16; i++)
{
state[i] = temp[i];
}
}

很神秘的自定义操作

轮密钥加

1
2
3
4
5
6
7
void addRoundKey(unsigned char* state, unsigned char* roundKey, unsigned int round) {
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 8; j++) {
state[i] ^= ((roundKey[i + round * 16] >> j) & 1) << j;
}
}
}

和AES的轮密钥加相同,将state和roudkey做异或

总的看下来没有什么很明显的漏洞点,主要就是key的长度相对较短,可以爆破

此外,看到官方的wp分享,我们可以采用下面的方法加快运行速度:

1.使用更高级的编译优化方式来加快运行速度:

-O0:不优化,方便单步调试。

-O2:较全面的优化,通常是性能和编译时间的良好平衡。

-O3:更激进,程序不一定总比 -O2 快。

-g:生成调试信息。

-Wall -Wextra:开启常用编译警告。

-static:静态链接 MinGW 运行库,使 EXE 不依赖对应 DLL。

2.针对字节逆序代码 由于函数是在内部处理每个字节的逆序和字符串的逆序的 效率较低,因此可以将字节逆序做预处理,函数内部只做字符串前后逆序

这边训练一下搓c++多线程脚本的能力

一般情况下,两种类型的多任务处理:基于进程和基于线程

  • 基于进程的多任务处理是程序的并发执行。
  • 基于线程的多任务处理是同一程序的片段的并发执行。

线程 (Thread)

  • 线程是程序执行中的单一顺序控制流,多个线程可以在同一个进程中独立运行。
  • 线程共享进程的地址空间、文件描述符、堆和全局变量等资源,但每个线程有自己的栈、寄存器和程序计数器。

并发 与并行

  • 并发:多个任务在时间片段内交替执行,表现出同时进行的效果。
  • 并行:多个任务在多个处理器或处理器核上同时执行。

C++11 及以后的标准提供了多线程支持,核心组件包括:

  • std::thread:用于创建和管理线程。
  • std::mutex:用于线程之间的互斥,防止多个线程同时访问共享资源。
  • std::lock_guardstd::unique_lock:用于管理锁的获取和释放。
  • std::condition_variable:用于线程间的条件变量,协调线程间的等待和通知。
  • std::futurestd::promise:用于实现线程间的值传递和任务同步。

创建多个线程

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <thread>

void work(int id)
{
std::cout << id << std::endl;
}

int main()
{
std::thread t1(work,1);
std::thread t2(work,2);
std::thread t3(work,3);

t1.join();
t2.join();
t3.join();
}

然而 如果要创建的线程很多,总不能像这样一个一个写,因此可以用vector管理线程:

1
2
3
4
5
std::vector<std::thread> pool;
for(int i=0;i<8;i++)
{
pool.emplace_back(work,i);
}

这里emplace_back和push_back功能类似,但是效率更高

这段代码能够自动创建多个线程:

1
2
3
4
5
6
7
thread0

thread1

...

thread7

全部创建。

然后:

1
2
for(auto &t:pool)
t.join();

等待全部结束。

解题代码如下:

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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#include "stdlib.h"
#include <stdio.h>
#include <string.h>
#include <thread>
#include <vector>
#define ROUND 16

int sBox[16] = {2, 10, 4, 12, 1, 3, 9, 14, 7, 11, 8, 6, 5, 0, 15, 13};
int rBox[16] = {13, 4, 0, 5, 2, 12, 11, 8, 10, 6, 1, 9, 3, 15, 7, 14};

// 将十六进制字符串转换为 unsigned char 数组
void hex_to_bytes(const char *hex_str, unsigned char *bytes, size_t bytes_len) {
size_t hex_len = strlen(hex_str);
if (hex_len % 2 != 0 || hex_len / 2 > bytes_len) {
fprintf(stderr, "Invalid hex string length.\n");
return;
}

for (size_t i = 0; i < hex_len / 2; i++) {
sscanf(hex_str + 2 * i, "%2hhx", &bytes[i]);
}
}

// 派生轮密钥
void derive_round_key(unsigned int key, unsigned char *round_key, int length) {

unsigned int tmp = key;
for (int i = 0; i < length / 16; i++) {
memcpy(round_key + i * 16, &tmp, 4);
tmp++;
memcpy(round_key + i * 16 + 4, &tmp, 4);
tmp++;
memcpy(round_key + i * 16 + 8, &tmp, 4);
tmp++;
memcpy(round_key + i * 16 + 12, &tmp, 4);
tmp++;
}
}

// 比特逆序(这里做了优化:字节预处理)
int rb[256] = {0};
void reverseBits(unsigned char* state) {
unsigned char temp[16];
for (int i = 0; i < 16; i++) {
temp[15 - i] = rb[state[i]];
}
for (int i = 0; i < 16; i++) {
state[i] = temp[i];
}
}

void sBoxTransform(unsigned char *state) {
for (int i = 0; i < 16; i++) {
int lo = sBox[state[i] & 0xF];
int hi = sBox[state[i] >> 4];
state[i] = (hi << 4) | lo;
}
}
void rBoxTransform(unsigned char *state) {
for (int i = 0; i < 16; i++) {
int lo = rBox[state[i] & 0xF];
int hi = rBox[state[i] >> 4];
state[i] = (hi << 4) | lo;
}
}

void leftShiftBytes(unsigned char *state) {
unsigned char temp[16];
for (int i = 0; i < 16; i += 4) {
temp[i + 0] = state[i + 2] >> 5 | (state[i + 1] << 3);
temp[i + 1] = state[i + 3] >> 5 | (state[i + 2] << 3);
temp[i + 2] = state[i + 0] >> 5 | (state[i + 3] << 3);
temp[i + 3] = state[i + 1] >> 5 | (state[i + 0] << 3);
}
for (int i = 0; i < 16; i++) {
state[i] = temp[i];
}
}

void rightShiftBytes(unsigned char *state) {
unsigned char temp[16];
for (int i = 0; i < 16; i += 4) {
temp[i + 0] = (state[i + 2] << 5) | (state[i + 3] >> 3);
temp[i + 1] = (state[i + 0] >> 3) | (state[i + 3] << 5);
temp[i + 2] = (state[i + 0] << 5) | (state[i + 1] >> 3);
temp[i + 3] = (state[i + 1] << 5) | (state[i + 2] >> 3);
}
for (int i = 0; i < 16; i++) {
state[i] = temp[i];
}
}

// 轮密钥加
void addRoundKey(unsigned char *state, unsigned char *roundKey,
unsigned int round) {
for (int i = 0; i < 16; i++)
state[i] ^= roundKey[i + round * 16];
}

void dump(unsigned char *d) {
for (int i = 0; i < 16; i++) {
printf("%02X", d[i]);
}
printf("\n");
}

// 加密函数
void encrypt(unsigned char *password, unsigned int key,
unsigned char *ciphertext) {
unsigned char roundKeys[16 * ROUND] = {}; //

// 生成轮密钥
derive_round_key(key, roundKeys, 16 * ROUND);

// 初始状态为16字节的口令
unsigned char state[16]; // 初始状态为16字节的密码
memcpy(state, password, 16); // 初始状态为密码的初始值

// 迭代加密过程
for (int round = 0; round < ROUND; round++) {
reverseBits(state);
sBoxTransform(state);
leftShiftBytes(state);
addRoundKey(state, roundKeys, round);
}

memcpy(ciphertext, state, 16);
}

void decrypt(unsigned char *ciphertext, unsigned int key,
unsigned char *password) {
unsigned char roundKeys[16 * ROUND] = {};
derive_round_key(key, roundKeys, 16 * ROUND);
unsigned char state[16];
memcpy(state, ciphertext, 16);
for (int round = 15; round >= 0; round--) {
addRoundKey(state, roundKeys, round);
rightShiftBytes(state);
rBoxTransform(state);
reverseBits(state);
}
memcpy(password, state, 16);
}
//多线程处理函数worker
void worker(unsigned int begin,
unsigned int end,
unsigned char ciphertext[16])
{
unsigned char passback[17] = {0};

for (unsigned int key = begin; key != end; key++)
{
decrypt(ciphertext, key, passback);

if (passback[0]=='p' &&
passback[1]=='w' &&
passback[2]=='d' &&
passback[3]==':')
{
printf("\n========== FOUND ==========\n");
printf("key = %08X\n", key);
printf("password = %16s\n", passback);
printf("===========================\n");

return;
}
}
}

int main() {
for (int i = 0; i <= 255; i++) {
unsigned char byte = 0;
for (int j = 0; j < 8; j++) {
byte |= ((i >> j) & 1) << (7 - j);
}
rb[i] = byte;
}
unsigned char password[] =
"pwd:1234xxxxabcd"; // 口令明文固定以pwd:开头,16字节的口令
unsigned int key = 0xF0FFFFFF; // 4字节的密钥
unsigned char ciphertext[16]; // 16字节的状态
unsigned char passback[17] = {0};
printf("Password: \n");
printf("%s\n", password);

encrypt(password, key, ciphertext);
decrypt(ciphertext, key, passback);
printf("%s\n", passback); // 检验解密函数正确性


memcpy(ciphertext,
"\x99\xF2\x98\x0A\xAB\x4B\xE8\x64\x0D\x8F\x32\x21\x47\xCB\xA4\x09",
16);
unsigned int s = 0xF0000000;
unsigned int e = 0x00000000;

//下面是多线程部分
unsigned threads = 8;
printf("Using %u threads\n", threads);

std::vector<std::thread> pool;

uint64_t total =
(uint64_t)e + (1ULL<<32) - s;

uint64_t block = total / threads;

for(unsigned t=0;t<threads;t++)
{
uint64_t offset_begin = t * block;

uint64_t offset_end =
(t == threads-1)
? total
: (t+1)*block;

unsigned int L =
s + (unsigned int)offset_begin;

unsigned int R =
s + (unsigned int)offset_end;

pool.emplace_back(worker,
L,
R,
ciphertext);
}

for(auto &th : pool)
th.join();
}

测试结果:根据官方wp分享 赛事中应该有提示说key最高位是f,所以从0xf0000000开始

不做字节逆序预处理优化且顺序爆破需要6min

预处理优化+顺序爆破需要2min

预处理+多线程(8 threads)仅需5s,即使不是从0xf0000000开始爆,而是从0开始,也仅需4min即可遍历完整个空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
g++ -O2 flag1mul.cpp -o flag1off.exe
.\flag1off.exe
Password:
pwd:1234xxxxabcd
pwd:1234xxxxabcd
Using 8 threads

========== FOUND ==========
key = 94B05686
password = pwd:?TP1?
===========================

========== FOUND ==========
key = FAB7C4D9
password = pwd:%@PRjd8)k5TV
===========================

Flag2:协同签名源码文件

题目描述:

解压协同签名源码文件,分析协同签名算法客户端和服务端代码,寻找并利用协同签名算法缺陷,求解协同签名服务端私钥

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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
//协同签名客户端代码
const form = ref({
password: "",
msgdigest: "",
})

const k1: any = ref("");

const submit = () => {
isform.value.validate((valid: boolean) => {
if (valid) {

loading.value = true;
let smPassword = ref("");
smPassword.value = sm3(form.value.password);
// 客户端通过用户口令、消息摘要和用户私钥d1,计算客户端协同签名值 p1x, p1y, q1x, q1y, r1, s1
var { str_e, str_p1x, str_p1y, str_q1x, str_q1y, str_r1, str_s1, errMessage } = clientSign1(smPassword.value, form.value.msgdigest);
if (errMessage) {
ElMessage.error(errMessage)
loading.value = false;
return
}
let data = {
q1x: str_q1x,
q1y: str_q1y,
e: str_e,
r1: str_r1,
s1: str_s1,
p1x: str_p1x,
p1y: str_p1y
}
// 客户端将 e, p1x, p1y, q1x, q1y, r1, s1发送给服务端
// 服务端用服务端私钥d2计算服务端协同签名值 s2, s3, r 发送给客户端
sign_param_send(data).then((res: any) => {
// 客户端通过s2, s3, r,计算协同签名值 s
let str_s: any = clientSign2(smPassword.value, res.s2, res.s3, res.r);
if (str_s.errMessage) {
ElMessage.error(errMessage)
loading.value = false;
return
}
ElMessage.success("协同签名成功");
signature_send({ client_sign: str_s }).then((res: any) => {
qmz.value = str_s;
loading.value = false;
}).then((err: any) => {
loading.value = false;
})
}).catch((err: any) => {
loading.value = false;
})
}
})
}
const clientSign1: any = (str_d1: any, str_e: any) => {
let d1 = new BN(str_d1, 16);
// console.log("e",str_e)

let e = new BN(str_e, 16);
// console.log("e",e)
const sm2: any = new elliptic.curve.short({
p: 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF',
a: 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC',
b: '28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93',
n: 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123',
g: [
'32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7',
'BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0'
]
} as any);

let n = new BN(sm2.n.toString(16), 16);
let G = sm2.g;

// generate random k1
const randomBytes = cryptoRandomStringAsync({ length: 64 });
k1.value = new BN(randomBytes as any, 16);
while(k1.value.mod(n).isZero()){
const randomBytes = cryptoRandomStringAsync({ length: 64 });
k1.value = new BN(randomBytes as any, 16);
}
k1.value = k1.value.mod(n);

// d1 = d1 mod n
d1 = d1.mod(n);
if (d1.isZero()) {
let errMessage = "d1=0,签名失败"
return { errMessage }
}

//P1 = ((d1)^(-1)) * G
let tmp1 = d1.invm(n);
let P1 = G.mul(tmp1);

//Q1 = k1*G = (x, y)
let Q1 = G.mul(k1.value);
let x = new BN(Q1.getX().toString(16), 16);

//r1 = x mod n
let r1 = x.mod(n);
if (r1.isZero()) {
let errMessage = "r1=0,签名失败"
return { errMessage }
}

//s1 = k1^(-1) * (e + d1^(-1) * r1) mod n
tmp1 = d1.invm(n);
let tmp2 = tmp1.mul(r1).mod(n);
let tmp3 = tmp2.add(e).mod(n);
tmp1 = k1.value.invm(n);
let s1 = tmp1.mul(tmp3).mod(n);
if (s1.isZero()) {
let errMessage = "s1=0,签名失败"
return { errMessage }
}

str_e = e.toString(16);
// console.log("str_e",str_e)
let str_p1x = P1.getX().toString(16);
let str_p1y = P1.getY().toString(16);
let str_q1x = Q1.getX().toString(16);
let str_q1y = Q1.getY().toString(16);
let str_r1 = r1.toString(16);
let str_s1 = s1.toString(16);
return { str_e, str_p1x, str_p1y, str_q1x, str_q1y, str_r1, str_s1 }
}
const clientSign2 = (str_d1: any, str_s2: any, str_s3: any, str_r: any) => {
const sm2 = new elliptic.curve.short({
p: 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF',
a: 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC',
b: '28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93',
n: 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123',
g: [
'32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7',
'BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0'
]
} as any);

let d1 = new BN(str_d1, 16);
let n = new BN(sm2.n.toString(16), 16);
let s2 = new BN(str_s2, 16);
let s3 = new BN(str_s3, 16);
let r = new BN(str_r, 16);
//s = d1*k1*s2 + d1*s3 -r mod n
let tmp1 = d1.mul(k1.value).mod(n);
let tmp2 = tmp1.mul(s2).mod(n);
let tmp3 = d1.mul(s3).mod(n);
tmp1 = tmp2.add(tmp3).mod(n);
let s = tmp1.sub(r).mod(n);
if (s.isZero()) {
let errMessage = "s=0,签名失败"
return { errMessage }
}
if (s.add(r).mod(n).isZero()) {
let errMessage = "s=n-r,签名失败"
return { errMessage }
}
let str_s = s.toString(16);
if (str_s[0] == '-') {
s = s.add(n).mod(n);
str_s = s.toString(16);
}
return str_s;
}

先对客户端代码进行一个解读,这边有一些vue3和typescript的代码 之前没有接触过,这里和AI做了下交流,进行一个记录:

1
2
3
4
5
6
const form = ref({
password: "",
msgdigest: "",
})

const k1: any = ref("");

这段代码中包含了

Vue3 的 ref()

TypeScript 的类型标注 : any

ref是一个响应式函数,能够在变量更新后在浏览器页面自动更新(重新渲染)

而any表示变量k1没有类型,任何类型的数据都是合法的,但是后面用的是””,所以应该是字符串类型

再往下读发现vue的代码要全部搞懂语法比较困难,于是就主要关注注释和主要逻辑了

首先是一个submit提交函数:

1
2
smPassword.value = sm3(form.value.password);
var { str_e, str_p1x, str_p1y, str_q1x, str_q1y, str_r1, str_s1, errMessage } = clientSign1(smPassword.value, form.value.msgdigest);

这里首先把用户的口令通过sm3计算其消息摘要作为smpassword

然后利用函数lientsign1的输出对这些变量进行赋值

看到client sign1函数:

首先需要声明,这里的所有BN都代表大整数的意思

然后捋一下clientsign1的核心操作

这里的e是消息摘要,d1是私钥,k1是生成的一个随机数,因此最后能够呈现的信息是点P,Q,签名(r,s),e

现在回到提交函数:

看注释:

客户端将 e, p1x, p1y, q1x, q1y, r1, s1发送给服务端

服务端用服务端私钥d2计算服务端协同签名值 s2, s3, r 发送给客户端

再看client sign2函数:

还是用的和sign1相同的曲线参数,核心操作:

利用服务器传过来的这些数据计算协同签名值s

看到目前没有发现什么可疑的点,再看下服务端代码:

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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
//协同签名服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <openssl/ec.h>
#include <openssl/rand.h>

#define SM2LEN 32

int error() {
printf("Error.\n");
return 0;
}

int error_partial_verify() {
printf("Error partial verify.\n");
return 0;
}

void print_flag2(const BIGNUM *d2) {
char *hex_str = BN_bn2hex(d2);
for (int i = 0; hex_str[i] != '\0'; i++) {
if (hex_str[i] >= 'A' && hex_str[i] <= 'F') {
hex_str[i] += 32;
}
}
printf("flag2{%s}\n", hex_str);
}

typedef struct {
char s2[SM2LEN * 2 + 1];
char s3[SM2LEN * 2 + 1];
char r[SM2LEN * 2 + 1];
int success;
} Result;

// 协同签名服务端签名算法
Result server(char* str_e,char* str_p1x,char* str_p1y,char* str_q1x,char* str_q1y,char* str_r1,char* str_s1){
Result res = {"", "", "", 0};

int rv = 1;
BIGNUM *e,*a,*b,*p,*n,*x,*y;
BIGNUM *d2,*r1,*s1,*p1x,*p1y,*q1x,*q1y;
BIGNUM *u1,*u2,*xprime,*yprime,*k2,*k3,*x1,*y1,*r,*s2,*s3,*s,*tmp1,*tmp2,*tmp3;
EC_GROUP* group;
EC_POINT *generator,*G,*P,*P1,*Q1,*TMP;

BN_CTX* bn_ctx = BN_CTX_new();
BN_CTX_start(bn_ctx);
if (!bn_ctx)
{ error(); return res; }
e = BN_CTX_get(bn_ctx);
a = BN_CTX_get(bn_ctx);
b = BN_CTX_get(bn_ctx);
p = BN_CTX_get(bn_ctx);
n = BN_CTX_get(bn_ctx);
d2 = BN_CTX_get(bn_ctx);
x = BN_CTX_get(bn_ctx);
y = BN_CTX_get(bn_ctx);
p1x = BN_CTX_get(bn_ctx);
p1y = BN_CTX_get(bn_ctx);
q1x = BN_CTX_get(bn_ctx);
q1y = BN_CTX_get(bn_ctx);
r1 = BN_CTX_get(bn_ctx);
s1 = BN_CTX_get(bn_ctx);
u1 = BN_CTX_get(bn_ctx);
u2 = BN_CTX_get(bn_ctx);
xprime = BN_CTX_get(bn_ctx);
yprime = BN_CTX_get(bn_ctx);
k2 = BN_CTX_get(bn_ctx);
k3 = BN_CTX_get(bn_ctx);
x1 = BN_CTX_get(bn_ctx);
y1 = BN_CTX_get(bn_ctx);
r = BN_CTX_get(bn_ctx);
s2 = BN_CTX_get(bn_ctx);
s3 = BN_CTX_get(bn_ctx);
s = BN_CTX_get(bn_ctx);
tmp1 = BN_CTX_get(bn_ctx);
tmp2 = BN_CTX_get(bn_ctx);
tmp3 = BN_CTX_get(bn_ctx);

if (
!BN_hex2bn(&e, str_e) ||
!BN_hex2bn(&p1x, str_p1x) ||
!BN_hex2bn(&p1y, str_p1y) ||
!BN_hex2bn(&q1x, str_q1x) ||
!BN_hex2bn(&q1y, str_q1y) ||
!BN_hex2bn(&r1, str_r1) ||
!BN_hex2bn(&s1, str_s1) ||
!BN_hex2bn(&a, "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC") ||
!BN_hex2bn(&b, "28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93") ||
!BN_hex2bn(&p, "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF") ||
!BN_hex2bn(&n, "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123") ||
// d2 = ds (server key)
!BN_hex2bn(&d2, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") ||
!BN_hex2bn(&x, "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7") ||
!BN_hex2bn(&y, "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0") ||
!BN_rand_range(k2,n) ||
!BN_copy(k3, k2)
)
{ error(); return res; }

// generate k2 in [1, n-1]
while(BN_is_zero(k2)){
if (
!BN_rand_range(k2,n) ||
!BN_copy(k3, k2)
)
{ error(); return res; }
}

group = EC_GROUP_new_curve_GFp(p, a, b, bn_ctx);
generator = EC_POINT_new(group);
if (!generator)
{ error(); return res; }
if (1 != EC_POINT_set_affine_coordinates_GFp(group, generator, x, y, bn_ctx))
{ error(); return res; }
if (1 != EC_GROUP_set_generator(group, generator, n, NULL))
{ error(); return res; }

G = EC_POINT_new(group);
P = EC_POINT_new(group);
P1 = EC_POINT_new(group);
Q1 = EC_POINT_new(group);
TMP = EC_POINT_new(group);

// if r1=0 or s1=0, error
if (BN_is_zero(r1) || BN_is_zero(s1))
{ error(); return res; }

// set P1 = (p1x, p1y)
if (1 != EC_POINT_set_affine_coordinates_GFp(group, P1, p1x, p1y, bn_ctx))
{ error(); return res; }

// set Q1 = (q1x, q1y)
if (1 != EC_POINT_set_affine_coordinates_GFp(group, Q1, q1x, q1y, bn_ctx))
{ error(); return res; }

//u1 = e * (s1^(-1)) mod n, u2 = r1 * (s1^(-1)) mod n
if (!BN_mod_inverse(tmp1, s1, n, bn_ctx) ||
!BN_mod_mul(u1, e, tmp1, n, bn_ctx) ||
!BN_mod_mul(u2, r1, tmp1, n, bn_ctx) ||
!BN_mod(u1, u1, n, bn_ctx) ||
!BN_mod(u2, u2, n, bn_ctx)
)
{ error(); return res; }

//u1*G + u2*P1 = (x', y')
if (!EC_POINT_mul(group, TMP, u1, P1, u2, bn_ctx))
{ error(); return res; }

if (!EC_POINT_get_affine_coordinates_GFp(group, TMP, xprime, yprime, bn_ctx))
{ error(); return res; }

// verify r1 = x' mod n
if (!BN_mod(xprime, xprime, n, bn_ctx))
{ error(); return res; }

if(BN_cmp(r1,xprime))
{ error_partial_verify(); return res; }

//k2*G + k3*Q1 = (x1, y1)
if (!EC_POINT_mul(group, TMP, k2, Q1, k3, bn_ctx))
{ error(); return res; }

if (!EC_POINT_get_affine_coordinates_GFp(group, TMP, x1, y1, bn_ctx))
{ error(); return res; }

// r=(e+x1) mod n
if (!BN_mod_add(r, e, x1, n, bn_ctx))
{ error(); return res; }

if (BN_is_zero(r))
{ error(); return res; }
strncpy(res.r, BN_bn2hex(r), 2*SM2LEN+1);

//s2 = d2 * k3 mod n, s3 = d2 * (r+k2) mod n
if (!BN_mod_mul(s2, d2, k3, n, bn_ctx) ||
!BN_mod_add(tmp1, r, k2, n, bn_ctx) ||
!BN_mod_mul(s3, d2, tmp1, n, bn_ctx) ||
!BN_mod(s2, s2, n, bn_ctx) ||
!BN_mod(s3, s3, n, bn_ctx)
)
{ error(); return res; }
printf("s2: %s\n",BN_bn2hex(s2));
printf("s3: %s\n",BN_bn2hex(s3));
strncpy(res.s2, BN_bn2hex(s2), 2*SM2LEN+1);
strncpy(res.s3, BN_bn2hex(s3), 2*SM2LEN+1);

// flag2 的格式如下:flag2{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx},大括号中的内容为 16 进制格式(字母小写)的 d2。
print_flag2(d2);

rv = 0;
BN_CTX_free(bn_ctx);

return rv;
}

// 计算公钥P
int getPublicKey(char *str_d2, char *str_p1x, char *str_p1y) {
int rv = 1;
BIGNUM *negone, *a, *b, *p, *n, *x, *y;
BIGNUM *d2, *p1x, *p1y, *px, *py;
BIGNUM *tmp1, *tmp2;
EC_GROUP *group;
EC_POINT *generator, *G, *P, *P1;

BN_CTX *bn_ctx = BN_CTX_new();
BN_CTX_start(bn_ctx);
if (!bn_ctx) {
error();
return 1;
}

negone = BN_CTX_get(bn_ctx);
a = BN_CTX_get(bn_ctx);
b = BN_CTX_get(bn_ctx);
p = BN_CTX_get(bn_ctx);
n = BN_CTX_get(bn_ctx);
d2 = BN_CTX_get(bn_ctx);
x = BN_CTX_get(bn_ctx);
y = BN_CTX_get(bn_ctx);
p1x = BN_CTX_get(bn_ctx);
p1y = BN_CTX_get(bn_ctx);
px = BN_CTX_get(bn_ctx);
py = BN_CTX_get(bn_ctx);
tmp1 = BN_CTX_get(bn_ctx);
tmp2 = BN_CTX_get(bn_ctx);

if (
!BN_hex2bn(&d2, str_d2) ||
!BN_hex2bn(&p1x, str_p1x) ||
!BN_hex2bn(&p1y, str_p1y) ||
!BN_hex2bn(&a, "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC") ||
!BN_hex2bn(&b, "28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93") ||
!BN_hex2bn(&p, "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF") ||
!BN_hex2bn(&n, "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123") ||
!BN_hex2bn(&x, "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7") ||
!BN_hex2bn(&y, "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0")
) {
error();
return 1;
}
group = EC_GROUP_new_curve_GFp(p, a, b, bn_ctx);
generator = EC_POINT_new(group);
if (!generator) {
error();
return 1;
}
if (1 != EC_POINT_set_affine_coordinates_GFp(group, generator, x, y, bn_ctx)) {
error();
return 1;
}
if (1 != EC_GROUP_set_generator(group, generator, n, NULL)) {
error();
return 1;
}

G = EC_POINT_new(group);
P = EC_POINT_new(group);
P1 = EC_POINT_new(group);

// set P1 = (p1x, p1y)
if (1 != EC_POINT_set_affine_coordinates_GFp(group, P1, p1x, p1y, bn_ctx)) {
error();
return 1;
}

//P = ((d2)^(-1)) * P1 - G
if (!BN_zero(tmp1) ||
!BN_one(tmp2) ||
!BN_mod_sub(negone, tmp1, tmp2, n, bn_ctx)
) {
error();
return 1;
}
if (!BN_mod_inverse(tmp1, d2, n, bn_ctx) || !EC_POINT_mul(group, P, negone, P1, tmp1, bn_ctx)) {
error();
return 1;
}

if (!EC_POINT_get_affine_coordinates_GFp(group, P, px, py, bn_ctx)) {
error();
return 1;
}
printf("Px: %s\n", BN_bn2hex(px));
printf("Py: %s\n", BN_bn2hex(py));

rv = 0;
BN_CTX_free(bn_ctx);

return rv;
}

int main(int argc, char *argv[]) {
int rv = 1;
if (server(argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7])) {
error();
return rv;
}

rv = 0;
return rv;
}

我们首先看到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (
!BN_hex2bn(&e, str_e) ||
!BN_hex2bn(&p1x, str_p1x) ||
!BN_hex2bn(&p1y, str_p1y) ||
!BN_hex2bn(&q1x, str_q1x) ||
!BN_hex2bn(&q1y, str_q1y) ||
!BN_hex2bn(&r1, str_r1) ||
!BN_hex2bn(&s1, str_s1) ||
!BN_hex2bn(&a, "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC") ||
!BN_hex2bn(&b, "28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93") ||
!BN_hex2bn(&p, "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF") ||
!BN_hex2bn(&n, "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123") ||
// d2 = ds (server key)
!BN_hex2bn(&d2, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") ||
!BN_hex2bn(&x, "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7") ||
!BN_hex2bn(&y, "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0") ||
!BN_rand_range(k2,n) ||
!BN_copy(k3, k2)
)
{ error(); return res; }

这里是在对消息能否正确转换类型做检查

可以看到最后有一个BN_copy,所以说正常情况下,k2=k3

服务器首先会对客户端传过来的签名进行常规的sm2验签流程(不过好像跟核心流程关系不大),然后做了个这样的操作:

1
2
3
4
5
6
7
8
9
10
//k2*G + k3*Q1 = (x1, y1)
if (!EC_POINT_mul(group, TMP, k2, Q1, k3, bn_ctx))
{ error(); return res; }

if (!EC_POINT_get_affine_coordinates_GFp(group, TMP, x1, y1, bn_ctx))
{ error(); return res; }

// r=(e+x1) mod n
if (!BN_mod_add(r, e, x1, n, bn_ctx))
{ error(); return res; }

然后将相关的值打印出来:

根据官方wp分享,服务端会给出r,s2,s3的值,又因为刚才审计发现k2=k3,因此可以用下面的方法得到d2:

解题代码比较简单 这里就不贴了(我说这里代码审计30+分钟 解题1分钟)

Flag3:数据库管理系统 [登录]

题目描述:

分析数据库管理系统服务端登录校验代码,寻找并利用登录校验缺陷,登录数据库管理系统。

题目代码:

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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package controllers

import (
"crypto/ecdsa"
"encoding/hex"
"encoding/pem"
"fmt"
jwtgo "github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/tjfoc/gmsm/sm2"
"github.com/tjfoc/gmsm/x509"
"http_svr/config"
"http_svr/models"
"http_svr/utils"
"math/big"
"net/http"
"time"
)

// 加载证书
func loadCertificate(certPEM string) (*x509.Certificate, error) {
//certPEM := "-----BEGIN CERTIFICATE-----\nMIIBQDCB6KADAgECAgECMAoGCCqBHM9VAYN1MBIxEDAOBgNVBAoTB1Jvb3QgQ0Ew\nHhcNMjQwNzI0MDkyMTI5WhcNMjUwNzI0MDkyMTI5WjAaMRgwFgYDVQQKEw9NeSBP\ncmdhbml6YXRpb24wWTATBgcqhkjOPQIBBggqgRzPVQGCLQNCAASlPepwTvt5c4rF\nEsg1Mqs+Tyx/BwRkwyWqDyZd/gBFKp7veuoZnGK11c24xPOqR/eQZNW7ugsZW6eb\nLyXSsE9ooycwJTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEw\nCgYIKoEcz1UBg3UDRwAwRAIgG4/snkgUCW819OotUWUfMOo0BzHX8KeTTUSLpIjy\nEO4CIEq6X7h3nVNeFzdtLWdy5+1MeNwsWawHU5YzITsNtqOe\n-----END CERTIFICATE-----\n"
block, _ := pem.Decode([]byte(certPEM))
if block == nil || block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("无效的证书格式")
}

return x509.ParseCertificate(block.Bytes)
}

// 验证证书
func validateCertificate(cert *x509.Certificate, rootCert *x509.Certificate) error {
// 检查颁发者
if cert.Issuer.CommonName != rootCert.Subject.CommonName {
return fmt.Errorf("证书校验失败")
}
// 检查颁发者组织
if len(cert.Issuer.Organization) != 1 || cert.Issuer.Organization[0] != rootCert.Subject.Organization[0] {
return fmt.Errorf("证书校验失败")
}
// 检查颁发者国家
if len(cert.Issuer.Country) != 1 || cert.Issuer.Country[0] != rootCert.Subject.Country[0] {
return fmt.Errorf("证书校验失败")
}

// 检查有效日期
if time.Now().Before(cert.NotBefore) || time.Now().After(cert.NotAfter) {
return fmt.Errorf("证书校验失败")
}

// 检查组织
if len(cert.Subject.Organization) != 1 || cert.Subject.Organization[0] != "ShangMiBei" {
return fmt.Errorf("证书校验失败")
}

// 检查组织单元
if len(cert.Subject.OrganizationalUnit) != 1 || cert.Subject.OrganizationalUnit[0] != "ShangMiBei2024" {
return fmt.Errorf("证书校验失败")
}

// 检查国家
if len(cert.Subject.Country) != 1 || cert.Subject.Country[0] != "CN" {
return fmt.Errorf("证书校验失败")
}

// 创建证书链
roots := x509.NewCertPool()
roots.AddCert(rootCert)

opts := x509.VerifyOptions{
Roots: roots,
CurrentTime: time.Now(),
}

// 验证证书链
if _, err := cert.Verify(opts); err != nil {
return fmt.Errorf("证书链校验失败: %v", err)
}

return nil
}

type SM2Signature struct {
R, S *big.Int
}

// 验证签名
func validateSignature(message, signature string, publicKey *sm2.PublicKey) (bool, error) {
//rawSignatureHex, err := base64.StdEncoding.DecodeString(base64EncodedSignature)
hexSignature, err := hex.DecodeString(signature)
if err != nil {
return false, fmt.Errorf("invalid signature format")
}

isValid := publicKey.Verify([]byte(message), hexSignature)
if isValid {
return true, nil
} else {
return false, fmt.Errorf("signature is invalid")
}
}

// Login 登录
func Login(c *gin.Context, conf config.Config) {
// 解析请求参数
var req models.LoginReq
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// 校验用户名是否已注册过
if _, exists := models.Users[req.Username]; !exists {
c.JSON(http.StatusBadRequest, gin.H{"error": "username not exists"})
return
}

// 校验随机字符串是否过期
randomStr, exists := conf.Cache.Get(req.Username)
if !exists {
c.JSON(http.StatusBadRequest, gin.H{"error": "random string has expired"})
return
}

// 校验证书
cert, err := loadCertificate(req.Cert)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := validateCertificate(cert, models.RootCert); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// 判断是否挑战成功(随机字符串的签名能否用证书中的公钥验签过)
ecdsaPubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "public key in cert is not sm2"})
return
}
sm2PubKey := sm2.PublicKey{
Curve: ecdsaPubKey.Curve,
X: ecdsaPubKey.X,
Y: ecdsaPubKey.Y,
}
isValid, err := validateSignature(randomStr.(string), req.Signature, &sm2PubKey)
if isValid {
//c.JSON(http.StatusOK, gin.H{"msg": "success", "flag3": config.Flag3, "download_url": config.DownloadUrl})
generateToken2(c, req.Username, conf)
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}

// 生成令牌
func generateToken2(c *gin.Context, username string, conf config.Config) {
j := &utils.JWT{
SigningKey: []byte(conf.SignKey),
}
claims := utils.CustomClaims{
Name: username,
StandardClaims: jwtgo.StandardClaims{
NotBefore: time.Now().Unix() - conf.NotBeforeTime, // 签名生效时间
ExpiresAt: time.Now().Unix() + conf.ExpiresTime, // 过期时间
Issuer: conf.Issuer, // 签名的发行者
},
}

token, err := j.CreateToken(claims)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 5091,
"msg": "登录失败,系统有误",
})
return
}

// 将当前用户对应的缓存中的随机字符串删除
conf.Cache.Delete(username)

isAdmin := false
if username == "shangmibeiadmin" {
isAdmin = true
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "登录成功",
"token": token,
"is_admin": isAdmin,
})
return
}

数据库管理系统管理员证书.cer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-----BEGIN CERTIFICATE-----
MIICXjCCAgWgAwIBAgIIatKGfgnOvYYwCgYIKoEcz1UBg3UwNjELMAkGA1UEBhMC
Q04xEzARBgNVBAoTClNoYW5nTWlCZWkxEjAQBgNVBAMTCVNoYW5nTWlDQTAeFw0y
NDA4MDUwNzUyMTdaFw0yNTEwMTAxMjAxMDFaMFUxEzARBgNVBAoTClNoYW5nTWlC
ZWkxFzAVBgNVBAsTDlNoYW5nTWlCZWkyMDI0MRgwFgYDVQQDEw9zaGFuZ21pYmVp
YWRtaW4xCzAJBgNVBAYTAkNOMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEiHG2
LM9gsuJXiyo+0yDDZEVP1+3Qh+47g65eMeoUXoi0eUiGPvhehh4RaWacpVrQKJXQ
qzCqkR4n1B+7ZymwXqOB3TCB2jAOBgNVHQ8BAf8EBAMCA4gwHQYDVR0lBBYwFAYI
KwYBBQUHAwIGCCsGAQUFBwMBMA8GA1UdDgQIBAYBAgMEBQYwDwYDVR0jBAgwBoAE
AQIDBDAuBgNVHREEJzAlgQtnaXRAZ2l0LmNvbYcEfwAAAYcQIAFIYAAAIAEAAAAA
AAAAaDBXBgNVHR8EUDBOMCWgI6Ahhh9odHRwOi8vY3JsMS5leGFtcGxlLmNvbS9j
YTEuY3JsMCWgI6Ahhh9odHRwOi8vY3JsMi5leGFtcGxlLmNvbS9jYTEuY3JsMAoG
CCqBHM9VAYN1A0cAMEQCIEU8qEYGqgRTJPGI8YLRrpR7x3M2HzZOt377PwsnivGW
AiA67pgq6qfrhKsWc/B2VUqi2t+ZlK+iAM6D+Ai7NoqYSw==
-----END CERTIFICATE-----

题目需要我们以shangmibeiadmin作为用户名登陆进入系统,我们同时需要输入的还有私钥以及公钥证书

因此第一想法是注册shangmibeiadmin这个用户,并生成一个公钥直接传给服务器 让服务器返回公钥证书

但是发现用户已注册

不过审计题目代码能够发现校验是并没有检验证书持有者和用户名是否一致,因此我们可以随便注册一个用户,然后指定其公私钥即可

现在问题在于如何生成一对裸露的公私钥

官方wp给出的是找到了示例代码中的裸露公私钥,小鸡块师傅的博客中给出的方法是直接修改源码 进行私钥打印

我感觉都不是很好

想了下直接在sagemath中过一遍sm2流程即可,只需要随机生成一个数作为私钥,然后求G的d倍点即可

sagemath代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
p = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF
a = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC
b = 0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93
n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123

gx = 0x32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7
gy = 0xBC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0

F = GF(p)

E = EllipticCurve(F, [a, b])

G = E(gx, gy)
d = ZZ.random_element(1, n)
P = d * G
Px = int(P.xy()[0])
Py = int(P.xy()[1])
print(hex(d))
print(hex(Px))
print(hex(Py))
pub='04'+hex(Px)[2:]+hex(Py)[2:]
print(pub)
1
2
3
4
0xbb00c2985990dec5ac9b69339d2d20de3e649cbbaf6b0c9c77f10637f2587bf8
0x74b81cee8dee2e991f1173432c47ff70375ed1081170100d62d89cef040d12e4
0xd1c4f987fbcbcd4f9858b2386c526abe5c96566e352fa801d3f9eb66abca0050
0474b81cee8dee2e991f1173432c47ff70375ed1081170100d62d89cef040d12e4d1c4f987fbcbcd4f9858b2386c526abe5c96566e352fa801d3f9eb66abca0050

这里需要注意:需要在公钥前面加上04

因为公钥是一个椭圆曲线点

一个点有很多编码方式:

1
2
3
04 || X || Y      未压缩
02 || X 压缩(Y为偶数)
03 || X 压缩(Y为奇数)

因此需要一个字节告诉别人

我采用的是哪一种编码。

然后本题就只需要把这些数据上传上去就能解决了

Flag4:数据库管理系统 [流量包解密]

题目描述:

登录数据库管理系统,下载并分析总经理协同签名流量包(加密后的文件).dat、SM4加密解密代码(用于加密流量包文件).py、总经理协同签名流量包加密使用的iv.txt,解密总经理协同签名流量包。

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
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT

MULTIPLIER = 6364136223846793005
ADDEND = 1
MASK = 0xffffffffffffffff
ITERATIONS = 1000

# 从文件中读取seed
def read_seed(file_path):
with open(file_path, 'r') as file:
seed = int(file.read().strip(), 16)
print("seed:", hex(seed))
return seed

global_seed = read_seed('seed.txt')

def genRandom():
global global_seed
# print("global_seed", hex(global_seed))
for _ in range(ITERATIONS):
global_seed = (global_seed * MULTIPLIER + ADDEND) & MASK
return (global_seed >> 32) & 0xffffffff

# 16进制字符串转bytes
def HexStringToBytes(hex_str):
return bytes.fromhex(hex_str)

# bytes转16进制字符串
def BytesToHexString(byte_seq):
return byte_seq.hex()

def genSM4KeyOrIV():
return HexStringToBytes(''.join(f'{genRandom():08x}' for _ in range(4)))

def SM4Encrypt(data_bytes, key_bytes, iv_bytes):
sm4 = CryptSM4()
sm4.set_key(key_bytes, SM4_ENCRYPT)
return sm4.crypt_cbc(iv_bytes, data_bytes)

def SM4Decrypt(cipher_bytes, key_bytes, iv_bytes):
sm4 = CryptSM4()
sm4.set_key(key_bytes, SM4_DECRYPT)
return sm4.crypt_cbc(iv_bytes, cipher_bytes)


print("############ SM4 Cryptographic Services Start... ###################")

iv_bytes = genSM4KeyOrIV()
print("iv hex:", BytesToHexString(iv_bytes))

key_bytes = genSM4KeyOrIV()
print("key hex:", BytesToHexString(key_bytes))

# 从test.pcapng读取数据并加密
with open('test.pcapng', 'rb') as f1:
plain1_bytes = f1.read()
cipher1_bytes = SM4Encrypt(plain1_bytes,key_bytes,iv_bytes)

# 写密文数据到cipherText.dat
with open('cipherText.dat', 'wb') as f2:
f2.write(cipher1_bytes)

# 从cipherText.dat读密文数据
with open('cipherText.dat', 'rb') as f3:
cipher2_bytes = f3.read()
plain2_bytes = SM4Decrypt(cipher2_bytes,key_bytes,iv_bytes)

# 解密密文并将明文写入到plainText.pcapng(含flag4)
with open('plainText.pcapng', 'wb') as f4:
f4.write(plain2_bytes)

总经理协同签名流量包加密使用的iv.txt:

1
90fc5cf2e2f47488a257fd51e0ae615b

这里有两种思路:

思路一

因为这个iv是完整的128bits,所以我们可以取前32bits和33-64bits作为我们的校验点

针对iv0先左移32位,然后爆破低位后 得到完整的当时的global_seed 然后再调用一次getrandom判断是否和iv1相同即可

找到正确的低位后直接再往后过4遍getrandom即可恢复key

为了加速,这里对getrandom函数进行重构:

显然这里面的循环过程是一个LCG循环,我们可以写成这样的形式:

所以我们这里也可以进行预处理优化,来简化getrandom的过程

1
2
3
4
for (int i=0;i<1000;i++){
mult = (mult * MULTIPLIER) & MASK;
add = (add * MULTIPLIER + ADDEND) & MASK;
}

这里巩固一下之前学的c++多线程操作:

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
#include "stdlib.h"
#include <stdio.h>
#include <string.h>
#include <thread>
#include <vector>

uint64_t mult = 1;
uint64_t add = 0;
uint64_t MULTIPLIER = 6364136223846793005ULL;
uint64_t ADDEND = 1ULL;
uint64_t MASK = 0xffffffffffffffffULL;
int ITERATIONS = 1000;


uint64_t iv0=0x90fc5cf2;
uint64_t iv1=0xe2f47488;


void worker(unsigned int begin,
unsigned int end)
{
for (unsigned int key = begin; key != end; key++)
{
uint64_t state0 = iv0<<32 |key;
uint64_t state1 = state0*mult+add & MASK;
state1 = (state1>>32) & 0xffffffff;
if (state1 == iv1){
printf("\n========== FOUND ==========\n");
printf("key = %08x\n", key);
printf("===========================\n");
}
}
}

int main() {
for (int i=0;i<1000;i++){
mult = (mult * MULTIPLIER) & MASK;
add = (add * MULTIPLIER + ADDEND) & MASK;
}
printf("mult = %llu\n", mult);
printf("add = %llu\n", add);

unsigned int s = 0x00000000;
unsigned int e = 0x00000000;

unsigned threads = 8;
printf("Using %u threads\n", threads);

std::vector<std::thread> pool;

uint64_t total =
(uint64_t)e + (1ULL<<32) - s;

uint64_t block = total / threads;

for(unsigned t=0;t<threads;t++)
{
uint64_t offset_begin = t * block;

uint64_t offset_end =
(t == threads-1)
? total
: (t+1)*block;

unsigned int L =
s + (unsigned int)offset_begin;

unsigned int R =
s + (unsigned int)offset_end;

pool.emplace_back(worker,
L,
R);
}

for(auto &th : pool)
th.join();
}

实测秒出

1
2
3
4
5
6
7
8
9
10
11
mult = 16758435521279586465
add = 4956409081608732536
Using 8 threads

========== FOUND ==========
key = ea3632d2
===========================

========== FOUND ==========
key = b8bcf1e5
===========================

思路二

可以将题目转化成HNP问题:

首先有

1000 次 LCG 合并后得到

代入得到

其中

对于四个连续状态,一共有三条方程:

因此最终真正送进 LLL 的矩阵就是

这个格中存在一个特别短的向量,对应于未知量

将它与矩阵相乘后,可以得到

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
c = "90fc5cf2e2f47488a257fd51e0ae615b"

MULTIPLIER = 6364136223846793005
ADDEND = 1
MASK = 0xffffffffffffffff + 1
ITERATIONS = 1000

t1,t2,t3,t4 = c[:8],c[8:16],c[16:24],c[24:32]
res = [t1,t2,t3,t4]
t = [int(i,16) for i in res]

##################################################
M = Matrix(Zmod(MASK),[
[MULTIPLIER,1],
[0,1]
])
Mn = M^ITERATIONS
a,b = Mn[0]
a,b = int(a),int(b)

nums = 4
L = Matrix(ZZ,2*nums,2*nums)
for i in range(nums+1):
L[i,i] = 1
for i in range(nums-1):
L[i,nums+i+1] = a
L[i+1,nums+i+1] = -1
c = a*2^32*t[i] - 2^32*t[i+1] + b
L[nums,nums+i+1] = c
L[nums,nums] = 2^32
for i in range(nums-1):
L[-i-1,-i-1] = MASK
L[:,-(nums-1):] *= MASK

res = L.LLL()[0][:4]
print(res)

Final Challenge: 伪造总经理签名

题目描述:

根据以上题目所获得的信息,对指定的消息摘要计算总经理签名值并提交,提交的签名值需保证通过验证。

我们需要利用两侧路径的解题结果,伪造总经理签名

首先看到flag4的流量包中的tcp流:

除了flag4外,剩下的数据很显然是和flag2的协同签名有关的,而相比于flag2来说,这里多给了一个client_sign字段的值,再回头看看可以发现这是clientSign2函数的返回值,也就是

联立方程组:

做一下变换:


构造矩阵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A = Matrix(Zmod(n),[
[s1,-e],
[s2,s3]
])

b = vector(Zmod(n),[
r1,
r+s
])

x = A.solve_right(b)

u = x[0]
d1 = x[1]
k1 = u * inverse_mod(d1,n)