2026上海市网络安全初赛wp
bad_share
shamir秘密共享,给出了9个份额,其中有两份是错的,门限是5。可以枚举 $C_{9}^{5}$ 种情况恢复秘密,计算恢复成功的概率:
\[P=1- \frac{2\cdot C_{7}^{4}+C_{7}^{3}}{C_{9}^{5}}=\frac{21}{126}\]所以会有21种情况恢复成功,将恢复成功的秘密解码即可
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
from itertools import combinations
p=6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151
shares=[(4,
1756507252888966302421944697206841956143152140868019425423436826040417874090358810282666628353861959194519032644153396178974087251656200520090451446037558616),(5,
291079203062482778164854164602169614214941906034816769921809749669038694270141709535829730588575563957368793945410386647770027583595565500385787765396679943),(6,
3353657858949220467189251132108794571872371959480001695994095692465794840483860565226874775451906223213941455607156170032589978963502693390308711679147462790),(3,
3036190474984169590950378504231555599054858759783145166750792797321971974661532479326963124473761720145245595100028856413919438990575098181766312281016449474),(2,
6194143121566534075891262449799030938599380831596361229544817695037620824633497487483944493077767373997778397971319267160293299864167417885508691584631990890),(1,
3190833648678294242003969261324345442368829714246140188627248466512581507880823091610739631577934516878443539932340272490275059447848911907619478425588241928),(7,
4068614300385897042736163548231103545434345847425657360978335306252159948463803163273112788447249335566556829589208573516328678189557514426677386978479001619),(8,
2524499628636075408730372448736615895921954107928295154192144197934845731982899262196954960370286591718071680967232201013594469797728189493226395106754824275),(9,
356348046654628493758686606367279817347683022998832017496460398267415288329999382120255439055531110457018951780523102517824504770874348953756247733306681596)]
def recover_secret(points, p) :
secret = 0
for i, (xi, yi) in enumerate(points):
numerator = denominator = 1
for j, (xj, _) in enumerate(points):
if i != j:
numerator = numerator * (-xj) % p
denominator = denominator * (xi - xj) % p
secret = (secret + yi * numerator * pow(denominator, -1, p)) % p
return secret
results=[]
for t in combinations(shares,5):
s=recover_secret(t,p)
results.append(s)
from collections import Counter
number = Counter(results)
result = number.most_common(1) # 获取出现次数最多的元素及其次数
print('出现最多的元素为:{},出现的次数为{}'.format(result[0][0], result[0][1])) # 输出结果[^1]
secret=854589733786575566913104637217829813415694121289115373995417199976902059405420769977951485783165
flag=secret.to_bytes((secret.bit_length()+7)//8,'big')
print(flag.decode())
flag:
1
flag{two_liars_cannot_hide_a_polynomial}
Baby_randomized_VM
是一个虚拟机程序,程序会读取输入的42个字符,然后做42组操作,每组针对输入的每一个字符。操作如下:
- 把字符装进一个8位寄存器
- 异或一个常数
- 加一个常数
- 循环左移若干位
- 和目标值比较,不相等立马退出
42组全部通过后输出correct
反编译得到的main函数主要分为两段,前半段是读取输入和解密字节码,后半段是虚拟机的解释器。从解释器中我们可以知道乱序的操作码分别对应什么。
字节码解密伪代码:
1
2
3
4
5
6
for ( i = 0; i != 463; ++i )
{
*v4 = byte_402040[i] ^ (v5 + 11 * (i >> 1));
v5 += 37;
++v4;
}
解释器伪代码:
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
for ( j = 0; ; j += 2 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
v10 = j + 1;
v11 = v13[j];
if ( v11 != 69 )
break;
j += 2;
v7 = v13[v10] == v8;
}
if ( (unsigned __int8)v11 > 0x45u )
break;
switch ( v11 )
{
case 33:
if ( !v7 )
goto LABEL_23;
++j;
break;
case 38:
j += 2;
v8 ^= v13[v10];
break;
case 30:
j += 2;
v8 += v13[v10];
break;
default:
goto LABEL_23;
}
}
if ( v11 != 79 )
break;
j += 2;
v8 = __ROL1__(v8, v13[v10]);
}
if ( v11 == 108 )
break;
if ( v11 != 75 || (v12 = v13[v10]) > 0x29u )
{
LABEL_23:
puts(s: "Wrong!");
return 1;
}
v8 = s[v12];
}
可以发现,加密数据在文件的初始地址为:
1
byte_402040[i] -> 0x402040
读解释器反编译结果,我们可知操作码如下:
| 0x1E | += |
|---|---|
| 0x26 | ^= |
| 0x4F | 循环左移 |
| 0x4B | 取输入数组的操作数下标的值 |
| 0x45 | 判断A是否等于操作数,将值存入flag |
| 0x21 | flag 为 1时继续进行 |
| 0x6C | 打印correct |
解题步骤
主要就是在检查操作中进行的运算都是可逆的,我们可以通过解密程序的字节码来发现这一点:
1
2
3
4
5
6
for i in range(463):
k= (key+11*(i >> 1)) & 0xFF
result.append(enc[i]^k)
key+=37
print([hex(b) for b in result[:16]])
输出:
1
['0x4b', '0x16', '0x26', '0xd8', '0x1e', '0xde', '0x4f', '0x2', '0x45', '0x6e', '0x21', '0x4b', '0x3', '0x26', '0xa4', '0x1e']
读取,异或,加法,循环左移,比较,…这些操作全都可逆,而且检查的标准值我们也全都知道了,所以就可以倒推回去
我们先获取全部42组操作数:
1
2
3
4
5
6
7
8
9
10
for pc in range(0,462,11):
i=result[pc+1]
c1=result[pc+3]
c2=result[pc+5]
r=result[pc+7]
t=result[pc+9]
nums.append((i,c1,c2,r,t))
print(len(nums))
print(nums[:3])
输出:
1
2
42
[(22, 216, 222, 2, 110), (3, 164, 37, 4, 142), (9, 236, 151, 3, 33)]
检查逻辑可以概括如下:
\[rol_{8}(((x\oplus c_{1})+c_{2}) \ \ mod \ \ 256,r)=t\]逆过程为:
\[x=((ror_{8}(t,r)-c_{2})\ \ mod \ \ 256)\]这些检查之间互不影响,所以每一组都可以反推出一个字符,最后拼在一起就是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
40
41
42
43
44
data=open("baby_randomized_vm_001","rb").read()
enc= data[0x2040:0x2040+463]
print([hex(b) for b in enc[:8]])
result=[]
key=0xCF
for i in range(463):
k= (key+11*(i >> 1)) & 0xFF
result.append(enc[i]^k)
key+=37
print([hex(b) for b in result[:16]])
print(hex(result[462]))
nums=[]
for pc in range(0,462,11):
i=result[pc+1]
c1=result[pc+3]
c2=result[pc+5]
r=result[pc+7]
t=result[pc+9]
nums.append((i,c1,c2,r,t))
print(len(nums))
print(nums[:3])
def f1(v,r):
r&=7
return ((v<<r)|(v>>(8-r))) & 0xFF if r else v
def f2(v,r):
r&=7
return ((v>>r)|(v<<(8-r))) & 0xFF if r else v
flag=[None]*42
for i,c1,c2,r,t in nums:
ch=((f2(t,r)-c2) & 0xFF)^c1
assert f1(((ch^c1)+c2)& 0xFF,r)==t
flag[i]=ch
answer= ''.join(chr(c)for c in flag)
print(answer)
flag:
1
flag{8775a086-19c3-423e-b3f8-20a6045b62de}
time_stream
用时间作为seed生成伪随机密钥,然后加密flag。但是给出了时间的一个范围,只要枚举每一个时间作为seed并尝试恢复flag,因为一共只有20000多个候选
生成密钥和解密算法全部用题目给的函数就可以了,最后判断解密出的字节是否在ascii字符集的可打印范围内,如果全在的话就是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
time1=1786147200
time2=1786168800
ciphertext=bytes.fromhex("c918c61e8d4a0c1212e7c9ef63a9b7f8a27fe6c839590c2b0627ea966ff4ad5fbac5")
WINDOW_SECONDS = 6 * 60 * 60
def xorshift32(x: int) -> int:
x ^= (x << 13) & 0xFFFFFFFF
x ^= x >> 17
x ^= (x << 5) & 0xFFFFFFFF
return x & 0xFFFFFFFF
def stream(seed: int, length: int) -> bytes:
state = seed & 0xFFFFFFFF
output = bytearray()
while len(output) < length:
state = xorshift32(state)
output.extend(state.to_bytes(4, "little"))
return bytes(output[:length])
def encrypt(message: bytes, seed: int) -> bytes:
keystream = stream(seed, len(message))
return bytes(left ^ right for left, right in zip(message, keystream))
for seed in range(time1,time2+1):
plaintext=encrypt(ciphertext,seed)
try:
text= plaintext.decode("ascii")
except UnicodeDecodeError:
continue
if all(32 <= ord(ch)<127 for ch in text):
print(text)
break
flag:
1
flag{seconds_are_not_secret_seeds}
echo_lwe
一个风味lwe加密系统,主要问题在于derive_seed中用的是lable 的前6个字节,而下面生成密钥算法中:
1
2
3
4
5
6
7
8
9
10
11
12
13
secret = sample_small(master, b"secret-key")
raw_error = sample_small(master, b"secret-noise")
error = negacyclic_rotate(raw_error, rotation)
while True:
public_a = [secrets.randbelow(Q) for _ in range(N)]
vulnerable_factor = public_a.copy()
vulnerable_factor[rotation] = (vulnerable_factor[rotation] + 1) % Q
if is_unit(vulnerable_factor):
break
public_b = add(negacyclic_mul(public_a, secret), error)
return public_a, public_b, rotation
所以 $e_{raw}=s$,$e=x^r\cdot s$ ,下面的循环是确保 $a+x^r$ 可逆,然后另一半公钥$b=a\cdot s+e=(a+x^r)\cdot s \ \ mod \ \ q$
由于可逆,我们可以直接获得私钥$s=b\cdot (a+x^r)^{-1} mod\ \ q$
然后再按lwe加密标准流程解密即可
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
import json
from pathlib import Path
q,N=12289,256
R=PolynomialRing(GF(q),'x')
x=R.gen()
mod=x^N+1
data= json.loads(Path("output.json").read_text())
a,b,u,v,r=(data[k] for k in ("a","b","u","v","rotation"))
f=R(a)+x^r
s=(f.inverse_mod(mod)*R(b))%mod
d=(R(v)-R(u)*s)%mod
list1=d.list()+[0]*(N-len(d.list()))
bits=[1 if min(int(c),q-int(c))>q//4 else 0 for c in list1]
bit_string="".join(str(bit) for bit in bits)
number=int(bit_string,2)
flag=number.to_bytes(N//8,"big")
flag1=flag.rstrip(b"\x00").decode()
print(flag1)
flag:
1
flag{8775a086-19c3-423e-b3f8-20a6045b62de}
nonce_again
AES-GCM重复使用了密钥和nonce,并给出了两组对应明密文和tag,需要根据目标明文,伪造密文和对应的tag
伪造密文是很容易的,因为key和nonce重用,只要将一组明密文xor即可获得GCM的密钥流,然后再用密钥流加密明文就行
难点在于tag,题目给出了GCM的ghash生成方式,通过查询资料可知GCM验证tag的生成方式为:
\[tag=ghash\oplus mask\]ghash:
1
2
3
4
5
6
7
8
9
10
def ghash(h: int, aad: bytes, ciphertext: bytes) -> int:
def blocks(data):
padded = data + b"\0" * ((-len(data)) % 16)
return [int.from_bytes(padded[i:i + 16], "big") for i in range(0, len(padded), 16)]
value = 0
for block in blocks(aad) + blocks(ciphertext):
value = gf_mul(value ^ block, h)
lengths = ((len(aad) * 8) << 64) | (len(ciphertext) * 8)
return gf_mul(value ^ lengths, h)
这里的生成算法实际上是在有限域 $GF(2^{128})$ 上进行的,大概翻译过来就是:
\[gash=A_{1}H^3\oplus CH^2\oplus LH\]$A_{1}$ 是aad块,C是密文块,L是一个根据长度获得的块
将两个已知tag做xor,可以解出H:
\[tag_{1}\oplus tag_{2}=(C_{1}\oplus C_{2})\cdot H^2\] \[H^2=tag_{1}\oplus tag_{2}\cdot (C_{1}\oplus C_{2})^{-1}\]这是因为两次的aad和L都是相同的,所以能抵消掉
注意这里的求逆算法和开方都要在有限域上实现。获得H后,就可以计算target_ct对应的ghash,然后用一个已知tag计算固定的mask,就可以进一步计算目标tag了
最后用:
1
2
def submission_flag(nonce: bytes, ciphertext: bytes, tag: bytes) -> str:
return "flag{" + hashlib.sha256(nonce + ciphertext + tag).hexdigest()[:32] + "}"
拼接获得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
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
import hashlib
nonce=bytes.fromhex("d243b6fb856593f87666e7a9")
aad=bytes.fromhex("74656c656d657472792d7631")
plain_text1=bytes.fromhex("726f6c653d757365723b7569643d3031")
cipher_text1=bytes.fromhex("e5237f27a70bfbd39e6d9e543c4beb54")
tag1=bytes.fromhex("29fd630ecfc03f0b4b4f9d095f857301")
plain_text2=bytes.fromhex("726f6c653d757365723b7569643d3032")
cipher_text2=bytes.fromhex("e5237f27a70bfbd39e6d9e543c4beb57")
tag2=bytes.fromhex("31bc44fb85f47d773d15a2622b574e4f")
tg_plain_text=bytes.fromhex("61646d696e3d747275653b7569643d30")
keystream=bytes(a^b for a,b in zip(plain_text1,cipher_text1))
print(keystream.hex())
tg_cipher_text=bytes(a^b for a,b in zip(tg_plain_text,keystream))
R = 0xE1000000000000000000000000000000
IDENTITY = 1 << 127
def gf_mul(x: int, y: int) -> int:
z = 0
v = y
for bit in range(127, -1, -1):
if (x >> bit) & 1:
z ^= v
v = (v >> 1) ^ (R if v & 1 else 0)
return z
def ghash(h: int, aad: bytes, ciphertext: bytes) -> int:
def blocks(data):
padded = data + b"\0" * ((-len(data)) % 16)
return [int.from_bytes(padded[i:i + 16], "big") for i in range(0, len(padded), 16)]
value = 0
for block in blocks(aad) + blocks(ciphertext):
value = gf_mul(value ^ block, h)
lengths = ((len(aad) * 8) << 64) | (len(ciphertext) * 8)
return gf_mul(value ^ lengths, h)
def gf_inv(x):
k=(1<<128)-2
result=1<<127
base=x
while k:
if k & 1:
result=gf_mul(result,base)
base = gf_mul(base,base)
k>>=1
return result
def gf_sqrt(x):
y=x
for _ in range(127):
y=gf_mul(y,y)
return y
xor1=int.from_bytes(cipher_text1,"big")^int.from_bytes(cipher_text2,"big")
xor2=int.from_bytes(tag1,"big")^int.from_bytes(tag2,"big")
h2=gf_mul(xor2,gf_inv(xor1))
h=gf_sqrt(h2)
print(hex(h))
ghash1=ghash(h,aad,cipher_text1)
ghash2=ghash(h,aad,cipher_text2)
mask1=int.from_bytes(tag1,"big")^ghash1
mask2=int.from_bytes(tag2,"big")^ghash2
print(mask1)
print(mask2)
tag=(ghash(h,aad,tg_cipher_text)^mask1).to_bytes(16,"big")
print(tag.hex())
def submission_flag(nonce: bytes, ciphertext: bytes, tag: bytes) -> str:
return "flag{" + hashlib.sha256(nonce + ciphertext + tag).hexdigest()[:32] + "}"
flag = submission_flag(nonce,tg_cipher_text,tag)
print(flag)
flag:
1
flag{f7b2a3eb374eaeebfe8a46aaa8c48a38}