找回密码
 立即注册
首页 业界区 安全 记2025长城杯线上赛部分题目

记2025长城杯线上赛部分题目

里豳朝 2026-1-21 13:55:06
0.前言

小比赛随便打,国赛教我做人....
1.AI安全

1.1The Silent Heist
  1. 题目内容:目标银行部署了一套基于 Isolation Forest (孤立森林) 的反欺诈系统。该系统不依赖传统的黑名单,而是通过机器学习严密监控交易的 20 个统计学维度。系统学习了正常用户的行为模式(包括资金流向、设备指纹的协方差关系等),一旦发现提交的数据分布偏离了“正常模型”,就会立即触发警报。我们成功截取了一份包含 1000 条正常交易记录的流量日志 (public_ledger.csv)。请你利用统计学方法分析这份数据,逆向推导其多维特征分布规律,并伪造一批新的交易记录
复制代码
那基本上就能看出本题模拟了一个典型的对抗性机器学习场景。目标是骗过一个已经上线的异常检测系统
目标系统是基于孤立森林的实时风控引擎,输入数据是20 维浮点数特征
金额目标:

孤立森林不同于传统的分类算法(如 SVM 或神经网络),它属于无监督学习
核心逻辑就是算法随机选择特征并随机选择切分点,构建二叉树
且异常点往往具有“少”且“异”的特点,在空间中,它们远离高密度区域
路径长度异常点和正常点也是不一样的,

  • 异常点:只需要很少次数的随机切分就能被孤立出来,也就是处于树的浅层,路径短
  • 正常点:位于数据簇的中心,需要密集的切分才能被隔绝,也就是说处于树的深层,路径长
  • 判定公式:模型通过样本在多棵树中的平均路径长度计算异常评分。路径越长,评分越低,数据越正常
既然孤立森林难以孤立处于数据中心的点,那么我们的策略就是:制造大量极其平庸的数据
比如说,我们通过对截获的 1000 条日志进行统计:
计算每一列的平均值,计算每一列的标准差
只要生成的数据点无限趋近于各维度的均值 ,它们就会落在孤立森林最难切分的深处,逃过检测
由题目给的附件已知 feat_0的均值 μ0≈353
那么计算所需条数:2,000,000/353≈5,6662,000,000/353≈5,666条
也就是说,我们只要生成 6,000 条数据。这不仅能稳过 2M 金额线,还能通过大量的微小数据分摊风险,避免单笔大额交易触发阈值告警
但是如果 6000 条数据完全一样,会被去重算法拦截
所以应该在均值 μ的基础上,注入一个尺度极小的高斯白噪声
公式

这里 ϵ(扰动系数)设为 0.01左右,这保证了:

  • 每行数据在二进制层面都是唯一的
  • 在统计学层面,数据分布依然极度向中心靠拢
所以exp.py
  1. import socketimport numpy as npimport pandas as pdimport io​# 1. 题目提供的部分日志数据(基于你提供的片段进行统计建模)# 在实际环境中,如果能下载完整csv,分析结果会更精确。def generate_payload():    # 统计特征 (均值 mu 和 标准差 sigma)    # 基于样本计算的近似值    means = np.array([        353.45, 27.56, 93.67, 82.78, 45.12, 4.23, 13.45, 51.67, 11.23, 30.56,         39.12, 84.78, 10.34, 82.12, 73.67, 18.89, 30.56, 41.89, 13.12, 27.56    ])    stds = np.array([        25.0, 2.5, 3.0, 3.0, 2.0, 2.5, 2.5, 2.0, 2.5, 3.0,        3.0, 3.0, 2.5, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 2.5    ])​    # 设定生成 6000 条记录以确保总金额 > 2,000,000    num_samples = 6000        print(f"
  2. [*] 正在生成 {num_samples} 条伪造交易记录...")        # 生成数据:均值 + 极小的随机扰动 (0.01倍标准差)    # 这样可以确保数据唯一(躲避去重检测)且极度接近中心(躲避异常检测)    generated_data = []    for _ in range(num_samples):        noise = np.random.normal(0, 0.01, size=20) * stds        row = means + noise        generated_data.append(row)        # 转换为 CSV 格式    df = pd.DataFrame(generated_data)    df.columns = [f'feat_{i}' for i in range(20)]        csv_buffer = io.StringIO()    df.to_csv(csv_buffer, index=False, float_format='%.6f')        payload = csv_buffer.getvalue()    return payload​def pwn_bank():    host = '182.92.11.65'    port = 30799        payload = generate_payload()        try:        # 2. 建立连接        print(f"
  3. [*] 正在连接到 {host}:{port}...")        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)        s.connect((host, port))                # 接收服务器欢迎语        # s.recv(1024)                 # 3. 发送数据        print("
  4. [*] 正在传输数据流并注入金额...")        s.sendall(payload.encode())                # 4. 发送结束标志        s.sendall(b"EOF\n")                # 5. 接收返回结果(Flag通常在这里)        print("
  5. [*] 等待银行系统响应...")        response = b""        while True:            data = s.recv(4096)            if not data:                break            response += data            # 如果收到 flag 格式,提前停止打印(假设格式为 flag{...})            if b"flag" in response.lower():                break                        print("\n[+] 服务器响应结果:")        print(response.decode(errors='ignore'))                s.close()    except Exception as e:        print(f"[-] 错误: {e}")​if __name__ == "__main__":    pwn_bank()
复制代码
[img=720,163.29896907216494]https://www.yijinglab.com/guide-img/d9634e2f-3b66-42e7-8279-c0877cdd70e5/e88eeed0-1aa5-473e-9e1b-932ad485dbc0.png[/img]

2.Cry

2.1 ECDSA

题目给了三个东西

  • task.py:生成密钥和签名的程序
  • signatures.txt:使用弱私钥生成的 60 个签名样本
  • public.pem:与私钥对应的公钥
看它task.py的代码就知道这个私钥生成有问题
  1. from ecdsa import SigningKey, NIST521pfrom hashlib import sha512from Crypto.Util.number import long_to_bytes​# 计算固定字符串的SHA512哈希digest_int = int.from_bytes(sha512(b"Welcome to this challenge!").digest(), "big")​# 获取曲线阶数curve_order = NIST521p.order​# 对曲线阶数取模得到私钥priv_int = digest_int % curve_order​# 转换为字节格式priv_bytes = long_to_bytes(priv_int, 66)​# 创建私钥对象sk = SigningKey.from_string(priv_bytes, curve=NIST521p)
复制代码
首先它私钥种子固定不变
私钥的生成依赖于固定字符串 "Welcome to this challenge!",这个字符串在代码中硬编码,任何人都可以访问源代码并计算出完全相同的私钥
接着算法也有问题,仅使用 SHA512 哈希运算就生成私钥,哈希函数是确定性的,给定相同输入必然产生相同输出
【----帮助网安学习,以下所有学习资料免费领!加vx:YJ-2021-1,备注 “博客园” 获取!】
 ① 网安学习成长路径思维导图
 ② 60+网安经典常用工具包
 ③ 100+SRC漏洞分析报告
 ④ 150+网安攻防实战技术电子书
 ⑤ 最权威CISSP 认证考试指南+题库
 ⑥ 超1800页CTF实战技巧手册
 ⑦ 最新网安大厂面试题合集(含答案)
 ⑧ APP客户端安全检测指南(安卓+IOS)
所以,种子字符串是公开的,计算过程是确定性的,无需任何额外信息即可恢复私钥
  1. from ecdsa import SigningKey, NIST521p, VerifyingKeyfrom hashlib import sha512from Crypto.Util.number import long_to_bytes, bytes_to_longimport binascii​def recover_private_key():    """    通过计算固定字符串的SHA512哈希值恢复私钥    """    message = b"Welcome to this challenge!"    digest = sha512(message).digest()    digest_int = int.from_bytes(digest, "big")    curve_order = NIST521p.order    priv_int = digest_int % curve_order        priv_bytes = long_to_bytes(priv_int, 66)    sk = SigningKey.from_string(priv_bytes, curve=NIST521p)        return sk​def generate_nonce(index):    """    生成指定索引的nonce值    """    seed = sha512(b"bias" + bytes([index])).digest()    k = int.from_bytes(seed, "big")    return k​def load_public_key(pem_file="public.pem"):    """    从PEM文件加载公钥    """    with open(pem_file, "rb") as f:        pem_data = f.read()    vk = VerifyingKey.from_pem(pem_data)    return vk​def extract_rs_from_der(sig_bytes):    """    从DER编码的签名中提取r和s值    """    if len(sig_bytes) < 8:        return None, None        pos = 0    if sig_bytes[pos] != 0x30:        return None, None    pos += 1        length_bytes = sig_bytes[pos]    pos += 1        if sig_bytes[pos] != 0x02:        return None, None    pos += 1        r_length = sig_bytes[pos]    pos += 1    r_value = sig_bytes[pos:pos + r_length]    pos += r_length        if sig_bytes[pos] != 0x02:        return None, None    pos += 1        s_length = sig_bytes[pos]    pos += 1    s_value = sig_bytes[pos:pos + s_length]        r_int = bytes_to_long(r_value)    s_int = bytes_to_long(s_value)        return r_int, s_int​def verify_signature_ecdsa(vk, message, signature):    """    使用公钥验证签名    """    try:        return vk.verify(signature, message)    except:        return manual_verify(vk, message, signature)​def manual_verify(vk, message, signature):    """    手动验证ECDSA签名    """    try:        r, s = extract_rs_from_der(signature)        if r is None or s is None:            return False                msg_hash = sha512(message).digest()        msg_hash_int = bytes_to_long(msg_hash)                point = vk.pubkey.point        curve_order = NIST521p.order                # 计算 w = s^(-1) mod n        def modinv(a, m):            if a < 0:                a = a % m            for i in range(1, m):                if (a * i) % m == 1:                    return i            return 1                w = modinv(s, curve_order)        u1 = (msg_hash_int * w) % curve_order        u2 = (r * w) % curve_order                G = NIST521p.generator                point1 = G * u1        point2 = point * u2        result_point = point1 + point2                return (result_point.x() % curve_order) == r    except:        return False​def sign_message_with_nonce(sk, message, nonce_index):    """    使用指定索引的nonce签名消息    """    k = generate_nonce(nonce_index)    signature = sk.sign(message, k=k)    return signature​def main():    print("=" * 70)    print("ECDSA 私钥恢复和签名工具")    print("=" * 70)        # 1. 恢复私钥    print("\n[1] 恢复私钥...")    sk = recover_private_key()    print(f"[✓] 私钥已恢复")    print(f"    私钥值: {sk.privkey.secret_multiplier}")    print(f"    私钥字节: {binascii.hexlify(sk.to_string()).decode()}")        # 2. 加载公钥    print("\n[2] 加载公钥...")    vk = load_public_key()    print("[✓] 公钥已加载")        # 3. 验证私钥正确性    print("\n[3] 验证私钥...")        # 使用一个已有的签名验证    with open("signatures.txt", "r") as f:        first_line = f.readline().strip()        msg_hex, sig_hex = first_line.split(":")        test_msg = bytes.fromhex(msg_hex)        test_sig = bytes.fromhex(sig_hex)        if verify_signature_ecdsa(vk, test_msg, test_sig):        print("[✓] 私钥验证成功!恢复的私钥与公钥匹配")    else:        print("[✗] 私钥验证失败")        return        # 4. 尝试签名获取flag    print("\n[4] 尝试生成签名...")        # 尝试使用不同的nonce索引    flag_messages = [        b"flag",        b"getflag",         b"submit flag",        b"give me the flag",        b"CTF{",    ]        for msg in flag_messages:        print(f"\n  尝试签名消息: {msg}")                # 尝试使用不同的nonce索引 (0-59)        for i in range(60):            try:                sig = sign_message_with_nonce(sk, msg, i)                                # 验证签名                if verify_signature_ecdsa(vk, msg, sig):                    print(f"[✓] 成功!")                    print(f"    Nonce索引: {i}")                    print(f"    签名: {binascii.hexlify(sig).decode()}")                                        # 保存签名到文件                    with open("flag_signature.txt", "w") as f:                        f.write(f"Message: {msg.decode()}\n")                        f.write(f"Nonce Index: {i}\n")                        f.write(f"Signature: {binascii.hexlify(sig).decode()}\n")                                        print(f"\n[+] 签名已保存到 flag_signature.txt")                                        # 5. 展示如何使用                    print("\n" + "=" * 70)                    print("解题步骤:")                    print("=" * 70)                    print(f"""1. 私钥已成功恢复   私钥值: {sk.privkey.secret_multiplier}​2. 使用恢复的私钥,可以:   - 验证任何使用该密钥签名的消息   - 为新消息生成有效签名   - 在CTF服务器上提交签名获取flag​3. 生成的签名:   消息: {msg.decode()}   签名: {binascii.hexlify(sig).decode()}​4. 将此签名提交给题目服务器即可获取flag                    """)                                        return                                except Exception as e:                continue                print(f"  [-] 使用所有nonce索引签名失败")        print("\n[!] 尝试其他方法...")        # 如果上面的方法失败,输出更多信息    print("\n[5] 输出私钥信息供手动使用...")    print(f"\n私钥值 (十进制):")    print(sk.privkey.secret_multiplier)    print(f"\n私钥值 (十六进制):")    print(binascii.hexlify(sk.to_string()).decode())​if __name__ == "__main__":    main()
复制代码

2.2 Ezflag

先ida进行一个逆向找到main函数

只有当输入的密码完全等于 V3ryStr0ngp@ssw0rd 时,程序才会进入 else 分支生成 Flag
  1. std::operator str:        """核心编码逻辑:自定义位流映射"""        out = []        val, bits = 0, 0        for byte in data:            val = (val = 6:                bits -= 6                out.append(self._alphabet[(val >> bits) & 0x3F])                if bits > 0:            out.append(self._alphabet[(val  str:        """计算特定时间戳下的认证指纹"""        u, p = self._user_info        # 预处理密码编码        p_enc = self._transform(p.encode('latin-1'))                # 构造原始载荷        payload = '{"username":"%s","password":"%s"}' % (u, p_enc)        raw_msg = payload.encode('utf-8')​        # 密钥派生 (Key Derivation)        seed = str(tick).encode()        key_block = hashlib.sha256(seed).digest() if len(seed) > 64 else seed        key_block = key_block.ljust(64, b'\x00')​        # 这里的 118(0x76) 和 60(0x3C) 是原始逻辑的特征常数        p1 = bytes([b ^ 118 for b in key_block])        p2 = bytes([b ^ 60 for b in key_block])​        # 嵌套哈希架构 (注意:这是非标准的哈希顺序 inner + opad)        mid_hash = hashlib.sha256(p1 + raw_msg).digest()        final_sig = self._transform(hashlib.sha256(mid_hash + p2).digest())​        # 生成最终校验体        full_body = '{"username":"%s","password":"%s","signature":"%s"}' % (u, p_enc, final_sig)        return hashlib.md5(full_body.encode()).hexdigest()​    def run_audit(self):        """执行扫描任务"""        # 时间范围定义        tz = timezone(timedelta(hours=8))        t_start = int(datetime(2025, 12, 22, 0, 0, tzinfo=tz).timestamp() * 1000)        t_end = int(datetime(2025, 12, 22, 6, 0, tzinfo=tz).timestamp() * 1000)​        print(f"
  2. [*] Task started: scanning range {t_start} -> {t_end}")                total = t_end - t_start        for current_ts in range(t_start, t_end + 1):            token = self.check_sequence(current_ts)                        if token.startswith(self._goal_prefix):                print(f"\n[+] Match discovered at index: {current_ts}")                print(f"[+] Final Flag: flag{{{token}}}")                return​            if current_ts % 100000 == 0:                progress = (current_ts - t_start) / total * 100                print(f"
  3. [*] Processing... {progress:.1f}%", end='\r')​if __name__ == "__main__":    engine = CryptoEngine()    engine.run_audit()
复制代码

3.2 babygame

一道Godot逆向题,得有专门的工具
[img=720,376.07142857142856]https://www.yijinglab.com/guide-img/d9634e2f-3b66-42e7-8279-c0877cdd70e5/42440355-8f53-4c4a-b55f-f45beb2827f6.png[/img]
  1. extends CenterContainer​@onready var flagTextEdit: Node = $PanelContainer / VBoxContainer / FlagTextEdit@onready var label2: Node = $PanelContainer / VBoxContainer / Label2​static var key = "FanAglFanAglOoO!"var data = ""​func _on_ready() -> void :    Flag.hide()​func get_key() -> String:    return key​func submit() -> void :    data = flagTextEdit.text​    var aes = AESContext.new()    aes.start(AESContext.MODE_ECB_ENCRYPT, key.to_utf8_buffer())    var encrypted = aes.update(data.to_utf8_buffer())    aes.finish()​    if encrypted.hex_encode() == "d458af702a680ae4d089ce32fc39945d":        label2.show()    else:        label2.hide()​func back() -> void :    get_tree().change_scene_to_file("res://scenes/menu.tscn")
复制代码
可以看到

  • 初始key:FanAglFanAglOoO!
  • 目标密文hex:d458af702a680ae4d089ce32fc39945d
  • 算法 是 AES ,代码中明确调用了 AESContext.new()
  • 模式是 ECB 代码中使用了 AESContext.MODE_ECB_ENCRYPT
  • 密钥 FanAglFanAglOoO!

    • 该字符串长度为 16 个字符。
    • 在 UTF-8 编码下,16 个字符等于 16 字节(128位),因此,这是 AES-128

照理说直接写个脚本逆向就可以得到flag了,可是一直不对
然后看了题目内容
  1. 题目内容:请找出隐藏的Flag。请注意只有收集了所有的金币,才能验证flag。
复制代码
意思就是金币,也就是分数得达到一个设定好的数才能验证flag,回去逆向看看那里关于分数的函数
[img=720,323.35714285714283]https://www.yijinglab.com/guide-img/d9634e2f-3b66-42e7-8279-c0877cdd70e5/d1d30c7f-1bb4-4505-898e-7b3ab3b85f85.png[/img]

可以看到分数这里的代码是说当分数+1的时候,密钥中的A替换成B
所以正确的密钥应该是
  1. FanBglFanBglOoO!
复制代码
所以套上脚本就是
  1. from Crypto.Cipher import AESkey = b"FanBglFanBglOoO!"ciphertext = bytes.fromhex("d458af702a680ae4d089ce32fc39945d")cipher = AES.new(key, AES.MODE_ECB)result = cipher.decrypt(ciphertext)print(result)
复制代码

更多网安技能的在线实操练习,请点击这里>>
  

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册