2025年“羊城杯”网络安全大赛

2025年“羊城杯”网络安全大赛

Aristore

Misc

成功男人背后的女人

Challenge

每个成功的男人背后都站着一个伟大的女人。

Solution

用 TweakPNG 打开图片检查发现 mkBT 块,这是 Adobe 专属的数据块,要使用 fireworks 8 打开

ycb2025-1

隐藏掉上面的图层,只显示下面的图层

ycb2025-2

图片下方的符号,将 映射为 1 映射为 0 得到:

1
010001000100000101010011010000110101010001000110011110110111011100110000011011010100010101001110010111110110001001100101011010000011000101101110010001000101111101001101010001010110111001111101

二进制转字符得到 flag

FLAG

1
DASCTF{w0mEN_beh1nD_MEn}

polar

Challenge

给你8个擦除率为0.5的比特位传输4比特信息,你能使每个比特的成功传输概率超过50%吗?

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
# default_transmission.py
import numpy as np, random

class DefaultTransmission:
def __init__(self):
self.n_channels = 8
self.original_erasure_probability = 0.5

def simulate_bit_recovery(self, n_bits=4, trials=100):
"""
模拟 8 信道传输 n_bits 比特,每比特重复 trials 次
返回每个比特的成功恢复率
"""
success = np.zeros((self.n_channels, n_bits))

for trial in range(trials):
# 随机生成 4 比特信号
bits = np.random.randint(0, 2, size=n_bits)

for ch in range(self.n_channels):
for i, b in enumerate(bits):
erased = (np.random.rand() < self.original_erasure_probability)
received = None if erased else b
if received == b:
success[ch, i] += 1

avg_success = success / trials
print("\n每信道比特成功率矩阵 (行=信道, 列=比特索引):")
for ch in range(self.n_channels):
rates = " ".join(f"{avg_success[ch,i]:.3f}" for i in range(n_bits))
print(f"信道 {ch+1}: {rates}")

bit_avg = np.mean(avg_success, axis=0)
print("\n每比特平均恢复率:")
for i, r in enumerate(bit_avg):
print(f"Bit {i}: {r:.3f}")

return avg_success
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
# polar_transmission.py
import numpy as np

# --- BEC 信道 ---
def transmit_BEC(x, eps):
y = np.array(x, dtype=object)
erasures = np.random.rand(len(x)) < eps
y[erasures] = None
return y

# --- 极化挑战 ---
def polar_bit_challenge(N=8, K=4, eps=0.5, trials=100, polar_funcs=None):
"""
polar_funcs: dict, 必须包含
'construction', 'encode', 'decode'
"""
if polar_funcs is None:
raise ValueError("必须传入 polar_funcs 字典")

construction = polar_funcs['construction']
encode = polar_funcs['encode']
decode = polar_funcs['decode']

info_idx, frozen_idx, _ = construction(N, K, eps)
per_bit_success = np.zeros(K, dtype=int)

for _ in range(trials):
u = np.zeros(N, dtype=int)
info_bits = np.random.randint(0, 2, K)
u[info_idx] = info_bits

x = encode(u, N)
y = transmit_BEC(x, eps)
u_hat = decode(y, frozen_idx)
recovered = u_hat[info_idx]
per_bit_success += (recovered == info_bits).astype(int)

per_bit_rate = per_bit_success / trials

print("\n=== 每个信息位恢复成功率 ===")
for i, idx in enumerate(info_idx):
print(f"信息位 {idx:2d} : 恢复率 = {per_bit_rate[i]:.3f}")

# 检查挑战条件
all_above_eps = np.all(per_bit_rate > eps)
two_above_07 = np.sum(per_bit_rate > 0.7) >= 2
challenge_pass = all_above_eps and two_above_07

print("\n=== 挑战条件 ===")
print(f"所有比特恢复率 > {eps:.2f} ? {'✅' if all_above_eps else '❌'}")
print(f"至少两个比特恢复率 > 0.7 ? {'✅' if two_above_07 else '❌'}")
print(f"挑战 {'成功' if challenge_pass else '失败'}")

return per_bit_rate, challenge_pass

Solution

任务: 在 8 条二进制擦除信道(BEC,擦除概率 $$\varepsilon=0.5$$)上传输 4 个信息比特(即 ($$N=8, K=4, \varepsilon=0.5$$)),实现一个 polar 库(包含 constructionencodedecode 三个函数),使得每个信息位的恢复成功率都大于 0.5,并且至少两个信息位的恢复率达到或超过 0.7。

约束: 只能替换上传的 polar 库代码;沙箱环境禁止使用某些内建(例如 setobject 等),代码命名要求(先前测试中)避免下划线等。通道模型为独立擦除:每个物理位以概率 0.5 被擦除(变成 None)。

关键观察与解法思路:

  • 在 BEC 上,若把同一信息比特独立重复发送 (r) 次,至少有一次未被擦除的概率为
    $$
    1 - \varepsilon^{r}.
    $$
    代入 ($$\varepsilon=0.5$$),当 ($$r=2$$) 时成功概率为
    $$
    1 - 0.5^{2} = 0.75.
    $$
    这个值既大于 0.5,也大于 0.7,所以满足题目要求。

  • 给定 ($$N=8, K=4$$),恰好可以把 4 个信息位各重复 2 次(占满 8 个物理位)。这是一种简单、鲁棒且在本问题规模上最优的策略。

结论: 使用“每信息位重复两次”的编码与对应的解码(只要任一副本到达即恢复该比特)在理论上能通过挑战。

上传的三函数实现片段:

  • 不依赖被禁用的内建(避免使用 setobject 等)。
  • 返回值与服务器接口兼容:construction(N,K,eps) 返回 infoIdx, frozenIdx, reliencode(u,N) 返回长度为 N 的数组;decode(y,frozenidx) 返回长度为 N 的估计向量(整数 0/1)。
  • 对于擦除(None)情形,decode 选择第一个非 None 的副本;若两副本均被擦除,默认判为 0(概率为 ($$\varepsilon^{2}$$))。

下面给出最终可上传的三函数实现片段(可直接放入 polar.py 并保证 import numpy as np 存在):

先写一个脚本用于发送/接收并处理数据:

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
# exp.py
from pwn import *

context.log_level = 'WARN'
HOST = "45.40.247.139"
PORT = 31149
SOLUTION_FILE = "solution.py"
io = remote(HOST, PORT)

# 读取代码
with open(SOLUTION_FILE, "r", encoding="utf-8") as f:
solution_code = f.read()

# 1. 接收欢迎信息和菜单
io.recvuntil(b'> ')

# 2. 发送选项 '2'
io.sendline(b'2')

# 3. 接收上传提示
prompt_str = '结束输入:'
io.recvuntil(prompt_str.encode('utf-8'))

# 4. 发送解决方案代码
io.send(solution_code.encode('utf-8'))

# 5. 发送结束标志 'END'
io.sendline(b'\nEND')

# 6. 接收并打印所有剩余的输出
response = io.recvall(timeout=10).decode('utf-8', errors='ignore')
print("\n--- 服务器最终响应 ---")
print(response)
print("--------------------")

io.close()

编写解题脚本:

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
# solution.py
def construction(N, K, eps):
# 将信息位安排在前 K 个位置(本题 N=8,K=4 时适配)
klocal = N // 2
infoIdx = np.arange(klocal)
frozenIdx = np.arange(klocal, N)
reli = np.zeros(N, dtype=float)
return infoIdx, frozenIdx, reli

def encode(u, N):
# 简单重复编码:将前 klocal 位各复制到两个位置
klocal = N // 2
x = np.zeros(N, dtype=int)
for i in range(klocal):
val = int(u[i])
# 放在第 i 和 i+klocal
x[i] = val
x[i + klocal] = val
return x

def decode(y, frozenidx):
# 对于每个信息位,任选第一个非擦除副本作为恢复值
N = len(y)
klocal = N // 2
uhat = np.zeros(N, dtype=int)
for i in range(klocal):
v = y[i]
if v is None:
v2 = y[i + klocal]
if v2 is None:
recovered = 0
else:
recovered = int(v2)
else:
recovered = int(v)
uhat[i] = recovered
for j in range(klocal, N):
uhat[j] = 0
return uhat

数学可信度:

  • 每个信息位重复两次、两次擦除独立的情况下成功率为
    $$
    1 - \varepsilon^{2}.
    $$
    代入 ($$\varepsilon=0.5$$) 得到 ($$1 - 0.25 = 0.75$$)。因此:
    • 每个比特恢复率 ($$=0.75>0.5$$),满足“全部>0.5”;
    • 所有信息位都达到了 0.75,自然至少两个比特 ≥ 0.7。
  • 该策略利用了简单而强力的独立重复冗余,适用于擦除通道且在资源($$N=8$$)刚好整除的情况下最直接最优。

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
--- 服务器最终响应 ---

✅ 用户 polar 库已安全加载

=== 每个信息位恢复成功率 ===
信息位 0 : 恢复率 = 0.880
信息位 1 : 恢复率 = 0.910
信息位 2 : 恢复率 = 0.910
信息位 3 : 恢复率 = 0.910

=== 挑战条件 ===
所有比特恢复率 > 0.50 ? ✅
至少两个比特恢复率 > 0.7 ? ✅
挑战 成功

挑战成功,FLAG: DASCTF{94485289736729714789730949105504}

欢迎参加挑战!
请选择操作:
1. 默认传输
2. 上传自定义 polar 库
3. 退出
>
--------------------

FLAG

1
DASCTF{94485289736729714789730949105504}

DS&Ai

Mini-modelscope

Challenge

This is Mini-modelscope, perhaps it has some issues. Note: signature is “serve”.

Solution

湾区杯原题,exp出自这篇wp:https://mp.weixin.qq.com/s/5HbnVnNCj0c2AsjDTT8oSg

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
# build_model_tfio.py
# 使用纯 TensorFlow op 在 Graph 中读取 /flag 并作为 signature 返回
# 运行环境需要安装 tensorflow (建议 tensorflow-cpu)
#
# 生成: model.zip

import os
import zipfile

try:
import tensorflow as tf
except Exception as e:
raise SystemExit("请先安装 TensorFlow: pip install tensorflow-cpu\n错误: " + str(e))

OUT_DIR = "model_saved"
ZIP_PATH = "model.zip"

# 清理
if os.path.exists(OUT_DIR):
import shutil
shutil.rmtree(OUT_DIR)
if os.path.exists(ZIP_PATH):
os.remove(ZIP_PATH)

# 纯 TF 的 serve 函数:在 Graph 中读取 /flag,确保返回 tf.Tensor (dtype=tf.string)
@tf.function(input_signature=[tf.TensorSpec(shape=[None, 1], dtype=tf.float32)])
def serve_fn(x):
# tf.io.read_file 是一个图操作,返回 tf.Tensor(dtype=tf.string, shape=())
data = tf.io.read_file("/flag")

# 为兼容一些加载器/调用方,明确设置形状(标量),或者扩展成 [batch] 形式:
# 1) 若调用端期待标量 string:直接返回 data
# 2) 若调用端以 batch 形式调用(输入是 [N,1]),可以把 data 扩成 [N]
# 下面示例把 data 重复为与输入 batch size 相同的向量
batch_size = tf.shape(x)[0]
data_vec = tf.repeat(tf.expand_dims(data, 0), repeats=batch_size) # shape [batch_size]
# 返回 dict,prediction 保持为 shape [batch_size] 的 tf.string 张量
return {"prediction": data_vec}

# 备用的纯 TF signature(不读取文件),便于测试加载器是否能读取 SavedModel
@tf.function(input_signature=[tf.TensorSpec(shape=[None, 1], dtype=tf.float32)])
def noop_fn(x):
batch_size = tf.shape(x)[0]
const = tf.constant("MODEL_OK", dtype=tf.string)
vec = tf.repeat(tf.expand_dims(const, 0), repeats=batch_size)
return {"prediction": vec}

# 保存 Module,并显式把 "serve" signature 写入
class ModelModule(tf.Module):
@tf.function(input_signature=[tf.TensorSpec(shape=[None, 1], dtype=tf.float32)])
def __call__(self, x):
return serve_fn(x)

module = ModelModule()
tf.saved_model.save(module, OUT_DIR, signatures={"serve": serve_fn, "noop": noop_fn})

# 打包为 zip
with zipfile.ZipFile(ZIP_PATH, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(OUT_DIR):
for fname in files:
full = os.path.join(root, fname)
arcname = os.path.relpath(full, OUT_DIR)
zf.write(full, arcname)

print("SavedModel saved to:", OUT_DIR)
print("Zipped to:", ZIP_PATH)

把生成的文件夹压缩后上传即可

ycb2025-3

FLAG

1
DASCTF{36064477511992355432059500484677}
  • 标题: 2025年“羊城杯”网络安全大赛
  • 作者: Aristore
  • 链接: https://www.aristore.top/posts/ycb2025/
  • 版权声明: 版权所有 © Aristore,禁止转载。
评论