初始谜题
flag1
源码
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 import secrets default_table = { "n" : "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123" , "p" : "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF" , "g" : "32c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589334c74c7" "bc3736a2f4f6779c59bdcee36b692153d0a9877cc62a474002df32e52139f0a0" , "a" : "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC" , "b" : "28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93" , }class Crypt : def __init__ (self, private_key, public_key, mode=0 ): self .private_key = private_key if public_key.startswith("04" ): self .public_key = public_key[2 :] else : self .public_key = public_key self .para_len = len (default_table["n" ]) self .ecc_a3 = (int (default_table["a" ], base=16 ) + 3 ) % int ( default_table["p" ], base=16 ) assert mode in (0 , 1 ), "mode must be one of (0, 1)" self .mode = mode def _kg (self, k, Point ): if k == 0 : return None Point = "%s%s" % (Point, "1" ) mask_str = "8" for i in range (self .para_len - 1 ): mask_str += "0" mask = int (mask_str, 16 ) Temp = Point flag = False for n in range (self .para_len * 4 ): if flag: Temp = self ._double_point(Temp) if (k & mask) != 0 : if flag: Temp = self ._add_point(Temp, Point) else : flag = True Temp = Point k = k << 1 return self ._convert_jacb_to_nor(Temp) def _double_point (self, Point ): if Point is None : return None l = len (Point) len_2 = 2 * self .para_len if l < self .para_len * 2 : return None else : x1 = int (Point[0 : self .para_len], 16 ) y1 = int (Point[self .para_len : len_2], 16 ) if l == len_2: z1 = 1 else : z1 = int (Point[len_2:], 16 ) T6 = (z1 * z1) % int (default_table["p" ], base=16 ) T2 = (y1 * y1) % int (default_table["p" ], base=16 ) T3 = (x1 + T6) % int (default_table["p" ], base=16 ) T4 = (x1 - T6) % int (default_table["p" ], base=16 ) T1 = (T3 * T4) % int (default_table["p" ], base=16 ) T3 = (y1 * z1) % int (default_table["p" ], base=16 ) T4 = (T2 * 8 ) % int (default_table["p" ], base=16 ) T5 = (x1 * T4) % int (default_table["p" ], base=16 ) T1 = (T1 * 3 ) % int (default_table["p" ], base=16 ) T6 = (T6 * T6) % int (default_table["p" ], base=16 ) T6 = (self .ecc_a3 * T6) % int (default_table["p" ], base=16 ) T1 = (T1 + T6) % int (default_table["p" ], base=16 ) z3 = (T3 + T3) % int (default_table["p" ], base=16 ) T3 = (T1 * T1) % int (default_table["p" ], base=16 ) T2 = (T2 * T4) % int (default_table["p" ], base=16 ) x3 = (T3 - T5) % int (default_table["p" ], base=16 ) if (T5 % 2 ) == 1 : T4 = (T5 + ((T5 + int (default_table["p" ], base=16 )) >> 1 ) - T3) % int ( default_table["p" ], base=16 ) else : T4 = (T5 + (T5 >> 1 ) - T3) % int (default_table["p" ], base=16 ) T1 = (T1 * T4) % int (default_table["p" ], base=16 ) y3 = (T1 - T2) % int (default_table["p" ], base=16 ) form = "%%0%dx" % self .para_len form = form * 3 return form % (x3, y3, z3) def _add_point ( self, P1, P2 ): if P1 is None : return P2 if P2 is None : return P1 len_2 = 2 * self .para_len l1 = len (P1) l2 = len (P2) if (l1 < len_2) or (l2 < len_2): return None else : X1 = int (P1[0 : self .para_len], 16 ) Y1 = int (P1[self .para_len : len_2], 16 ) if l1 == len_2: Z1 = 1 else : Z1 = int (P1[len_2:], 16 ) x2 = int (P2[0 : self .para_len], 16 ) y2 = int (P2[self .para_len : len_2], 16 ) T1 = (Z1 * Z1) % int (default_table["p" ], base=16 ) T2 = (y2 * Z1) % int (default_table["p" ], base=16 ) T3 = (x2 * T1) % int (default_table["p" ], base=16 ) T1 = (T1 * T2) % int (default_table["p" ], base=16 ) T2 = (T3 - X1) % int (default_table["p" ], base=16 ) T3 = (T3 + X1) % int (default_table["p" ], base=16 ) T4 = (T2 * T2) % int (default_table["p" ], base=16 ) T1 = (T1 - Y1) % int (default_table["p" ], base=16 ) Z3 = (Z1 * T2) % int (default_table["p" ], base=16 ) T2 = (T2 * T4) % int (default_table["p" ], base=16 ) T3 = (T3 * T4) % int (default_table["p" ], base=16 ) T5 = (T1 * T1) % int (default_table["p" ], base=16 ) T4 = (X1 * T4) % int (default_table["p" ], base=16 ) X3 = (T5 - T3) % int (default_table["p" ], base=16 ) T2 = (Y1 * T2) % int (default_table["p" ], base=16 ) T3 = (T4 - X3) % int (default_table["p" ], base=16 ) T1 = (T1 * T3) % int (default_table["p" ], base=16 ) Y3 = (T1 - T2) % int (default_table["p" ], base=16 ) form = "%%0%dx" % self .para_len form = form * 3 return form % (X3, Y3, Z3) def _convert_jacb_to_nor (self, Point ): if Point is None : return None len_2 = 2 * self .para_len x = int (Point[0 : self .para_len], 16 ) y = int (Point[self .para_len : len_2], 16 ) z = int (Point[len_2:], 16 ) z_inv = pow ( z, int (default_table["p" ], base=16 ) - 2 , int (default_table["p" ], base=16 ) ) z_invSquar = (z_inv * z_inv) % int (default_table["p" ], base=16 ) z_invQube = (z_invSquar * z_inv) % int (default_table["p" ], base=16 ) x_new = (x * z_invSquar) % int (default_table["p" ], base=16 ) y_new = (y * z_invQube) % int (default_table["p" ], base=16 ) z_new = (z * z_inv) % int (default_table["p" ], base=16 ) if z_new == 1 : form = "%%0%dx" % self .para_len form = form * 2 return form % (x_new, y_new) else : return None def verify (self, Sign, data ): if Sign is None or Sign == "" : return None r = int (Sign[0 : self .para_len], 16 ) s = int (Sign[self .para_len : 2 * self .para_len], 16 ) e = int (data.hex (), 16 ) if not ( 1 <= r < int (default_table["n" ], base=16 ) and 1 <= s < int (default_table["n" ], base=16 ) ): return False t = r + s if t == 0 : return False else : t = t % int (default_table["n" ], base=16 ) if self .public_key is None or self .public_key == "" : return None P1 = self ._kg(s, default_table["g" ]) if P1 is None : return False P2 = self ._kg(t, self .public_key) if P1 == P2: P1 = "%s%s" % (P1, 1 ) P1 = self ._double_point(P1) else : P1 = "%s%s" % (P1, 1 ) P1 = self ._add_point(P1, P2) P1 = self ._convert_jacb_to_nor(P1) x = int (P1[0 : self .para_len], 16 ) return r == ((e + x) % int (default_table["n" ], base=16 )) def sign (self, data ): k = secrets.randbelow(int (default_table["n" ], 16 ) - 1 ) + 1 if not (1 <= k <= int (default_table["n" ], base=16 ) - 1 ): return None E = data.hex () e = int (E, 16 ) if self .private_key is None or self .private_key == "" : return None d = int (self .private_key, 16 ) P1 = self ._kg(k, default_table["g" ]) x = int (P1[0 : self .para_len], 16 ) A = (e + x) % int (default_table["n" ], base=16 ) if A == 0 or A + k == int (default_table["n" ], base=16 ): return None d_1 = pow ( d + 1 , int (default_table["n" ], base=16 ) - 2 , int (default_table["n" ], base=16 ), ) B = (d_1 * (k + A) - A) % int (default_table["n" ], base=16 ) if B == 0 : return None else : return "%064x%064x" % (A, B)if __name__ == "__main__" : crypt = Crypt( public_key="" , private_key="" , ) data = "919535c3ef53d3fa359196b5229c4bbb386ce209f5905d33fc7bcdff46ae27c2" sign = crypt.sign(bytes .fromhex(data)) print (sign) verify = crypt.verify(sign, bytes .fromhex(data)) print (verify)
分析
额啊,好久没碰 ECC 了,手感欠佳啊。距离之前学这部分知识有点久了,借着这道题回顾一下。
先看雅各比坐标。它带来的第三个量
Z
并不是给椭圆曲线增加了真实的第三维,而是引入了辅助坐标,用于解决普通算法频繁模逆的算力消耗。
假设我们使用普通二维仿射坐标计算
P = ( x 1 , y 1 )
的倍点,需要先算
λ = 3 x 1 2 + a 2 y 1
。其中对
2 y 1
在有限域上求模逆是很麻烦的。而真实场景中进行倍点运算的次数又极多,因此若是每一步都要求逆,性能会被大量浪费,而雅各比坐标的作用就是尽可能到最后一步才做模逆运算,从而大幅降低性能损耗。
不使用雅各比坐标的倍点运算:
记
P = ( x 1 , y 1 )
,
2 P = ( x 3 , y 3 )
。计算斜率:
λ = ( 3 x 1 2 + a ) ( 2 y 1 ) − 1 ( mod p )
得到最终结果:
x 3 = λ 2 − 2 x 1 , y 3 = λ ( x 1 − x 3 ) − y 1 ( mod p )
斜率计算时要进行一次模逆。
使用雅各比坐标的倍点运算:
设
P = ( X 1 , Y 1 , Z 1 )
,代表真正的仿射坐标
P = ( X 1 Z 1 − 2 , Y 1 Z 1 − 3 )
。记
M = 3 X 1 2 + a Z 1 4 , S = 4 X 1 Y 1 2 ( mod p )
最终有:
X 3 = M 2 − 2 S , Y 3 = M ( S − X 3 ) − 8 Y 1 4 , Z 3 = 2 Y 1 Z 1 ( mod p )
无需模逆,得到的雅各比坐标可以直接用于下次运算。这就是它的核心优势。
再看签名函数。对于给定的私钥
d
和整数
e
,加以基点
G
和基点阶
n
,返回签名
( r , s )
。
生成随机数
k ∈ [ 1 , n − 1 ]
计算椭圆曲线点
( x 1 , y 1 ) = k G
计算
r = ( e + x 1 ) mod n
,要求
k ≠ 0 , r + k ≠ n
计算
s = ( ( 1 + d ) − 1 ⋅ ( k − r d ) ) mod n
,要求
s ≠ 0
输出
( r , s )
对于验签流程,基于给定的数据
e
,签名
( r , s )
和公钥
P = d G
,返回验签结果。
从签名公式
s = ( 1 + d ) − 1 ⋅ ( k − r d )
入手,两边先同乘
( 1 + d )
,化简整理后有:
k = s + ( r + s ) d
同乘基点
G
:
k G = s G + ( r + s ) d G
代入公钥
P = d G
:
k G = s G + ( r + s ) P
此时等式右边都是已知量,因此验签方可以算出
k G
。取
k G ( x 1 , y 1 )
的
x 1
坐标,计算
( e + x 1 ) mod n
,验证其是否与签名中的
r
相符,是则返回 true。
最后回到本题目。对于这道题,目标是提交一组可以通过验签的摘要和签名,但并没有给出公钥。也就是说,这组签名需要对任意公钥均能成功,乍一听好像很难。先看 Verify(Sign, data) 函数前半部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def verify (self, Sign, data ): if Sign is None or Sign == "" : return None r = int (Sign[0 : self .para_len], 16 ) s = int (Sign[self .para_len : 2 * self .para_len], 16 ) e = int (data.hex (), 16 ) if not ( 1 <= r < int (default_table["n" ], base=16 ) and 1 <= s < int (default_table["n" ], base=16 ) ): return False t = r + s if t == 0 : return False else : t = t % int (default_table["n" ], base=16 )
源码先校验
( r , s )
是否在有限域范围内,再计算
t = r + s
,校验不为零后再取模。
核心的缺陷就在这里。因为是先校验再取模,所以只要把
r + s
构造为
n
的倍数,取模之后的
t
依然为
0
。一旦
t = 0
,后续 P2 = self._kg(t, self.public_key) 直接返回 None,_add_point(P1, None) 返回 P1。此时最终的验签就只会返回
r == ( e + x )
,其中
x = s G . x
。
伪造
s = 1 , r = n − 1
。此时
x = G . x
是固定值。因此构造
e = r − G . x ( mod n )
。此时我们就得到了摘要及其签名。
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 default_table = { 'n' : 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123' , 'p' : 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF' , 'g' : '32c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589334c74c7' 'bc3736a2f4f6779c59bdcee36b692153d0a9877cc62a474002df32e52139f0a0' , 'a' : 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC' , 'b' : '28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93' , } n = int (default_table["n" ], 16 ) s = 1 r = n - 1 e = (r - int (default_table["g" ][:64 ], 16 )) % nprint (f"{e:064x} " )print (f"{r:064x} {s:064x} " )
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 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modesimport structdef encrypt_card_data (item ) -> str : result = f"{item.card} ,{item.pin} ,{item.id } " plaintext = result.encode("utf-8" ) key = get_key() nonce = get_iv_byte(item.index) tmp = encrypt(key, nonce, plaintext).hex () return tmpdef get_iv_byte (index: int , prefix=b"" , length=16 ) -> bytes : if not (0 <= index < 2 **32 ): raise ValueError("Index 必须在 0 到 2^32-1 之间" ) index_bytes = struct.pack(">I" , index) pad_len = length - len (prefix) - len (index_bytes) if pad_len < 0 : prefix = b"" pad_len = length - len (index_bytes) return prefix + (b"\x00" * pad_len) + index_bytesdef encrypt (key, nonce, plaintext ) -> bytes : algorithm = algorithms.SM4(key) mode = modes.CTR(nonce) cipher = Cipher(algorithm, mode) encryptor = cipher.encryptor() ciphertext = encryptor.update(plaintext) + encryptor.finalize() return ciphertext
序号
密文 (十六进制)
1000
7d2a3dc99109ff913cc52877cf3578621eaf51d256b043550eac8c377c98357de1b0d616039e6f821850386d86d96057f195539c28b8ed31430f27b16ca55f
1001
1cae58cb5cb947500aa4963f7d93607eeeb4d717069f678b0053226887cd6754fdc24cd32bebb8395f0a72b86ee90c278c4323c288be33e5d9282884acf9fb
1002
eeb6d40e049f6f8003523e6f8fc33854f0ce46d229e3bf3d45083cb96efd0b25d4453c8e8db930ecc67c2ed7a8edff97a668a0c45d36b0c2b36257a2039117
序号
银行卡号
PIN 码
用户 ID
1000
6205310997298382439
984631
019f1942-4000-7650-a25c-b7ce812783a2
1001
6205310379680232603
118954
019f1942-4000-7b11-a3a2-a17392cc649e
1002
分析
被这道题阴了,我丢
我原本的理解是传进去 nonce 之后底层会自动在后面拼接 counter,也就是 00001000 || 00000001,想了半天不知道怎么做。最后看到源码里的 length=16 才发现传进去的是整个 counterblock,加密每个分组后直接 nonce++。由于加密使用的 nonce 来源为 index,而 index 本身也是连续递增的,所以加密第一份明文的密钥流的第二组,就是加密第二份密文的密钥流的第一组。
密钥流的两种生成方法:
0 ⊕ k e y s t r e a m = k e y s t r e a m
(需要密钥),或是
p l a i n t e x t ⊕ c i p h e r t e x t = k e y s t r e a m
。这里采用第二种。
由于每份数据长度都是 63 字节,也即 4 个分组,那么第二份密钥流的第 2-4 组就对应第三份密钥流的第 1-3 组。尽管最后一个分组还原不出来,但题目只让我们求卡号和 pin,已经足够了。
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import xorfrom Crypto.Util.number import * plaintext_1001 = b"6205310379680232603,118954,019f1942-4000-7b11-a3a2-a17392cc649e" cipher_1001 = bytes .fromhex( "1cae58cb5cb947500aa4963f7d93607eeeb4d717069f678b0053226887cd6754fdc24cd32bebb8395f0a72b86ee90c278c4323c288be33e5d9282884acf9fb" ) ks_1 = xor(cipher_1001[16 :32 ], plaintext_1001[16 :32 ]) ks_2 = xor(cipher_1001[32 :48 ], plaintext_1001[32 :48 ]) ks_3 = xor(cipher_1001[48 :], plaintext_1001[48 :]) cipher = bytes .fromhex( "eeb6d40e049f6f8003523e6f8fc33854f0ce46d229e3bf3d45083cb96efd0b25d4453c8e8db930ecc67c2ed7a8edff97a668a0c45d36b0c2b36257a2039117" ) res = xor(cipher[:16 ], ks_1) + xor(cipher[16 :32 ], ks_2) + xor(cipher[32 :48 ], ks_3)print (res)
工程师站路线
flag1 - 登录工程师站服务器
源码
login_handler.go
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 package controllerimport ( "bytes" "crypto/ecdsa" "encoding/hex" "engineer_station_server/config" "engineer_station_server/sdk" "engineer_station_server/service" "engineer_station_server/utils" "net/http" "github.com/gin-gonic/gin" "github.com/tjfoc/gmsm/sm2" "github.com/tjfoc/gmsm/sm3" )type AuthType string const ( AuthTypePassword AuthType = "password" AuthTypeSymmetric AuthType = "symmetric" AuthTypeSignature AuthType = "signature" )type LoginReq struct { AuthType AuthType `json:"auth_type" binding:"required,min=1,max=256"` Username string `json:"username" binding:"required,min=1,max=256"` Password string `json:"password" binding:"omitempty,min=1,max=1024"` Challenge string `json:"challenge" binding:"omitempty,min=64,max=64"` AuthInfo string `json:"auth_info" binding:"omitempty,min=1,max=1024"` }var authHandlers = map [AuthType]func (*gin.Context, *LoginReq) { AuthTypePassword: handlePassword, AuthTypeSymmetric: handleSymmetric, AuthTypeSignature: handleSignature, }var ( certService = service.NewCertService() )func LoginHandler (c *gin.Context) { var req LoginReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "参数不合法" }) return } handler, ok := authHandlers[req.AuthType] if !ok { c.JSON(http.StatusBadRequest, gin.H{"error" : "不支持的认证方式" }) return } handler(c, &req) }func handlePassword (c *gin.Context, req *LoginReq) { if len (req.Password) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error" : "参数不合法" }) return } userInfo, err := sdk.GetUserInfo(req.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "从账号管理服务器获取用户信息失败" }) return } if userInfo == nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "用户不存在" }) return } if userInfo.Role != sdk.RoleUser { c.JSON(http.StatusBadRequest, gin.H{"error" : "该用户不支持所选登录方式" }) return } if len (userInfo.PasswordHash) == 0 { c.JSON(http.StatusInternalServerError, gin.H{"error" : "账号管理服务器中口令哈希值不合法" }) return } passwordHashBytes, err := hex.DecodeString(userInfo.PasswordHash) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "账号管理服务器中口令哈希值不合法" }) return } salt, ok := config.SaltByUsername[userInfo.Username] if !ok { c.JSON(http.StatusInternalServerError, gin.H{"error" : "盐值错误" }) return } saltBytes, err := hex.DecodeString(salt) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "盐值错误" }) return } hashValue := sm3.Sm3Sum(append ([]byte (req.Password), saltBytes...)) if !bytes.Equal(passwordHashBytes, hashValue) { c.JSON(http.StatusBadRequest, gin.H{"error" : "口令有误" }) return } ObtainFlag1() token, err := utils.GenerateToken(req.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "令牌生成失败" }) return } c.JSON(http.StatusOK, gin.H{"token" : token}) }func handleSymmetric (c *gin.Context, req *LoginReq) { if len (req.Challenge) == 0 || len (req.AuthInfo) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error" : "参数不合法" }) return } challengeFromReq, err := hex.DecodeString(req.Challenge) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "参数不合法" }) return } if _, err = hex.DecodeString(req.AuthInfo); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "参数不合法" }) return } userInfo, err := sdk.GetUserInfo(req.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "从账号管理服务器获取用户信息失败" }) return } if userInfo == nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "用户不存在" }) return } if userInfo.Role != sdk.RoleAdmin { c.JSON(http.StatusBadRequest, gin.H{"error" : "该用户不支持所选登录方式" }) return } if userInfo.EncryptedAuthKey == "" { c.JSON(http.StatusInternalServerError, gin.H{"error" : "账号管理服务器中身份认证密钥密文不合法" }) return } authKeyCiphertextBytes, err := hex.DecodeString(userInfo.EncryptedAuthKey) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "账号管理服务器中身份认证密钥密文不合法" }) return } if len (authKeyCiphertextBytes) != 113 { c.JSON(http.StatusInternalServerError, gin.H{"error" : "账号管理服务器中身份认证密钥密文不合法" }) return } authKeyHandle, err := sdk.ImportKeyToHSM(1 , authKeyCiphertextBytes) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "账号管理服务器中身份认证密钥密文不合法" }) return } challenge := GetChallenge(req.Username) if !bytes.Equal(challengeFromReq, challenge) { c.JSON(http.StatusBadRequest, gin.H{"error" : "挑战值已过期,请重新获取挑战值" }) return } decryptedData, err := sdk.SM4ECBDecryptAndPKCS7UnPaddingFromHex(authKeyHandle, req.AuthInfo) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "鉴别信息有误" }) return } if !bytes.Equal(decryptedData, challenge) { c.JSON(http.StatusBadRequest, gin.H{"error" : "鉴别信息有误" }) return } ObtainFlag1() token, err := utils.GenerateToken(req.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "令牌生成失败" }) return } c.JSON(http.StatusOK, gin.H{"token" : token}) }func handleSignature (c *gin.Context, req *LoginReq) { if len (req.Challenge) == 0 || len (req.AuthInfo) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error" : "参数不合法" }) return } challengeFromReq, err := hex.DecodeString(req.Challenge) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "参数不合法" }) return } if _, err := hex.DecodeString(req.AuthInfo); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "参数不合法" }) return } userInfo, err := sdk.GetUserInfo(req.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "从账号管理服务器获取用户信息失败" }) return } if userInfo == nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "用户不存在" }) return } if userInfo.Role != sdk.RoleSuperAdmin { c.JSON(http.StatusBadRequest, gin.H{"error" : "该用户不支持所选登录方式" }) return } if len (userInfo.Cert) == 0 { c.JSON(http.StatusInternalServerError, gin.H{"error" : "账号管理服务器中证书不合法" }) return } challenge := GetChallenge(req.Username) if !bytes.Equal(challengeFromReq, challenge) { c.JSON(http.StatusBadRequest, gin.H{"error" : "挑战值已过期,请重新获取挑战值" }) return } cert, err := certService.LoadCertificate(userInfo.Cert) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "账号管理服务器中证书不合法" }) return } err = certService.ValidateCertificate(cert, service.RootCert) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "账号管理服务器中证书不合法" }) return } ecdsaPubKey, ok := cert.PublicKey.(*ecdsa.PublicKey) if !ok { c.JSON(http.StatusInternalServerError, gin.H{"error" : "账号管理服务器中证书不合法" }) return } sm2PubKey := sm2.PublicKey{ Curve: ecdsaPubKey.Curve, X: ecdsaPubKey.X, Y: ecdsaPubKey.Y, } _, err = utils.ValidateSignature(req.Challenge, req.AuthInfo, &sm2PubKey) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "鉴别信息有误" }) return } ObtainFlag1() token, err := utils.GenerateToken(req.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "令牌生成失败" }) return } c.JSON(http.StatusOK, gin.H{"token" : token}) }
密码机接口调用 demo.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 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 #include "sdf_cryptoapi.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> void * devHandle = NULL ;void * sessionHandle = NULL ;void * keyHandle = NULL ;int open_session () { int ret = 0 ; ret = SDF_OpenDeviceWithPath("./sdt_hsmcrypt.conf" , &devHandle); if (ret) { printf ("SDF_OpenDeviceWithPath error, ret=%08x\n" , ret); return ret; } ret = SDF_OpenSession(devHandle, &sessionHandle); if (ret) { printf ("SDF_OpenSession error, ret=%08x\n" , ret); SDF_CloseDevice(devHandle); return ret; } return ret; }int close_session () { int ret = 0 ; ret = SDF_CloseSession(sessionHandle); if (ret) { printf ("SDF_CloseSession error, ret=%08x\n" , ret); SDF_CloseDevice(devHandle); return ret; } ret = SDF_CloseDevice(devHandle); if (ret) { printf ("SDF_CloseDevice error, ret=%08x\n" , ret); return ret; } return ret; }int init () { int ret = 0 ; ret = open_session(); if (ret) { printf ("open_session error, ret=%08x\n" , ret); return ret; } return 0 ; }int destroy () { int ret = 0 ; ret = close_session(); if (ret) { printf ("[ts_symm] close_session error, ret=%08x\n" , ret); return ret; } return 0 ; }int hex_string_to_char_array (const char * hex, unsigned char * out) { size_t hex_len = strlen (hex); if (hex_len % 2 != 0 ) return -1 ; for (size_t i = 0 ; i < hex_len; i += 2 ) { unsigned int byte; if (sscanf (hex + i, "%2X" , &byte) != 1 ) return -1 ; out[i / 2 ] = (unsigned char )byte; } return hex_len / 2 ; }size_t ECCCipher_serialize (const ECCCipher* cipher, unsigned char * out) { size_t offset = 0 ; out[0 ] = 0x04 ; offset += 1 ; memcpy (out + offset, cipher->x + 32 , 32 ); offset += 32 ; memcpy (out + offset, cipher->y + 32 , 32 ); offset += 32 ; memcpy (out + offset, cipher->M, 32 ); offset += 32 ; memcpy (out + offset, cipher->C, 16 ); offset += 16 ; return offset; }const char hex_table[] = "0123456789ABCDEF" ;void char_array_to_hex_string (const unsigned char * src, size_t len, char * dst) { for (size_t i = 0 ; i < len; i++) { dst[2 * i] = hex_table[(src[i] >> 4 ) & 0x0F ]; dst[2 * i + 1 ] = hex_table[src[i] & 0x0F ]; } dst[2 * len] = '\0' ; }int main (int argc, char * argv[]) { int ret = 0 ; ret = init(); if (ret) { printf ("init error, ret=%08x\n" , ret); return ret; } ret = destroy(); if (ret) { printf ("destroy error, ret=%08x\n" , ret); } return 0 ; }
分析
当时进 gitee 之后看到代码一半 Go 一半 C,感觉心都凉了。学了一段时间 Go 之后回来复现一下。当时隐隐约约感觉到好像是要从 RoleAdmin 部分下手,但是苦于看不懂代码逻辑,又不会伪造手法,最终还是 return null 了。
先介绍一下挑战-应答机制。其核心思想是:验证方向证明方提出一个不可预测的随机质询,证明方利用自身掌握的秘密计算出对应的应答并返回,从而在不泄露秘密的情况下完成身份验证。一个完整的挑战-应答认证流程分为四部分:
声明身份。客户端(证明方)向服务器(认证方)发送认证请求,声明自己的身份标识。
下发质询。服务器生成一个不可预测且一次性使用的密码学安全的随机数(称为质询值
R
),发送给客户端。
计算应答。客户端使用本地保存的秘密(密钥或私钥)与质询值
R
结合,经过密码学算法运算得到响应值
r e s = f ( s e c r e t , R )
,并将
r e s
返回给服务器。
校验结果。服务器利用自己持有的信息(密钥或公钥)独立验证
r e s
的合法性(在非对称加密中相当于验签,对称加密中计算
r e s ′ = f ( s e c r e t ′ , R )
,比对
r e s == r e s ′
即可),验证成功则通过认证,该质询值立即失效。
挑战-应答机制天生抗重放攻击。在本题中,RoleAdmin 用户尝试登录的正常流程如下:
用户向服务器申请登录
服务器调用密码机获取挑战值(SDF_GenerateRandom),下发给用户
用户使用 SM4-ECB 模式下的密钥对挑战值加密,将结果发送给服务器
服务器调用密码机(其内部存储着被 SM2 加密过的密钥)。密码机使用 1 号私钥解密 密钥的密文 ,生成临时密钥 hKey(SDF_ImportKeyWithISK_ECC(1, Enc_Key, &hKey))
密码机使用 hKey 解密挑战值(SDF_Decrypt(hKey, C, &R')),比对
R == R ′
(这里直接比对解密后的挑战值)
销毁密码机内部临时密钥(SDF_DestroyKey(hKey)),认证流程结束
密码机的公钥可以获取,密钥的密文可以任意篡改,我们只需保证最终
R == R ′
就行。因此先任意伪造一份密钥,接着获取密码机的公钥,使用公钥加密密钥,最终传入密钥的密文。这样密码机在收到解密请求后,得到的 hKey 就会是我们自行伪造的密钥。前后都是同一密钥,自然有
R == R ′
。
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 #include "sdf_cryptoapi.h" #include <stdio.h> #include <string.h> int get_sm2_public_key (unsigned int key_index, int is_sign_key, unsigned char *out_pubkey_65b) { int ret = 0 ; ECCrefPublicKey ecc_pubkey; memset (&ecc_pubkey, 0 , sizeof (ECCrefPublicKey)); if (is_sign_key) { ret = SDF_ExportSignPublicKey_ECC(sessionHandle, key_index, &ecc_pubkey); } else { ret = SDF_ExportEncPublicKey_ECC(sessionHandle, key_index, &ecc_pubkey); } if (ret != 0 ) { printf ("Export ECC PublicKey failed, key_index=%u, ret=%08X\n" , key_index, ret); return ret; } out_pubkey_65b[0 ] = 0x04 ; memcpy (out_pubkey_65b + 1 , ecc_pubkey.x + 32 , 32 ); memcpy (out_pubkey_65b + 1 + 32 , ecc_pubkey.y + 32 , 32 ); return 0 ; }
随意设定一个密钥(b"A" * 16),用先前获得的 SM2 公钥对其加密。
将得到的密文前加上 04,用自己的密文替换掉 Bob 的身份验证密钥密文。
获取挑战值后对该挑战值使用自己的密钥进行 SM4-ECB 加密即可通过验证。
flag3 - 解密 PLC 控制指令加密程序源码密文
源码
加密接口文档:
接口功能说明
该加密接口用于加密 PLC 控制指令加密程序源码。
接口地址
POST /api/encrypt
请求头
Content-Type: application/json
请求参数
1 2 3 { "plaintext" : "string, 必填, 待加密原文" }
响应示例
1 2 3 4 { "ciphertext" : "3770977f80623bacf81636b5a8834149b8307d5e246cb4889eb15f92f5d366" , "iv" : "eb032da9bd6b4c0f4a11b3fe26232f89" }
状态码
200: 加密成功
400: 参数错误
401: 权限不足
完整 python 调用实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import requestsimport json IP = "xxx.xxx.xxx.xxx" TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" if __name__ == "__main__" : url = f"http://{IP} :5000/api/encrypt" payload = json.dumps({ "plaintext" : "123" }) headers = { 'Authorization' : TOKEN, 'Content-Type' : 'application/json' } response = requests.request("POST" , url, headers=headers, data=payload) print (response.text)
分析
这部分没有原题,只能看着 Van1sh 师傅的解答复现一下了。
已知 iv 会改变,但改变的范围很小。大量爆破,尝试碰撞与加密目标数据时相同的 iv。不断传入与目标数据等长的明文,获得对应密文和 iv。已知密钥流等于明文与密文异或,那么只需使用每次的密钥流解密目标数据,检测是否能够还原出可读数据(开头含有 import)即可。
AES 的流密码模式:
CTR
原理:将一个初始向量与一个随分组递增的计数器拼接,传入算法生成密钥流,再与明文异或得到密文。
密钥块产生:
K i = A E S k e y ( N o n c e | | i )
并行能力:加解密均可并行(同步计算第
i
块的密钥流即可)
密文单比特错误传播:仅影响对应的 1bit
OFB
原理:通过不断迭代加密 iv 以生成密钥流,即前一个 AES 输出作为后一个 AES 输入,生成的密钥流同样独立于明文和密文。
密钥块产生:
K 1 = A E S k e y ( i v )
,后续
K i = A E S k e y ( K i − 1 )
并行能力:均不可并行,但是允许提前计算密钥流(因为不依赖具体明密文)
密文单比特错误传播:仅影响对应的 1bit
CFB
原理:将前一个密文块送入算法加密,加密后的输出与当前明文块异或得到明文。
密文产生:
C 1 = P 1 ⊕ A E S k e y ( i v )
,后续
C i = P i ⊕ A E S k e y ( C i − 1 )
明文还原:
P 1 = C 1 ⊕ A E S k e y ( i v )
,后续
P i = C i ⊕ A E S k e y ( C i − 1 )
并行能力:加密时不可并行,解密时可并行(此时密文完全已知)
密文单比特错误传播:影响对应的 1bit 与下一个明文块(17 字节,不为最后一块的情况)
本题中 相同明文加密后生成的密文相同 ,也就是没有随机变量影响,故排除依赖 Nonce 的 CTR。又因为本题不返回密钥本身,CFB 的解密又强依赖
A E S k e y ( C i )
,故只可能是流密码中的 OFB。
flag5 - 操控机器人
源码
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 import jsonimport numpy as np n = 256 q = 3329 t = 2 Delta = q // 2 eta = 2 def poly_mul (a, b ): """多项式乘法 in R_q = Z_q[x]/(x^n + 1)""" out = np.zeros(n, dtype=int ) for i in range (n): for j in range (n): if i + j < n: out[i + j] = (out[i + j] + a[i] * b[j]) % q else : out[(i + j) % n] = (out[(i + j) % n] - a[i] * b[j]) % q return outdef poly_add (a, b ): return (a + b) % qdef poly_sub (a, b ): return (a - b) % qdef gen_cbd_poly (): a = np.random.randint(0 , 2 , size=(eta, n)).sum (axis=0 ) b = np.random.randint(0 , 2 , size=(eta, n)).sum (axis=0 ) return (a - b) % qdef gen_uniform_poly (): """生成均匀分布的多项式""" return np.random.randint(0 , q, n)def str_to_bits (s ): bits = np.zeros(n, dtype=int ) for i in range (min (len (s), 32 )): val = ord (s[i]) for j in range (8 ): bits[i * 8 + j] = (val >> (7 - j)) & 1 return bitsdef bits_to_str (bits ): chars = [] for i in range (32 ): val = 0 for j in range (8 ): val = (val << 1 ) | bits[i * 8 + j] if val == 0 : break chars.append(chr (val)) return "" .join(chars)def keygen (): s = gen_cbd_poly() a = gen_uniform_poly() e = gen_cbd_poly() b = poly_add(poly_mul(a, s), e) return (a, b), sdef encrypt (pk, msg_bits ): a, b = pk u = gen_cbd_poly() e1 = gen_cbd_poly() e2 = gen_cbd_poly() part1 = poly_add(poly_mul(a, u), e1) m_scaled = (msg_bits * Delta) % q part2 = poly_add(poly_add(poly_mul(b, u), e2), m_scaled) return part1, part2def decrypt (sk, part1, part2 ): noisy_m = poly_sub(part2, poly_mul(part1, sk)) return np.round (noisy_m * 2 / q).astype(int ) % 2 def bytes_to_numpy (byte_data ): arr = np.frombuffer(byte_data, dtype="<i4" ) return arrdef numpy_to_bytes (arr ): arr_le = arr.astype("<i4" , copy=False ) return arr_le.tobytes()def encrypt_plc_command (cmd ): """ 加密 PLC 控制指令 :param cmd: 加密控制指令 :return: """ public_key = { "a" : "7c080000f302000091080000880400008e050000ec0b00002303000066030000970a00001b080000170c0000b00b00009b070000a60400001a0c000046060000820400007404000098060000470000000a060000cf0b0000ca050000bb0c00004c0b0000690b000023040000de020000ac0900008b030000be020000e20c00006c000000ed050000c90b0000770c0000930200003b0300005509000026040000a409000049050000160b00004b030000ab040000b5090000af050000330a00007c0600004404000087080000430a00000d0a0000fb060000d7080000ab0c0000ed0b00008a0b0000cc050000220b00003d0a00005408000008020000dc05000009000000a704000052090000470800006c060000330a000078030000370b0000600c0000090700000d040000830b0000100600001607000032000000bb020000d6060000f90800003b0700001d030000720200003e0a000017010000a7090000b2040000990c0000c800000007000000d5000000d501000025090000070b0000050600004c030000e8000000d20000007101000016080000a90c0000e408000010020000a00b000079070000340200000b020000180700005b0500009e0b0000e3080000060600002a040000220c00002309000061090000dc08000024050000290c00001a00000083090000650700001b0a000084060000740c0000370b0000c20200006a040000c00b0000540300003e02000078020000bd02000053070000610200009302000033010000bf0600001e090000990300000d04000099010000fa070000df03000087040000e40b00008a05000097020000f00b000084000000e10700005d0c0000dd090000cc0c0000280c0000690b0000a10b00007308000082070000f003000081020000b90a00004b0400008e0300009107000054080000ce0b0000fd0a0000ab0700004b020000970900004a040000ed020000d10b0000200a0000c20c00005e00000044040000220b00005c0200001e030000a60800003d00000033080000b107000010070000260100004b0a00008c0300002c05000053030000a7070000da000000b20100001407000055070000540c0000990a00002c0c00007d0500000c06000094090000fd0800002b0100003e04000010000000250a00004601000050070000980b0000df050000c50400006d03000022080000cf0000003d04000019030000520300008e030000940a000001000000460500006c040000a0080000b70c0000180b0000db0b0000370c00002d0b000091080000160400001d0a00009e040000d008000086080000c7040000340c0000be070000a900000019090000480700002e0b0000290800001f0700007d030000b106000048010000170a00000d040000be0400005b0b0000dc0600009103000039030000" , "b" : "bc08000032060000ea000000c50000008e030000340b00002500000034030000da040000a0030000a7010000a704000096090000cb090000d20500004d0a00004d0200007f0b0000af0a00009b0a00008f030000e007000028030000730600008d050000390b0000bc02000087060000330a00000f0a0000cb080000ac0c0000f0050000af01000018040000b0080000a50a00009e0a0000690b0000c4030000dd010000dd0900000b0200001b0000001a010000de090000690b000028070000540400008d010000be040000af010000800a00001b0300004e040000210400005f050000af0300002d070000e6030000130300001907000004070000d7040000c300000076020000e30800008d000000390b000015060000220b00002205000043080000e8060000ca040000ec080000300b0000f20a0000df08000034030000d10b0000780b000010090000610100007e00000023050000e40600003e0c0000a20a00005c020000b4090000b904000063080000db040000c7020000b6050000630b0000e9090000ff0b00008700000025030000180b000057050000a90400001c020000080700000e0b0000df0b000076080000d50800008c06000075070000b30c0000b5030000490300008b0200004b0000008d090000f30100005a020000c0010000f50c0000330a00002501000067000000c9030000bc05000042090000a20c00008f04000013000000690a00004d020000df0a00004405000003080000db010000f30c0000e0020000f6000000ee090000720b0000e2040000f80b00000b0300009f070000cb040000020a0000df0b0000170a0000ae0000005c08000074010000ff000000ac000000be09000010020000d3090000b2030000cb0800008f080000b40200003a02000023080000be08000098010000730900003c0c00009d050000d90200007d0b00003e090000de0800009006000004040000060c00003c0100008c000000ce0900009700000004070000e9070000c8090000ce090000330300001e0600007d040000d804000033050000b70600000c07000004000000b4090000750600009e0500001c090000b7000000e1070000930a00004903000034070000db0c000068000000620900000d0600009300000090030000dd060000b1080000d30600003e080000fc0a0000280b00001d070000e8040000df0c00009d0100000f010000bd0a00006c0b0000ad0400004d0c00004c01000008080000ff0a0000ec0b0000a8000000ce080000c2090000f10700000a0a0000ac05000053090000a40900002b0c00005b060000d80800004a0600000a050000120a000080060000240c0000a20200004d020000660200001e0a0000a2050000b6010000cb090000e3020000f7060000170a0000db040000d10b00004c020000d60b0000" , } a = bytes_to_numpy(bytes .fromhex(public_key["a" ])) b = bytes_to_numpy(bytes .fromhex(public_key["b" ])) cmd_bits = str_to_bits(cmd) part1, part2 = encrypt((a, b), cmd_bits) return part1, part2if __name__ == "__main__" : with open ("robot_id.txt" , "r" ) as f: robot_id = f.read() start_cmd = robot_id + "Start###" part1, part2 = encrypt_plc_command(start_cmd) part1_hex = numpy_to_bytes(part1).hex () part2_hex = numpy_to_bytes(part2).hex () print (part1_hex) print (part2_hex) """ 输出的机器人开机控制指令密文: { "part1": "820a000033020000c9090000b30c0000430800000009000063020000310400000c070000740700004101000070010000f90700002705000045010000b8090000eb0a0000cd010000ab030000a8010000f6000000ed06000030040000e6040000b1030000d60700008b050000ba040000e2090000950700005e000000b0070000310c0000f704000077070000d20900001d0100007b0a0000da050000b1000000d3060000d10c0000e6000000c204000031080000bb000000e5020000f8070000e90700000a000000c50000006e040000290200007c0a000001010000bb080000640a0000f50b0000160800003204000075050000af0b0000050a00002d02000099070000e6040000fd03000066050000810700002a010000810b0000e306000014030000490500005f010000d6010000050000004c0200009e0600006b050000c60200001a080000f10700004d080000850200006e09000083070000b103000001090000a3010000100a00001a0200009e0b00006e030000dd080000680b000096020000ca0600008c06000046030000ac09000038020000050100003e0600005c010000170400007a050000a40800008f040000460800002b0500006b090000f60900004807000061060000850c0000cb050000540900006d0c0000a506000071000000b7010000010b0000d3030000700b00005d0c0000ec0000007d0200001f070000ec0800006d0c00007e0000008d060000b10600005e030000d2060000f6080000d2020000fb0b000086010000b80600006e060000de010000c805000073080000550c0000eb0a00002e04000091060000ca08000033010000c80b0000b7090000e40000005b080000580a0000670b00005d0800007c0100006b0700007905000076040000a80800004506000051000000c201000016000000f903000078060000ba0500002f000000d0050000cf02000058010000820c0000ec0800002909000042020000ec090000fe010000c1060000660700000e0a000047060000d301000054060000550a00003f030000f808000073010000f80800005b070000d50900008e000000480000006b000000b9090000460c0000a70b00008e0a0000910c00007d0b0000110400009000000089070000f2090000b70a0000c8030000110200006e050000060400002e090000c00700001a090000c0040000a808000028060000570700008d0a0000400c0000020500002f0900000b000000a807000051060000e80b0000630c0000de040000a90b00007e090000ec050000290700008c0700002a0c0000d2030000f4000000b80100009c010000ba020000370a000032020000c60b000072040000170400009a010000c5030000b20c0000500c0000660a0000e8030000bf0500005c040000a0070000640100002f06000038090000", "part2": "47080000c00500006f0a00004b090000bf0600004e010000fb0400002d01000043010000a7070000dd0b0000e40a0000bb0300003e060000e50800005a070000e3080000f70000007e08000092010000b4080000c7010000f00b000084070000e00c000052080000ba030000ae050000eb090000dc04000021080000f1080000e3010000aa0c00000b08000032010000a60b0000cd000000710400001f0a0000080c0000d3090000ea070000cb010000870300006a0200005d00000088090000c7020000c40100009a07000042000000d3000000c5040000e00700007b0a0000bb0900000907000039000000060600002f020000c60300001d040000a6090000640b000078090000e30900004002000084030000ef010000740400000b0b0000cf0c0000b30600007904000028030000cf0b0000ed06000089000000570100009701000018040000760400000b0900008305000007030000960400001b0700005303000006060000ec090000f4050000130b0000b50800000402000079010000770a0000ac090000ec090000170a000047000000f40c0000a4050000a50c0000100400007a0400008f030000cc0a0000b5080000e306000097040000a4030000ce0a0000c20900007c0700003d0c000046090000c00600000b0c0000ef0a000046000000580b0000fb0100004b07000012010000c005000041090000580b0000f10000000d080000c101000008090000cf090000a0000000f30700009e020000bb040000fa0a0000730b0000b7000000810200009a0c0000510400000b0500006b070000230b0000990c0000bc04000003010000c8080000560900005c090000ee080000ab0500001a080000110a000029020000be0700002a0c00004a050000fd010000d80b00007206000087060000cc020000970000008305000024010000a40000006c050000f60300006a050000cd01000027080000bd030000e6050000e1080000280a0000c3030000ba06000084030000ce0700007a0b00001c0c000026020000d6090000ed0000005e080000250b000050060000f90c00002c0c0000de0c0000350b0000c4080000fd0300008b0a0000ca070000ea030000a8090000e40a0000d4070000610400004d040000e80a0000b50700005e0c0000200c00007d080000470100004d01000035020000e3030000020c0000d90b0000190b00009a090000ab090000e40a0000680400009e0b0000c607000083090000490600006d030000d009000069010000a7000000a60100001b0c0000b5090000ef050000e80b0000610b0000e1020000ca01000024080000880a0000ee060000190b0000b40100001105000071090000490200008b000000080600000c0500003a06000085070000010c0000170700000f0b0000330b000042060000d70a00007e020000" } """
分析
很有趣的题目,浅析一下。
源码实现了一份基于 Ring-LWE 的公钥加密方案,所有的运算在多项式环
Z q [ x ] / ( x n + 1 )
上进行。
n = 256
是多项式的最高次幂,
q = 3329
是所有系数的模数。
密钥生成
私钥:使用 gen_cbd_poly() 生成一个系数极小的多项式
s
作为私钥保存。
公钥:先使用 gen_uniform_poly() 生成一个系数在
0 ∼ 3328
之间均匀分布的多项式
a
,再次使用 gen_cbd_poly() 生成一个小的误差多项式
e
。计算
b = a ⋅ s + e ( mod p )
,将
( a , b )
作为公钥公开。
依赖 LWE 问题的特质,攻击者无法在多项式时间内准确找到私钥
s
。
加密
使用公钥对 256bits 的二进制明文消息加密。
生成三个新的小误差多项式
u , e 1 , e 2
将输入的 msg_bits 乘以 Δ,将比特 1 映射到 Δ,比特 0 仍为 0,得到
m s c a l e d
分别计算密文的两部分:
p a r t 1 = a ⋅ u + e 1 ( mod q )
,
p a r t 2 = b ⋅ u + e 2 + m s c a l e d ( mod q )
,返回
( p a r t 1 , p a r t 2 )
解密
使用私钥
s
恢复明文。
计算
( m n o i s e = p a r t 2 − p a r t 1 ⋅ s = ( b ⋅ u + e 2 + m s c a l e d ) − ( a ⋅ u + e 1 ) ⋅ s
。由于
b = a ⋅ s + e
,所以
m n o i s e = m s c a l e d + ( e ⋅ u + e 2 − e 1 ⋅ s )
,其中
m s c a l e d = m ⋅ Δ
。
此时得到的
m n o i s e
是一个被放大的明文加多个小噪声。执行 noisy_m * 2 / q 后,明文原本为 1 的位变为
1 ± e r r
,为 0 的位变为
± e r r
。注意这里在模环上,当 0 位变为
− e r r
时,会回绕到模数的上界(这也是最后模 2 的原因)。结合 np.round(),最终舍去
e r r
,得到原本的
m
。
加法同态性
假设有两个 1bit 明文
m 1 , m 2 ∈ { 0 , 1 }
使用相同的公钥进行加密,得到两个密文
C 1 = ( c 11 , c 12 ) , C 2 = ( c 21 , c 22 )
:
c 11 = a ⋅ u 1 + e 11 c 12 = b ⋅ u 1 + e 12 + m 1 ⋅ Δ c 21 = a ⋅ u 2 + e 21 c 22 = b ⋅ u 2 + e 22 + m 2 ⋅ Δ
将两个密文按位相加得到
C a d d = ( c 11 + c 21 , c 12 + c 22 ) = ( c a d d 1 , c a d d 2 ) ( mod q )
。解密有
m n o i s e A d d = c a d d 2 − c a d d 1 ⋅ s
。代入公钥定义后化简:
m n o i s e A d d = ( m 1 + m 2 ) ⋅ Δ + [ e ⋅ ( u 1 + u 2 ) + ( e 12 + e 22 ) − s ⋅ ( e 11 + e 21 ) ]
记噪声项为
E a d d
,那么后面就可看作一个小的复合噪声。此时有:
m n o i s e A d d = ( m 1 + m 2 ) ⋅ Δ + E a d d
经过 round() 函数和模运算后,可以还原出
m 1 + m 2 ( mod 2 )
,也即
m 1 ⊕ m 2
。综上:
E n c ( m 1 ) + E n c ( m 2 ) = E n c ( m 1 ⊕ m 2 )
EXP
根据异或性质,我们只需要加密一份 0(24) || (Start### ⊕ PickUp##),将该密文与原始密文相加,机器人解密之后就会得到 Start### ⊕ Start### ⊕ PickUp## = PickUp##。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from flag5 import *from pwn import xor suffix = xor(b"Start###" , b" PickUp##" ) payload = (b"\x00" * 24 + suffix).decode("latin1" ) new_part1, new_part2 = encrypt_plc_command(payload) orig_part1 = bytes_to_numpy(bytes .fromhex(orig_dict_hex["part1" ])) orig_part2 = bytes_to_numpy(bytes .fromhex(orig_dict_hex["part2" ])) final_part1 = new_part1 + orig_part1 final_part2 = new_part2 + orig_part2print (numpy_to_bytes(final_part1).hex ())print (numpy_to_bytes(final_part2).hex ())
日志管理服务器路线
flag2 - 连接日志管理服务器
源码
历史 TLS 流量包一份
分析
先用 Wireshark 分析流量包,如图:
共 18 帧,其中含有大量 ACK 确认包。收到对方的数据时必须要发 ACK 确认;如果自己恰好有数据要回复,就顺路捎带确认信息一起发(本流量包的 Frame7 就捎带了 Frame6 的确认消息)。如果自己没数据要回,才会单独发一个只有确认消息的空包。
在上方的过滤器中输入 tcp.len > 0 以筛除三次握手和 ACK 包,得到:
由上图可以看出双向 TLS 在 Frame 4, 6, 7, 9 中完成身份认证,随后客户端加密向服务端发送 4 条短的 Application Data,但服务端并未正式回应。消息不重要,先简析一下双向 TLS:
普通 HTTPS 是单向认证,只验证服务器的身份(要求网站出示 SSL/TLS 证书)。双向 TLS(Mutual TLS,简称 mTLS),顾名思义,就是客户端和服务器需要互相验证身份。具体步骤如下:
客户端发起认证(Client Hello)
客户端主动向服务端发包,附上自己支持的加密规则(密码套件)以及随机数(用于后续密钥协商)。
服务器回应(Server Hello)
服务器收到请求后,选定一套双方都支持的加密规则,并生成另一个随机数发送给客户端。
服务器自证(Server Certificate)
服务器将自己的数字证书发给客户端(客户端需要信任 CA 根证书),向客户端证明正规性。
服务器请求证书(Certificate Request)
在普通 HTTPS 中,服务器到上一步就会结束身份验证。但在 mTLS 中,服务器会额外向客户端发送请求,要求客户端也必须交出证书。
客户端自证(Client Certificate)
客户端验证服务器的证书是否合法。确认无误后,客户端把自己的专属证书发送给服务器。
客户端额外校验(Certificate Verify)
为了证明证书归属无误,客户端会用私钥对前面所有的交流记录进行数字签名,一并发给服务器校验。
密钥交换(Key Exchange)
服务端验证客户端身份通过后,各自用对方的公钥和自己的临时私钥本地算出同一个会话密钥。
握手完成(Finished)
双方互发消息确认身份核验完毕。
接着我们提取三份证书:服务端、客户端、自签发 CA。在过滤器中输入 tls.handshake.type == 11 以筛选证书。其中 Frame6 是服务端传证书给客户端,截获的是服务端证书。Frame7 截获的是客户端证书和用以让服务端验证权威性的 CA 证书。
Frame6:
右键选择导出分组字节流(或快捷键 ctrl+shift+X),将导出的文件后缀命名为 .der 即可。用同样的方法导出客户端证书和 CA 证书。
Frame7:
最终得到:
本题最关键的信息在于客户端传入的证书信息与要求的不一致但仍可以通过握手。从 Frame6 的 Certificate Request 中可以明确看出服务端要求签发者必须为 ShangMiBei Root CA:
但是再看 Frame7,客户端证书是由 Test Root CA 签名的:
我们又抓到了 Test Root CA 自签名的根证书本身:
证书中只有公钥,没办法进行私钥签名。于是我们可以本地生成一份公私钥,把原 CA 和客户端证书的公钥篡改为自己的公钥,再用自己的私钥实现 CA 的自签名和客户端证书的重签名。最终提交客户端私钥文件,新的 Log Client,新的 Test Root CA。
EXP
找出证书中的公钥:
30 82 01 0a 是 ASN.1 SEQUENCE 头,代表 RSA 公钥结构。02 82 01 01 00 是字段头。
末尾的 02 03 01 00 01 是公钥指数 65537,需要剔除掉。
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 from Crypto.Util.number import * p = getPrime(1024 ) q = getPrime(1024 ) n = p * q phi = (p - 1 ) * (q - 1 ) e = 0x10001 d = pow (e, -1 , phi) new_n = f"{n:x} " print (p)print (q)print (new_n)with open ("CA_cert.der" , "rb" ) as f: orig_CA_hex = f.read().hex () orig_n = "c4 50 9c 65 52 84 3b 61 72 5c 3b 57 d0 63 ee f9 e4 6f 6a 5a 15 25 fb 21 2c 30 27 b6 e9 de 96 01 03 cb d6 96 cf b8 24 8c 88 fd 25 f0 43 69 8f 23 fa 4a e8 db 1e d7 52 b3 c3 f8 40 13 69 f9 de 9e a5 3e 74 c9 ff 82 99 b0 3d f7 bb 58 e3 7c 44 db bb 83 c1 67 cc 10 f1 7a 91 a7 dc 07 c0 08 76 0c 17 e9 74 7b df 4b 87 8c d4 a3 3b 94 65 9d eb 67 b5 55 c0 42 a1 6b 2a df 75 98 7c 9c d9 3b b2 bc 41 c9 f5 cf 8e 75 55 23 8c 95 81 5a 83 74 8a d9 eb eb 30 ec 2c 1e 98 6b 03 a3 e3 0b 10 5d cb 98 8d f5 c6 2a f3 3c ac cc d6 ae 85 5c 2b f8 95 3c f8 bf 52 6f 6b d4 06 30 5e 97 98 ac 5b 7d 14 3c ee a0 cc 1b 7d b1 53 32 2e 72 87 ab d2 e8 cd e5 0c cc 51 5e ee 7a af 3e 9c 9e 9b 3e a6 f5 20 07 77 a4 3d 82 d9 52 c4 be bc 10 96 fb 83 9e fe 36 a7 af 5a 5c 3f e2 e2 15 19 09 53 cf 87 86 0b 73" .replace( " " , "" ) new_CA_hex = orig_CA_hex.replace(orig_n, new_n)with open ("CA_cert.der" , "wb" ) as f: f.write(bytes .fromhex(new_CA_hex))""" 160871856688921207037609741428797793399360913231770735754794785730507606610102065797319011671856310018450905814932829450257510111413079407661803748197289136436964490482108168303630078322031294926629863910107101155782982354309913378873186827872372876212340353975852939737969957092740507268289127239905155936363 162057750553435929932225668099824602517741211662885314084566528678308228549967051063930190956236227902161275795101328234480836868994877370691320781698138873185389816559855557031894129644205722966993615209530748538041501438342655634173018831856611853949926400561237435981052372231047219355874623044017957953481 ce84b5106157c3bf7aebdedb4e0adb36cac9766de6cd9917df109b22c7d0d49ea6605503e3cfddcc4a4fbcf567df4494d9c09e41ca4ed0fa5b9b8b7d57b9b126139287fdcf8d2c19dba47e8264695e5817b63d9c8893138d6cb110815a3f3769cf9682b4f12ea18ea13df4d9d666ac8f42276c1916b29f986f8e1a2564ae349d6394af8b2baec6f382c33706b69ae91c68bf083888c00352ebc35de70c8e5bc6f23ccc652080ef50a1f55b488689e929b270f08641d33277881d7caedb5276987ac42ed4fef22707ed12debb5dfca36a6d77c922217b9c09450274ed700803ff8a6c785d5c6a4e0c858191d3ed9dd4984108e1bf7abe0fb480ab5b78b8d7f903 """
更改证书公钥之后将 .der 转为 .pem:
1 openssl x509 -inform der -in CA_cert.der -out CA_cert.pem
使用新密钥对进行签名篡改。之后对于 .der 证书,用新签名替换旧签名:
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 Crypto.PublicKey import RSAfrom Crypto.Signature import pkcs1_15from Crypto.Hash import SHA256from cryptography import x509 p = 160871856688921207037609741428797793399360913231770735754794785730507606610102065797319011671856310018450905814932829450257510111413079407661803748197289136436964490482108168303630078322031294926629863910107101155782982354309913378873186827872372876212340353975852939737969957092740507268289127239905155936363 q = 162057750553435929932225668099824602517741211662885314084566528678308228549967051063930190956236227902161275795101328234480836868994877370691320781698138873185389816559855557031894129644205722966993615209530748538041501438342655634173018831856611853949926400561237435981052372231047219355874623044017957953481 n = p * q e = 0x10001 phi = (p - 1 ) * (q - 1 ) d = pow (e, -1 , phi) privK = RSA.construct((n, e, d))with open ("CA_cert.pem" , "rb" ) as f: CA_pem = f.read() CA_pem = x509.load_pem_x509_certificate(CA_pem) tbs = CA_pem.tbs_certificate_bytes old_sig = CA_pem.signature.hex () new_sig = pkcs1_15.new(privK).sign(SHA256.new(tbs)).hex ()with open ("CA_cert.der" , "r+b" ) as f: CA_der = f.read() CA_hex = CA_der.hex () new_CA = CA_hex.replace(old_sig, new_sig) f.write(bytes .fromhex(new_CA))
完成 CA 的篡改,客户端证书的篡改也是简简单单。
flag4 - 登录日志管理服务器
源码
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 package shangmibeiimport ( "crypto/subtle" "encoding/hex" "strings" "unicode" "golang.org/x/crypto/chacha20poly1305" )const keyHex = "d0eb801593baacc3001025e6ce26e0cb855d26766970e82b0f9e3712ecd9ea15" const nonceHex = "bd17a47f94bf6ebd8ec876c7" func UserLogin (username string , password string ) map [string ]any { result := make (map [string ]any) hash := GetAuthHash(username) result["msg" ] = "验证不通过" result["hash" ] = hash result["is_match" ] = false if "LogAdmin" != username { result["msg" ] = "用户名不存在" return result } if len (password) < 8 || len (password) > 16 || strings.Contains(password, " " ) { result["msg" ] = "口令要求:[8-16]个可打印字符(不包含空格)" return result } for _, r := range password { if !unicode.IsPrint(r) { result["msg" ] = "口令要求:[8-16]个可打印字符(不包含空格)" return result } } key, _ := hex.DecodeString(keyHex) nonce, _ := hex.DecodeString(nonceHex) aead, err := chacha20poly1305.New(key) if err != nil { return result } plaintext := []byte ("" ) additionalData := []byte (password) tag := aead.Seal(nil , nonce, plaintext, additionalData) tagString := hex.EncodeToString(tag) var match = subtle.ConstantTimeCompare([]byte (tagString), []byte (hash)) == 1 if match { result["msg" ] = "验证通过" result["is_match" ] = true return result } return result }
分析
先介绍一下 Chacha20-Poly1305。这是一种 AEAD 算法,将流密码与消息认证码结合,以确保数据的机密性和完整性。该算法由两个独立但互补的算法组成:
Chacha20:流密码。输入 32 字节的密钥,16 字节的随机数+计数器,每次生成 64 字节的密钥流。在与 Poly1305 配合时,取第一个数据块(64 字节)的前 32 字节,将其中的前 16 字节经过位限处理形成
r
,后 16 字节直接作为
s
。
Poly1305:消息认证码,负责生成认证标签。算法把原始数据(AAD、Ciphertext、Length)按小端序排列后单独补零,最终组合成若干个 16 字节的数据块。接着在每个数据块后补 01(小端序下相当于加
2 128
,以防止生成空数据块),经过计算后产出 16 字节的认证标签(Tag)
本题中 AAD 是未知口令,密文为空,最终产出的 Hash 是 Tag,且 Nonce 和 Key 均已给出。求解口令等价于求 AAD,因此问题转化为在 Chacha20-Poly1305 中,已知
( k e y , n o n c e , t a g , c i p h e r t e x t = n u l l )
,求解
A A D
。
已知口令长度在 8 到 16 之间,只占用一个数据块;空密文不占用数据块;长度数据块的前 8 字节允许爆破,后 8 字节固定为 0。故最终数据块大概是:
1 AAD(小端) | 0x01 + AAD_length(小端) | 0x00(8) | 0x01
记数据块分别为
c 1 , c 2
,数据块总数
q = 2
,素模数
p = 2 130 − 5
,截断模数为
2 128
,密钥对为
( r , s )
,计算:
A ≡ ∑ i = 1 q c i ⋅ r q − i + 1 ( mod p )
在本题中:
A ≡ c 1 r 2 + c 2 r ( mod p )
最终 Tag:
T a g ≡ ( A + s ) ( mod 2 128 )
已知
( r , s )
,还原出来的
A b a s e
大概比原本的
A
丢失了两个比特的信息,爆破一下即可。
在方程
A ≡ c 1 r 2 + c 2 r ( mod p )
中,我们的目标是求解
c 1
(即包含 AAD 的数据块)。
c 2
是长度块,通过爆破也是已知的,那么
c 1 ≡ ( A − c 2 r ) ⋅ r − 2 ( mod p )
。求解后以小端序转字节,筛选就能得到口令。
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 from Crypto.Cipher.ChaCha20 import ChaCha20Cipherfrom Crypto.Util.number import *from struct import pack keyHex = "d0eb801593baacc3001025e6ce26e0cb855d26766970e82b0f9e3712ecd9ea15" nonceHex = "bd17a47f94bf6ebd8ec876c7" tagHex = "bc98a8cc57cad084607c89dffd7988eb" p = 2 **130 - 5 mod = 2 **128 res = [] first_block = ChaCha20Cipher(bytes .fromhex(keyHex), bytes .fromhex(nonceHex)).encrypt( b"\x00" * 32 ) r = int .from_bytes(first_block[:16 ], byteorder="little" ) s = int .from_bytes(first_block[16 :], byteorder="little" ) r &= 0x0FFFFFFC0FFFFFFC0FFFFFFC0FFFFFFF A_base = (int .from_bytes(bytes .fromhex(tagHex), "little" ) - s) % pfor i in range (2 ): for j in range (2 ): A = A_base + (1 << 128 ) * i + (1 << 129 ) * j for length in range (6 , 16 ): c2_bytes = pack("<QQ" , length, 0 ) c2_int = int .from_bytes(c2_bytes, byteorder="little" ) + (1 << 128 ) r_inv_2 = pow (r, -2 , p) c1_int = ((A - c2_int * r) * r_inv_2) % p res.append(bytearray (c1_int.to_bytes(17 , "little" ))) res = [bytes (x[:-1 ]) for x in res if x[-1 ] == 0x01 ] [print (x) for x in res]''' b"\xf7\xfd\xe7\xf6l\x9d\xd2;:\xe3\x88'r\xa5d\xd2" b'\x95\xae0\xa9a\xe0\x8b2\xd8\x0b{\xcb\xf4R2i' b'3_y[V#E)v4mow\x00\x00\x00' b'\xc2\x0eT\x07 _EM\xb5\xea\xf1\xd2Q\xb4\xe8W' b'1\x00S\xa7\x05\xa6\xf5\x8c\xd0\x9e\xd1\xa6\x82\x9e\xc8\xb7' b'\x14\x7f\x1dO\xb9!dd\x05A-g\xfa\xa1\x16\xc5' b'\xb2/f\x01\xaed\x1d[\xa3i\x1f\x0b}O\xe4[' b'A\xdf@\xadw\xa0\x1d\x7f\xe2\x1f\xa4nW\x03\xcd\xb3' b'\xdf\x8f\x89_l\xe3\xd6u\x80H\x96\x12\xda\xb0\x9aJ' '''
复现得到 3_y[V#E)v4mow\x00\x00\x00
flag6 - 获取删除日志权限
源码
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 package shangmibeiimport ( "config" "crypto/subtle" "encoding/hex" "log" "golang.org/x/crypto/chacha20poly1305" )func IsLogTokenMatch (hexToken string ) map [string ]any { result := make (map [string ]any) result["is_match" ] = false hexChallenge := GetLogChallengeHexFromSession() data, _ := hex.DecodeString(hexChallenge) var tmpToken = GenLogTokenHex(data) if hexToken == "" || len (tmpToken) != len (hexToken) { result["msg" ] = "验证不通过" return result } var match = subtle.ConstantTimeCompare([]byte (tmpToken), []byte (hexToken)) == 1 if match { log.Printf("身份验证成功 %v\n" , match) result["is_match" ] = true result["msg" ] = "验证通过" } else { result["msg" ] = "验证不通过" } return result }func GenLogTokenHex (challengeByte []byte ) string { key, _ := hex.DecodeString(config.LogKeyHex) nonce, _ := hex.DecodeString(config.LogNonceHex) aead, err := chacha20poly1305.New(key) if err != nil { return "" } plaintext := []byte ("" ) additionalData := challengeByte tag := aead.Seal(nil , nonce, plaintext, additionalData) hexTag := hex.EncodeToString(tag) return hexTag }func GenerateLogChallengeHex () string { tmpChallenge, _ := generateSecureAlphanumeric(48 ) hexChallenge := hex.EncodeToString([]byte (tmpChallenge)) return hexChallenge }
分析
依旧 Chacha20-Poly1305,这题考察的是 nonce 复用求解
( r , s )
。分组数
q = 4
,有方程组:
A 1 ≡ c 11 ⋅ r 4 + c 12 ⋅ r 3 + c 13 ⋅ r 2 + c 14 ⋅ r + s ( mod p ) T a g 1 ≡ A 1 ( mod 2 128 ) A 2 ≡ c 21 ⋅ r 4 + c 22 ⋅ r 3 + c 23 ⋅ r 2 + c 24 ⋅ r + s ( mod p ) T a g 2 ≡ A 2 ( mod 2 128 ) c 14 = c 24
两式相减:
T a g 2 − T a g 1 ≡ ( c 21 − c 11 ) ⋅ r 4 + ( c 22 − c 12 ) ⋅ r 3 + ( c 23 − c 13 ) ⋅ r 2 ( mod 2 128 )