文章

2026上海市网络安全初赛wp

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组操作,每组针对输入的每一个字符。操作如下:

  1. 把字符装进一个8位寄存器
  2. 异或一个常数
  3. 加一个常数
  4. 循环左移若干位
  5. 和目标值比较,不相等立马退出

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
0x21flag 为 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}
本文由作者按照 CC BY 4.0 进行授权