NepCTF2025-Crypto-WP

赛中只写了Crypto部分,最终排名No.10。

本次比赛有些题目比较特别,总体感觉是挺有趣的。

Nepsign

题目描述

1
来签个名吧!flag格式为NepCTF{xxx}

题目附件

Nepsign.zip

解题思路

签名由48个哈希值组成,当私钥相同时,每个位置的哈希值只与当前位置\(i\)\(step[i]\)有关,\(step[i]\)由签名的消息确定。所以先求出happy for NepCTF 2025\(step\),然后随机选取一些消息发送给服务器,从返回的签名中拿到\((i, step[i])\)对应的哈希值,需要的签名都拿到后,发给服务器就行。

exp

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
# exp.py
from pwn import process, remote
from gmssl import sm3
import os
from tqdm import tqdm

def SM3(data):
d = [i for i in data]
h = sm3.sm3_hash(d)
return h

step = [133, 173, 19, 145, 60, 63, 104, 221, 135, 205, 35, 251, 11, 191, 48, 240, 88, 181, 216, 241, 218, 121, 210, 108, 166, 236, 59, 171, 38, 144, 58, 168, 147, 53, 124, 191, 0, 71, 168, 61, 168, 110, 19, 222, 129, 178, 51, 133]
step1 = set([(i, a)for i, a in enumerate(step[:32])])
step2 = set([(i, a)for i, a in enumerate(step[32:])])
def get_valid_msg():
acquired1 = set()
acquired2 = set()
cnt1 = 0
cnt2 = 0
valid_msg = []
while True:
msg = os.urandom(32)
hs = SM3(msg)
hb = bytes.fromhex(hs)
valid = False
for i in range(32):
a = hb[i]
if (i, a) in step1 and (i, a) not in acquired1:
valid = True
cnt1 += 1
acquired1.add((i, a))

hex_symbols = '0123456789abcdef'
sum = [0 for _ in range(16)]
for i in range(16):
for j in range(1, 65):
if hs[j - 1] == hex_symbols[i]:
sum[i] += j
a = sum[i] % 255
if (i, a) in step2 and (i, a) not in acquired2:
valid = True
cnt2 += 1
acquired2.add((i, a))
if valid:
valid_msg.append(msg)
# print(f"cnt1 = {cnt1}, cnt2 = {cnt2}")
if cnt1 == len(step1) and cnt2 == len(step2):
break
return valid_msg
# io = process(["python", "/d/CTF/Chall/2025/NepCTF/Nepsign/server.py"], env={"FLAG": "NepCTF{TEST_FLAG}"})
io = remote("nepctf31-gntx-mv97-i9h2-9sccsetde288.nepctf.com", 443, ssl=True)
valid_msg = get_valid_msg()
HashMap1 = dict()
HashMap2= dict()
for msg in tqdm(valid_msg):
io.sendafter(b"> ", b"1\n")
io.sendafter(b"msg: ", msg.hex().encode() + b"\n")
recvhash = eval(io.recvline())
hs = SM3(msg)
hb = bytes.fromhex(hs)
for i in range(32):
a = hb[i]
if (i, a) in step1 and (i, a) not in HashMap1.keys():
HashMap1.update({(i, a): recvhash[i]})

hex_symbols = '0123456789abcdef'
sum = [0 for _ in range(16)]
for i in range(16):
for j in range(1, 65):
if hs[j - 1] == hex_symbols[i]:
sum[i] += j
a = sum[i] % 255
if (i, a) in step2 and (i, a) not in HashMap2.keys():
HashMap2.update({(i, a): recvhash[32 + i]})

print("Sending signature...")
signature = [HashMap1[(i, a)] for i, a in [(i, a)for i, a in enumerate(step[:32])]] + [HashMap2[(i, a)] for i, a in [(i, a)for i, a in enumerate(step[32:])]]
io.sendafter(b"> ", b"2\n")
io.sendafter(b"give me a qq: ", str(signature).encode() + b"\n")
print(io.recvuntil(b">").decode())
io.close()
# NepCTF{6575a53c-3e44-2b93-c17e-7610a1192514}

latticebros

题目描述

1
你能从给定的信息中找出隐藏的flag吗? flag格式为NepCTF{xxx}

题目附件

LatticeBros.zip

解题思路

已知: \[ \alpha^3 + a_2\alpha^2 + a_1\alpha + a_0 = 0 \] 这里\(a_i\)都是整数,构造如下格:

\[ (1, a_2, a_1, a_0) \begin{pmatrix} \alpha^3C & 1 & 0 & 0 \\ \alpha^2C & 0 & 1 & 0 \\ \alpha C & 0 & 0 & 1 \\ C & 0 & 0 & 0 \\ \end{pmatrix}=(\epsilon C, 1,a_2, a_1) \]

由于\(\alpha\)精度有限,所以\(\epsilon\)是一个非常接近0的数。
求出\(a_2, a_1\)后代入原来的多项式得到\(a_0\)。剩下的就是一个经典的HNP。

exp

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

data = [(541847931463604073209188621415697353813245102261880389530448, 293760933113243563398917466885108625646262447370201484418246), (235213326900086489464935804156966465366154623411555613791270, 660823982268225103178763707015491421784294988488272636270997), (826464884761457937459245903152143755707241416981488127320435, 428521663319038461250005113612781686761766888058391496085911), (589542000504317435156560078533519448295689695687499354390208, 155284353896000150766154807679279597476176668344402166959399), (968823371588600973965757332601758200815345862153455338808286, 870008943690791009196027169525956126827736285614393106689402), (621636099728440147413990266662022925118216803638588918660041, 265635912066749696542909843111997941904342442664219734956888), (426696569424050102229606043215592727790577655338668728275370, 279313121876980354011480010042682666651614765507190502627689), (89450479064580125731654556963306718472532905610952012502649, 465933125964565419295325650759566635253450915499965633327941), (480355476500393865742379469913983270769356894135485925662119, 894041172171871806404285309781862268351135623868845025443422), (842436524669577199024236805258573090764419350786291073287889, 345478552143958037534551648319293899442551000874041707820740), (650054674429185550652935714084022116516082323269321462104664, 441999979283903658157822753439653947343822546158589507765994), (46289431385578693366971976442426853079852982529357847290686, 625618376463384339878849844467050454204685252824782609369180), (71444185449163133531919043374545893927347050624346741281881, 955925578289311966288639224625142299309823207245807788495453), (192579726169321656812883068526498248523814846320328766176253, 626481822474054336470183912297952839011392733501646931370367), (736527635648804640774976580747540045854351230084566721853611, 276626211757586963928788091386096607703513204646314683038338), (177922521867185878959621840269164617147915792720210315529733, 541058782621716573816245900423919799500476442285991532228641), (40610451174818168154306630612571678739921107216052349044576, 727642592899858828601137105077611015328512898368636299587376), (385012983728389322601149562441674995471397288632464238356283, 353921151307105661267278594470212933060655245893209524497156), (750447975601038834764379841158092390933760641866111445401426, 391626416964965737035878375834907580903143512300198923948189), (115058604943298010958881205548782439407592353731185670266593, 491630592857258949793489206081490523001249620510479961058022), (327389234395954477946639629629085910688793716425320663599360, 24975272330009592102362429346350824580378490147041708568130), (115595274689129534885608766476695918464309130165432995990883, 757961876891952019297626599379744405302595090402128271144165), (950804723308776351161744501221236453742418549093165078282534, 20307246759635231945223392614290397512873344480184942904518), (724537610412063699714461780160573528810830178440136810747811, 149681928388378582933943374524511804362928290938917573644613), (340891278018589324130004945217960336392205386747747011263373, 683307718413135477104477081812052183267507312278283317237187), (104379682905784169840335131193505192063050242530811180817410, 715010230598797717533306270232399781090458356371977748416491), (644160326926600986730919713173510327120201404569141824224075, 127877985489410167008195578625004740882394608402141169695352), (549253388716005399852261816416312267100135940382820676807345, 210560134643237517255193955173709174155305784935427470113433), (968265711632086435506163736279856957220961064226797549228006, 273174723915971720522674140326199419265943707917542063022561), (704367622558261900937184683100177434487519780290678439135652, 959106497548134540301589019840013331842784496835379005298630)]
alpha = 54236.606188881754809671280151541781895183337725393
d = 981020902672546902438782010902608140583199504862558032616415
alpha = QQ(alpha)
M = matrix(QQ, [
[alpha**3, 1, 0, 0],
[alpha**2, 0, 1, 0],
[alpha**1, 0, 0, 1],
[alpha**0, 0, 0, 0]
])
M[:, 0] *= getrandbits(32)
L = M.LLL()
v = L[0]
if v[1] == -1:
v *= -1
a2, a1 = v[2], v[3]
a0 = -(alpha**3 + a2 * alpha**2 + a1 * alpha)
p = d - int(a0)

al = []
tl = []
for t, a in data:
al.append(a)
tl.append(t)
M = matrix(ZZ, [al, tl])
M = M.stack(p * identity_matrix(ZZ, M.ncols()))
L = M.LLL()
beta = L[2]
b0, b1 = beta[0], beta[1]
t0, t1 = tl[0], tl[1]
a0, a1 = al[0], al[1]
alpha = (b0 + a0) * pow(t0, -1, p) % p
print(long_to_bytes(alpha))
# NepCTF{c0ngr4tv!at1Ons!!}

ezRSA2

题目描述

1
2
Easy RSA, easy strategy. No need to brute force.
Perhaps it's easier than ezRSA in NepCTF 2024.

题目附件

ezrsa2.zip

解题思路

\(d \approx N^{0.33}\),上界大于\(N^{0.292}\),但是给了\(d\)的低位,可以用来优化。 已知: \[ e(d_{m}M + d_{l}) = 1 + k(N - p - q + 1) \] 变形得: \[ ed_{m}M = 1 + k(N - p - q + 1) - ed_{l} \]\(X = k, Y = p + q\),然后在模\(eM\)对如下多项式使用二元copper就行: \[ f(X, Y) = 1 + X(N - Y + 1) - ed_{l} \]

exp

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

# https://github.com/defund/coppersmith
from sage.tools.coppersmith import small_roots

e=0x73915608ed64c9cf1a2279684cab4f4a78fba229d45d4f860971a241481363470a19cb0dc0d00f816b5befdaca017cf71483e96ef17b36179012f5194a0e6bf481bb06c2644f74c6812efb65d05c00631f282d6aa55c0bc140a1830b95a1cf4b6024cb0db53f2c2189897c41f22e2eec773723f531ec4bfa537fae6de5fe480cf46fe17850f7eb47df08194d95db3d26ac923b26e110ee645239ab586bbc546ddc5906f280a106edbb727ccb05536b5a3f5c0ebcf865c95ce58be54f7f3547aa53baa218b0dfa98e42d925fa341e45f94a3b16b0c83802660c7f34de3336cb21f219073cf8e9f5e39d47f0a9a9ee7c255f09a6add9a2f7a47960f4a853183d29
N=0xba8956e81394f3f1265ca5d9c4ad1ab0078bb43c4b80a231ab2cc62246ae45f66a562252622aed2cbbfc08647ef2fec0f97a632bf2242845f4b3af0c427cec3d90f42e90278a5a0feeed0922a8cd2278074ac54e9cfc0e96ff68f8d8f266dd87dc1cc59c2895ec884de2022311767f6a9a7e0bd288c79620e28b83bb3c8d8ad1047c839d6ccf5544eaf434a5f00b951769ab3121298d04b63a162757beb3d49917cd0c9e02ee1ac29398c8130961d5a2f2833aba1e538edb7bb97071f40fae543d1622f0c9206c6d4d8abb2ac1b93ebfb603c2f3a909ede357ade4043550fe540d13a4e87db8d731fe130f15a43a1a00364f5da2d87f7b660c3a04e734218a11
hints=[1, 3, 0, 3, 9, 16, 10, 14, 5, 11, 21, 18, 30, 30, 38, 2, 20, 62, 66, 1, 22, 56, 41, 13, 78, 59, 51, 6, 57, 117, 73, 75, 96, 112, 50, 93, 158, 97, 146, 8, 65, 96, 186, 161, 90, 131, 46, 32, 140, 133, 50, 43, 151, 234]
ct=0x101b284ad196b5bbd3d3df00a7d3577caeb29c681bdd122582b705afc671febf45d4f3786640e55aadd6a31ecc49175f97b772720f1735f8555f768b137a4643cd6958f80a3dfca4d0270ad463d6dde93429940bd2abb5ad8408b0906fa8d776544a1c50cc0d95939bef4c3fb64d0b52dca81ff0f244fc265bfc0bc147435d05f8f1a146e963a1403b3c123b4d6e73d1fd897109995009be1673212607f0ea7ae33d23f3158448b05c28ea6636382eee9436c4a6c09023ead7182ecd55ac73a68d458d726e1abc208810468591e63f4b4c2c1f3ce27c4800b52f7421ccab432c03e88b3b255740d719e40e0226eabb7633d97ed210e32071e2ac36ed17ef442e

Ml = []
for i in range(1, len(hints) + 1):
Ml.append(sieve_base[i])
d_lsb = crt(hints, Ml)
M = prod(Ml)
PR.<x, y> = PolynomialRing(Zmod(e * M))
f = 1 - e*d_lsb + x*(N - y + 1)
X = 2**680
Y = 2**1024
x0, y0 = small_roots(f, (X,Y), 6, 2)[0]
x0, y0 = int(x0), int(y0)
p = ((y0**2 - 4*N).sqrt() + y0) / 2
q = N // p
d = pow(e, -1, (p - 1) * (q - 1))
flag = pow(ct, d, N)
print(long_to_bytes(flag))
# NepCTF{larg3r_M0du1u5_1nf0_g1ves_b3773r_b0und5}

transistion

题目描述

1
2
3
我们通过一个DFF串行把数据从本机传输进FPGA里然后进行能量采集。先对时钟采样来了解一下板子的基础信息,或许我们还能从中发现一些关于transition的侧信道信息……
由于出题人疏忽,本题在第一次放出时未提供附件,在此滑跪。
(flag格式为flag{})

题目附件

transition.zip

解题思路

测试发现,一共采样了8000个数据,根据data的长度为64,推出data中的一个数据在采样中占125次,所以估计在125处附近会发生跳变,接着测试110~140的连续采样,发现在128处的采样能反映所有的跳变情况:

  • \(samples[128] \leq 0.5\),不跳变,相邻2比特是00。
  • \(0.5 < samples[128] \leq 2\),跳变,相邻2比特是01。
  • \(2 < samples[128] \leq 3\),跳变,相邻2比特是10。
  • \(3 < samples[128]\),不跳变,相邻2比特是11。

所以每次采样可以获取2比特信息,这样再以250为间隔采样,只需32次就能完全确定data,这也足够获取完整的flag。

exp

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
# exp.py
from scipy.io import savemat,loadmat
from pwn import remote
from tqdm import trange

io = remote("nepctf31-nizx-0vvs-rjlc-pse68tvxp993.nepctf.com", 443, ssl=True)
samples = 64
# sampledata = []
# int_sampledata = []
guess = ""
for i in range(128, 8000, 250):
io.sendafter(b">", b"1\n")
position = i
io.sendafter(b">", (str(position) + "\n").encode())
recv = eval(io.recvline().decode())
print(f"{position}:\t", recv)
if recv <= 0.5:
guess = guess + "00"
elif 0.5 < recv <= 2:
guess = guess + "01"
elif 2 < recv <= 3:
guess = guess + "10"
elif 3 < recv:
guess = guess + "11"
# sampledata.append(eval(recv))
# int_sampledata.append(int(eval(recv)))
# print(sampledata)
# print(int_sampledata)

io.sendafter(b">", b"2\n")
for i in trange(64):
io.recvuntil(b">")
if guess[i] == "0":
io.sendline(b"0\n")
else:
io.sendline(b"1\n")
print(io.recvall(3).decode())
io.close()
# flag{lt's_tRANsiTlOn_IE@k4GE26fca40f}

unloopshock

题目描述

1
turtle 会使用AES加密你的信息,且每一个人(每一次链接)都会使用不同的KEY,你能从turtle口中泄露你的KEY吗

题目附件

unloopshock.zip

解题思路

根据提示,关注密钥拓展部分(密钥拓展函数地址为0x01BC):

下图已恢复部分函数名。

通过查看key_expand函数的引用,可以知道a2数组用来存放拓展密钥。第一个for循环将初始密钥复制到a2中,并在接下来的do-while循环中开始密钥拓展,如果能让do-while循环不执行那么拓展的密钥只有前16字节是初始密钥,而后面全为0字节。这样就可以根据已知明密文推出初始密钥。

为了让do-while循环不起作用,可以在前面的for循环结束时,通过注入错误修改变量i的值,让i足够大且i不是4的倍数,这样在进行了一些无意义的赋值后就可以直接跳出do-while循环。

所以在汇编中找到修改变量i的地方:

可以发现是通过将i赋值给eax,在对eax加1后再赋值给i实现的i的加1。那么可以hook add eax 1指令,通过随机修改eax的值来改变i的值。

exp

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
# exp.py
from pwn import remote
def recoverkey(plain, cipher):
inv_s_box = ( 0X52, 0X09, 0X6A, 0XD5, 0X30, 0X36, 0XA5, 0X38, 0XBF, 0X40, 0XA3, 0X9E, 0X81, 0XF3, 0XD7, 0XFB,
0X7C, 0XE3, 0X39, 0X82, 0X9B, 0X2F, 0XFF, 0X87, 0X34, 0X8E, 0X43, 0X44, 0XC4, 0XDE, 0XE9, 0XCB,
0X54, 0X7B, 0X94, 0X32, 0XA6, 0XC2, 0X23, 0X3D, 0XEE, 0X4C, 0X95, 0X0B, 0X42, 0XFA, 0XC3, 0X4E,
0X08, 0X2E, 0XA1, 0X66, 0X28, 0XD9, 0X24, 0XB2, 0X76, 0X5B, 0XA2, 0X49, 0X6D, 0X8B, 0XD1, 0X25,
0X72, 0XF8, 0XF6, 0X64, 0X86, 0X68, 0X98, 0X16, 0XD4, 0XA4, 0X5C, 0XCC, 0X5D, 0X65, 0XB6, 0X92,
0X6C, 0X70, 0X48, 0X50, 0XFD, 0XED, 0XB9, 0XDA, 0X5E, 0X15, 0X46, 0X57, 0XA7, 0X8D, 0X9D, 0X84,
0X90, 0XD8, 0XAB, 0X00, 0X8C, 0XBC, 0XD3, 0X0A, 0XF7, 0XE4, 0X58, 0X05, 0XB8, 0XB3, 0X45, 0X06,
0XD0, 0X2C, 0X1E, 0X8F, 0XCA, 0X3F, 0X0F, 0X02, 0XC1, 0XAF, 0XBD, 0X03, 0X01, 0X13, 0X8A, 0X6B,
0X3A, 0X91, 0X11, 0X41, 0X4F, 0X67, 0XDC, 0XEA, 0X97, 0XF2, 0XCF, 0XCE, 0XF0, 0XB4, 0XE6, 0X73,
0X96, 0XAC, 0X74, 0X22, 0XE7, 0XAD, 0X35, 0X85, 0XE2, 0XF9, 0X37, 0XE8, 0X1C, 0X75, 0XDF, 0X6E,
0X47, 0XF1, 0X1A, 0X71, 0X1D, 0X29, 0XC5, 0X89, 0X6F, 0XB7, 0X62, 0X0E, 0XAA, 0X18, 0XBE, 0X1B,
0XFC, 0X56, 0X3E, 0X4B, 0XC6, 0XD2, 0X79, 0X20, 0X9A, 0XDB, 0XC0, 0XFE, 0X78, 0XCD, 0X5A, 0XF4,
0X1F, 0XDD, 0XA8, 0X33, 0X88, 0X07, 0XC7, 0X31, 0XB1, 0X12, 0X10, 0X59, 0X27, 0X80, 0XEC, 0X5F,
0X60, 0X51, 0X7F, 0XA9, 0X19, 0XB5, 0X4A, 0X0D, 0X2D, 0XE5, 0X7A, 0X9F, 0X93, 0XC9, 0X9C, 0XEF,
0XA0, 0XE0, 0X3B, 0X4D, 0XAE, 0X2A, 0XF5, 0XB0, 0XC8, 0XEB, 0XBB, 0X3C, 0X83, 0X53, 0X99, 0X61,
0X17, 0X2B, 0X04, 0X7E, 0XBA, 0X77, 0XD6, 0X26, 0XE1, 0X69, 0X14, 0X63, 0X55, 0X21, 0X0C, 0X7D,
)
s_box = (
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)

def xor_bytes(a, b):
return bytes(i^j for i, j in zip(a, b))

def add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j] ^= k[i][j]

def sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = s_box[s[i][j]]

def shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]

xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)

def mix_single_column(a):
t = a[0] ^ a[1] ^ a[2] ^ a[3]
u = a[0]
a[0] ^= t ^ xtime(a[0] ^ a[1])
a[1] ^= t ^ xtime(a[1] ^ a[2])
a[2] ^= t ^ xtime(a[2] ^ a[3])
a[3] ^= t ^ xtime(a[3] ^ u)

def inv_sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = inv_s_box[s[i][j]]

def mix_columns(s):
for i in range(4):
mix_single_column(s[i])

def inv_shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]

def inv_mix_columns(s):
for i in range(4):
u = xtime(xtime(s[i][0] ^ s[i][2]))
v = xtime(xtime(s[i][1] ^ s[i][3]))
s[i][0] ^= u
s[i][1] ^= v
s[i][2] ^= u
s[i][3] ^= v

mix_columns(s)

def bytes2matrix(text):
return [list(text[i:i+4]) for i in range(0, len(text), 4)]

enc = bytes2matrix(cipher)
inv_shift_rows(enc)
inv_sub_bytes(enc)
for i in range(9, 0, -1):
inv_mix_columns(enc)
inv_shift_rows(enc)
inv_sub_bytes(enc)

key = xor_bytes(bytes(enc[0] + enc[1] + enc[2] + enc[3]), plain)
return key

io = remote("nepctf30-tk8l-9zea-1qz3-w8rqzwa7f403.nepctf.com", 443, ssl=True)
# AES_PTS = b'___HELLO_WORD___'
# Shocktime = 90
# cipher = run(AES_PTS,AES_KEY,Shocktime)
# recoveredkey = recoverkey(AES_PTS, cipher)
# print(recoveredkey == AES_KEY)
AES_PTS = b'___HELLO_WORD___'
Shocktime = 90
io.recvuntil(b"Input your data: in hex(max 16 bytes)\n")
io.sendline(AES_PTS.hex().encode())
io.recvuntil(b"time to shock: ")
io.sendline(str(Shocktime).encode())
io.recvuntil(b"Result:")
cipher = bytes.fromhex(io.recvline().decode().strip())
key = recoverkey(AES_PTS, cipher)
io.recvuntil(b"now give me your guess key: ")
io.sendline(key.hex().encode())
print(io.recvall(2).decode())
io.close()
# NepCTF{ThE_4eS-CH1p_G3t_5HOcK3d-y0u-G3T-Th3-cRIt1Cal_kEyYYyY0}

*国产密码应用

题目描述

1
小明通过开源的GMSSL采用TLCP协议实现与服务端之间数据的传输机密性和完整性保护,且均采用服务器密码机实现数据加解密和签名验签的运算。但是在运行过程中似乎都出现了一些小问题,你能发现吗?(答案格式:NepCTF{XXX})

题目附件

国产密码应用.zip

解题思路

学了2天的TLCP,终于成功复现。

学习过程中主要参考了如下资料:
1. GB/T 32918 SM2椭圆曲线公钥密码算法:总则
2. GB/T 32918 SM2椭圆曲线公钥密码算法:数字签名算法
3. GB/T 32918 SM2椭圆曲线公钥密码算法:密钥交互协议
4. GB/T 32918 SM2椭圆曲线公钥密码算法:公钥加密算法
5. GB/T 32918 SM2椭圆曲线公钥密码算法:参数定义
6. GB/T 35276 SM2密码算法使用规范
7. GB/T 38636 传输层密码协议(TLCP)
8. bilibili-泥瓦匠的标准解读-【GB/T 38636】TLCP协议中密钥计算(手动计算主密钥、工作密钥)
9. bilibili-国密TLCP协议解析
10. csdn-预主密钥/主密钥计算和Finished消息的加解密

使用的gmssl的python实现:
11. https://github.com/py-gmssl/py-gmssl

获取签名私钥

TLCP的握手流程如下[7]

wireshark解析的流量包:(注意低版本的wireshark不能正确显示TLCP协议,这里使用的版本为4.4.8)

首先来看Client Hello消息: 这里会包含一个客户端生成的随机数,我们先把他记录下来,后续生成工作密钥会用到:

1
CR = "687b612b5cb1222783ee426a817d3da6a05ed985fd06dd72ff3cf58a3ffda4aa"

同样的,Server Hello消息也会包含一个服务端随机数,也记录下来:

1
SR = "687b612bdbd4517e6fc989ddd2bf25e78b5e21e576bbd8f46add6d7df527887f"

双方在交换随机数后,服务端会发来Certificate消息: 这里面包含2个证书,第1个是签名证书,第2个是加密证书。
签名证书用来验证加密证书的有效性,加密证书用于后续的密钥交换。

签名证书的内容如下: 其中subjectPublickey就是签名的公钥,把04的tag去掉,剩下的64字节就是公钥的横纵坐标:

1
2
x = 0xd401ce6a6684178fa77940108f2f8946352208b29bd6f9bda14a38ddec05668c
y = 0x71a68d1199f376f5656e0d6fa93d73196e02c95ad2b311f9c220d3139ad0ccdc

签名值在Server Key Exchange消息中:

按如下方式解析签名[6]

签名值是:

1
2
r = 0x04a361f2c6cf3e297e5e5dc73df6ffc1bd129ff6a1c0baa19b294953ce374abd
s = 0x3916042142eb8919763f7f304a48be1aaf02866e6935a30503c5d90e2f4c7927

拿到公钥和签名后,来看签名过程[2]

其中\(ID_A\)使用默认值[6]

消息\(M\)由客户端随机数、服务端随机数、加密证书拼接得到。在复制加密证书的时候,一定要把表示证书长度的3字节也复制上。

最后,拿到数据,我们可以验证一下:

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
# verify.py
from gmssl import sm3
n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123
p = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF
g = (0x32c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589334c74c7,
0xbc3736a2f4f6779c59bdcee36b692153d0a9877cc62a474002df32e52139f0a0)
a = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC
b = 0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93
E = EllipticCurve(GF(p), [a, b])
G = E(g)

def verify(r, s, e, PubKey):
t = (s1 + r1) % n
P1 = s1 * G
P2 = t * PubKey
x = int((P1 + P2).x())
return (e1 + x - r1) % n == 0

ENTL_A = b"\x00\x80"
ID_A = b"1234567812345678"
a_byte = bytes.fromhex(hex(a)[2:].zfill(64))
b_byte = bytes.fromhex(hex(b)[2:].zfill(64))
xG = bytes.fromhex(hex(int(G.x()))[2:].zfill(64))
yG = bytes.fromhex(hex(int(G.y()))[2:].zfill(64))
PK = E(0xd401ce6a6684178fa77940108f2f8946352208b29bd6f9bda14a38ddec05668c, 0x71a68d1199f376f5656e0d6fa93d73196e02c95ad2b311f9c220d3139ad0ccdc)
CER = "0001dd308201d93082017fa003020102020c5d4a7a54c074b6550ae82dd4300a06082a811ccf550183753066310b300906035504061302434e3111300f0603550408130850524f56494e43453111300f060355040713084c4f43414c495459310c300a060355040a13034f52473110300e060355040b13074f524755494e543111300f060355040313084e65704e65704341301e170d3235303731333035313231345a170d3335303731313035313231345a3067310b300906035504061302434e3111300f0603550408130850524f56494e43453111300f060355040713084c4f43414c495459310c300a060355040a13034f52473110300e060355040b13074f524755494e5431123010060355040313094e65704e6570456e633059301306072a8648ce3d020106082a811ccf5501822d03420004b1e942c2068b9312f82cf22b5c01f4910e49e10408478c03ad9c5e37013852212f90f0d88d7e2ca86bb718772b213e757d0e6ca505444d19d5496c9a70323d46a3123010300e0603551d0f0101ff040403020520300a06082a811ccf55018375034800304502206671281fe2bf53d3c596132fe5a58101ec805566b5b76a5ac7b549fe5e4805c5022100cf148ffa42395c3a60cf103953fcf728d566080fced186c9191fef9b5f81e081"
xA = bytes.fromhex(hex(int(PK.x()))[2:].zfill(64))
yA = bytes.fromhex(hex(int(PK.y()))[2:].zfill(64))
ZA = sm3.sm3_hash(list(ENTL_A + ID_A + a_byte + b_byte + xG + yG + xA + yA))

CR = "687b612b5cb1222783ee426a817d3da6a05ed985fd06dd72ff3cf58a3ffda4aa"
SR = "687b612bdbd4517e6fc989ddd2bf25e78b5e21e576bbd8f46add6d7df527887f"
r1 = 0x04a361f2c6cf3e297e5e5dc73df6ffc1bd129ff6a1c0baa19b294953ce374abd
s1 = 0x3916042142eb8919763f7f304a48be1aaf02866e6935a30503c5d90e2f4c7927
M = ZA + CR + SR + CER
M = bytes.fromhex(M)
e1 = int(sm3.sm3_hash(list(M)), 16)
assert verify(r1, s1, e1, PK)

同样的,我们还可以获取第二个通信的签名,然后根据题目提示的随机数复用(随机数复用这一点,也可以通过比对两次签名的\((r - e) \mod n\)值来发现),只需要解一个线性方程组来恢复签名的私钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# get_sign_prikey.py
n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123
def sollin(r1, s1, r2, s2):
k, d = var("k d")
f1 = (1 + d) * s1 - k + r1 * d
f2 = (1 + d) * s2 - k + r2 * d
fd = f1.resultant(f2, k)
fk = f1.resultant(f2, d)
PR.<d> = PolynomialRing(GF(n))
fd = PR(str(fd))
d = int(fd.roots()[0][0])
PR.<k> = PolynomialRing(GF(n))
fk = PR(str(fk))
k = int(fk.roots()[0][0])
return k, d
r1 = 0x04a361f2c6cf3e297e5e5dc73df6ffc1bd129ff6a1c0baa19b294953ce374abd
s1 = 0x3916042142eb8919763f7f304a48be1aaf02866e6935a30503c5d90e2f4c7927
r2 = 0x604b0232ee0907c6957c1521457ff2ab5bb32a7637221eec88c0e7fba6fc4b12
s2 = 0x6c0e3e59b46bb87abf7c9c7398bc75bec795e85e92c2414770772ea32a0b8205
k, d = sollin(r1, s1, r2, s2)
print(f"{d = }")
1
d = 56431408153695370061795421010209097251225434679562598847096735820032467303653

解密通信流量

首先需要获取预主密钥(48字节),前2字节是0x0101(客户端所支持的版本号),后46字节是随机数。预主密钥由客户端生成,并在Client Key Exchange消息中通过加密证书加密传输。

密文可以按如下结构解析[6] 得到:

1
2
3
4
x = 0x00c749061668652e26040e008fdd5eb77a344a417b7fce19dba575da57cc372a9e
y = 0x00f2df5db2d144e9454504c622b51cf38f5006206eb579ff7da6976eff5fbe6480
h = "d682849ee64d092a590e0a981d750a608e59d08fdc9511426aa90b99e7c215c7"
c = "67ec55ae9437abdf46ee8a1406a3356c2123b598d92a2980d7dab8ec1a51fb9d7c987b2f7d16dcf9da95ea2c86875191"
通过与数据库表中加密信息的x,y值比对,能够发现与第4条加密信息用了同样的随机数。 SM2的加密方法,可以直接查看gmssl的python实现[11] 可以发现,当使用相同的随机数时,最终生成的异或密钥也会相同,这样就可以通过数据库中已知的明密文,获取异或密钥,然后解密上面的密文获得预主密钥。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def xor_bytes(a, b):
return bytes([i ^ j for i, j in zip(a, b)])
db_plain = "4e475132595451784e7a6b305a5455314d7a5532597a597a4e44637a4e545a6a4e6a4d304e54526c4e5455314d6a"
db_cipher = "28AA2529E18960E93052D856ED1D1476B19FE04367EDAAD6E2F177AD7DE0F9B04B2C8C98EAC83397DA0CDEB7E281"
db_plain = bytes.fromhex(db_plain)
db_cipher = bytes.fromhex(db_cipher)
xor_key = bytes([i^j for i, j in zip(db_plain, db_cipher)])

enc_pre_secret = "67ec55ae9437abdf46ee8a1406a3356c2123b598d92a2980d7dab8ec1a51fb9d7c987b2f7d16dcf9da95ea2c86875191"
enc_pre_secret = bytes.fromhex(enc_pre_secret)
pre_secret_prefix = xor_bytes(enc_pre_secret, xor_key)
assert len(pre_secret_prefix) == 46

print(pre_secret_prefix.hex())
# 010121b52cea9a4e38c63972b1ea742bddc600e9e7bdda2c7b6fac3b29e5584779deba87d98abd024ecd61aa296c
可以发现,前面2字节是0x0101(协议的版本号),说明解密是正确的。

由于数据库中已知明密文长度都是46字节,所以我们只拿到了预主密钥的前46字节,还需要爆破2字节。

拿到预主密钥后,根据如下方法生成主密钥[7]

PRF函数定义如下[7]

获取到主密钥后,再获取工作密钥[7] 这些密钥长度分别是32、32、16、16、16、16。

在双方成功获取到工作密钥后,都会发送一个Finished消息,在wireshark中显示为Encrypted Handshake Message

我们先来看客户端发送的这个消息,长度80字节(16字节IV+cipher(16字节finished消息明文+32字节MAC+16字节的padding))[10]

1
f1de37c49eda363496b66f93d2c8536573e9099388cdf94e3b32c04cc091cbf79c85e78a9036f5d504a37fcf16150f368f8f78bf1ce45a0e198d58b21aaf587f2b060fb603b4d51b59de367a7587e04d

解析得:

1
2
3
4
iv = "f1de37c49eda363496b66f93d2c85365"
finished_cipher = "73e9099388cdf94e3b32c04cc091cbf7"
mac_cipher = "9c85e78a9036f5d504a37fcf16150f368f8f78bf1ce45a0e198d58b21aaf587f"
padding_cipher = "2b060fb603b4d51b59de367a7587e04d"
由于finished消息前4字节一定是0x1400000C[10],所以可已用来爆破前面未知的预主密钥:
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
# brute_pre_secret.py
from gmssl import sm2, sm3, sm4
from tqdm import trange

def SM3(msg: bytes):
return bytes.fromhex(sm3.sm3_hash(list(msg)))

def HMAC(key: bytes, message: bytes):
if isinstance(key, str):
key = key.encode('utf-8')
if isinstance(message, str):
message = message.encode('utf-8')

block_size = 64

if len(key) > block_size:
key = bytes.fromhex(sm3.sm3_hash(list(key)))
if len(key) < block_size:
key += b'\x00' * (block_size - len(key))

ipad = b'\x36' * block_size
opad = b'\x5c' * block_size

inner_key = bytes([k ^ i for k, i in zip(key, ipad)])
inner_data = inner_key + message
inner_hash = sm3.sm3_hash(list(inner_data))

outer_key = bytes([k ^ o for k, o in zip(key, opad)])
outer_data = outer_key + bytes.fromhex(inner_hash)
outer_hash = sm3.sm3_hash(list(outer_data))

return bytes.fromhex(outer_hash)

def P_hash(secret: bytes, seed: bytes, len: int):
A = seed
result = b""
for _ in range(len // 32):
A = HMAC(secret, A)
result += HMAC(secret, A + seed)
A = HMAC(secret, A)
result += HMAC(secret, A + seed)[:len % 32]
return result

def PRF(secret, label, seed, len):
return P_hash(secret, label + seed, len)

def xor_bytes(a, b):
return bytes([i ^ j for i, j in zip(a, b)])

db_plain = "4e475132595451784e7a6b305a5455314d7a5532597a597a4e44637a4e545a6a4e6a4d304e54526c4e5455314d6a"
db_cipher = "28AA2529E18960E93052D856ED1D1476B19FE04367EDAAD6E2F177AD7DE0F9B04B2C8C98EAC83397DA0CDEB7E281"
db_plain = bytes.fromhex(db_plain)
db_cipher = bytes.fromhex(db_cipher)
xor_key = bytes([i^j for i, j in zip(db_plain, db_cipher)])
print(f"{xor_key = }")

CR = "687b612b5cb1222783ee426a817d3da6a05ed985fd06dd72ff3cf58a3ffda4aa"
SR = "687b612bdbd4517e6fc989ddd2bf25e78b5e21e576bbd8f46add6d7df527887f"
CR = bytes.fromhex(CR)
SR = bytes.fromhex(SR)

enc_pre_secret = "67ec55ae9437abdf46ee8a1406a3356c2123b598d92a2980d7dab8ec1a51fb9d7c987b2f7d16dcf9da95ea2c86875191"
enc_pre_secret = bytes.fromhex(enc_pre_secret)
pre_secret_prefix = xor_bytes(enc_pre_secret, xor_key)
assert len(pre_secret_prefix) == 46

iv = bytes.fromhex("f1de37c49eda363496b66f93d2c85365")
c = bytes.fromhex("73e9099388cdf94e3b32c04cc091cbf7")

for i in trange(256 * 256 - 1, 0, -1):
pre_secret = pre_secret_prefix + bytes.fromhex(hex(i)[2:].zfill(4))
master_secret = PRF(pre_secret, b"master secret", CR + SR, 48)
ALL_KEY = PRF(master_secret, b"key expansion", SR + CR, 80)
ck = ALL_KEY[64: 64 + 16]
SM4 = sm4.CryptSM4()
SM4.set_key(ck, sm4.SM4_DECRYPT)
# 这里只解密了16字节,结果肯定不满足PKCS#7的填充方式,SM4.crypto_cbc会返回空,所以最好直接把源码里面的去填充部分去掉
pt = SM4.crypt_cbc(iv, c)
if pt[:4] == b"\x14\x00\x00\x0c":
print(f"{pre_secret = }")
break
# pre_secret = b'\x01\x01!\xb5,\xea\x9aN8\xc69r\xb1\xeat+\xdd\xc6\x00\xe9\xe7\xbd\xda,{o\xac;)\xe5XGy\xde\xba\x87\xd9\x8a\xbd\x02N\xcda\xaa)l\xf7B'

拿到预主密钥后,再按照上面的方法,生成工作密钥,就可以解密Application Data消息中的密文了。

最后的SM2

拿到工作密钥后,我解密了第一个Application Data中的密文,解密结果为:

1
2
3
4
b'\xb3K\x9a\xe3z\x124Y{\xf2\x9a^[\xa5ck308189022100D41161C94F626297DB5CAE9D1B1ABC1C6363064DA2D82C48F0DDF7F8B98966D102204AEC64B7742E5C014665BCB9BD1AEFB4BE973854929DA50F23B583D2CC283DD90420F19E80E48A53140E33178DC48F9195E6D87463608464E64AF47C0DF58A3B083104208D4C762D5BAF59B6787B11DB97A4129B44CEE3BE7782D5A35A7FA57F59089089\n\x90)\xd7\x00\x05\x1fe\xc7\x9d\xb8\xea\xddcG)\xc4\xee\x81\xcf\x18\xf5\xeb\xf5\xea\x8a\xc9g\xee\x18\xd5O\xc6\x06\x06\x06\x06\x06\x06\x06'
b'b\xf5\xdc#\x91\xcbEL\xe0\xf0$\xd72H\x07\xd33081AA02201169369B36F4266E9A86007020B8917DEE190E7F3323BA889FE4FD9423EB39CE02210099ADDE8DC3128E6FE4521A2E1740AD73A570FB7F5D289435E71E47218BEC28010420957B7D29A5734EB548E03E5E0A4A2CA87CE493D5E6908503D7D36881C71F09A30441B111A30786E760723E744718DF10E4E33CA0D7003FB7A0A4EE37226586D2F7B7A3BB9A8D35134C39192B3735FA76C3562CC3E3E158F40ED83BBF321387B8A65848\n\xbc\xd5A\xcd\x7f@Rh\x81\x99\xeaK\x9a\xfe\x02\xea\xc9\x8f\x8b\xb2\xf3\xee\x80\x1b[ZG\xc4\xaa\xf8?\xec\x04\x04\x04\x04\x04'
b't\xc1\xe6[\xcbY\x94NH\xf1\xdb\x8d\xcc\xb0\x97\xf330819202203F271466AE70B4B61C660B3E727BAB30FA988751F771F4380E3ED95A706979CC022100D23C2B2662E8AFA757F136B47116DB9F52A05EF2FD7F7326D08FE113B9E9EA79042061D6AFB6D97BD62119D3E90527A093AEE6A059A015B20960BF7574F1CAA39D4C0429FB2E3155BA0383D07CFE0D866E73DBB890B2DA037F07BA5D2A0D7E2CDAAECEF91DD4E169DB8412245C\n\x9e\xd3\xa2\xb9\x06\xbb\x0f\xd5o\xdb\x03\xee\x93\x84\x00\x8a\xc4\x98\x1d]q\x87?\x96f\xa8\rvH\x1b\xfa\xba\x04\x04\x04\x04\x04'
b"v\xf7\xc0O\xd9\x14\xb5\x08'\x17j\xe9\x89\xcc\x8b`308199022100C749061668652E26040E008FDD5EB77A344A417B7FCE19DBA575DA57CC372A9E022100F2DF5DB2D144E9454504C622B51CF38F5006206EB579FF7DA6976EFF5FBE64800420CD0DF57ACF39667C8E3BD5C16B9381DEAB56A2CE696A3ACE83E4D189AD98964A042F51D4412F8CBF06F3494D8500D32F70229FD381170BA690CD9BD376B55580C6E33523F79D9DA9519FF668EFB29888AC\n\x13\n\x1f\xdfjGh\xfc\x89Q\x9ez2\x0eD\xd5\xc0LC\xee\xe0\xed\xb6W\xa9\xde\x9c\x08)\x08\xe6\xc8\x06\x06\x06\x06\x06\x06\x06"
可以发现中间又套了一个SM2密文。同样的,和数据库表中的密文比对一下数据,发现用的随机数也是一样的,所以和上面一样的套路,都可以解密。解密的结果和数据库表中的明文也是一样的。

唯一不同的是最后一次TLCP通信,里面的Application Data消息解密后剩下的SM2密文,没法在数据库表中找到对应密钥,所以没法解密。这个时候我又去问了问出题人,了解到了最后一次SM2加密的随机数,其实用的是前面签名证书的私钥,知道了这一点后,就很快解密了数据,成功拿到flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# final.py
from gmssl import sm3
n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123
p = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF
a = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC
b = 0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93
E = EllipticCurve(GF(p), [a, b])
d = 56431408153695370061795421010209097251225434679562598847096735820032467303653
x = 0xde11a990ff68085d48d482ebc43f69806843810d57e89b348e052406d33ec1c2
y = 0x197f651f724edd26b43f283569c117b8586d2e0278f8b1fbb64f9247e08d0ca7
cipher = bytes.fromhex("8ffd6249f3e6733945100a05b0893e2d9290173f1101c3b2f0a7eb618464d2b594ea09f5")
PK = E((x, y))
S = d * PK
Sx = hex(int(S.x()))[2:].zfill(64)
Sy = hex(int(S.y()))[2:].zfill(64)
t = bytes.fromhex(sm3.sm3_kdf((Sx + Sy).encode(), len(cipher)))
flag = bytes([i^^j for i, j in zip(t, cipher)])
print(flag)
1
NepCTF{Y0u_A1e_900D_at_TLCP_And_SM_A1g0r1tm}