0.前言
小比赛随便打,国赛教我做人....
1.AI安全
1.1The Silent Heist
- 题目内容:目标银行部署了一套基于 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- 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"
- [*] 正在生成 {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 payloaddef pwn_bank(): host = '182.92.11.65' port = 30799 payload = generate_payload() try: # 2. 建立连接 print(f"
- [*] 正在连接到 {host}:{port}...") s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, port)) # 接收服务器欢迎语 # s.recv(1024) # 3. 发送数据 print("
- [*] 正在传输数据流并注入金额...") s.sendall(payload.encode()) # 4. 发送结束标志 s.sendall(b"EOF\n") # 5. 接收返回结果(Flag通常在这里) print("
- [*] 等待银行系统响应...") 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的代码就知道这个私钥生成有问题- 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)
所以,种子字符串是公开的,计算过程是确定性的,无需任何额外信息即可恢复私钥- from ecdsa import SigningKey, NIST521p, VerifyingKeyfrom hashlib import sha512from Crypto.Util.number import long_to_bytes, bytes_to_longimport binasciidef 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 skdef generate_nonce(index): """ 生成指定索引的nonce值 """ seed = sha512(b"bias" + bytes([index])).digest() k = int.from_bytes(seed, "big") return kdef 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 vkdef 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_intdef 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 Falsedef sign_message_with_nonce(sk, message, nonce_index): """ 使用指定索引的nonce签名消息 """ k = generate_nonce(nonce_index) signature = sk.sign(message, k=k) return signaturedef 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服务器上提交签名获取flag3. 生成的签名: 消息: {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- 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"
- [*] 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"
- [*] 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] - extends CenterContainer@onready var flagTextEdit: Node = $PanelContainer / VBoxContainer / FlagTextEdit@onready var label2: Node = $PanelContainer / VBoxContainer / Label2static var key = "FanAglFanAglOoO!"var data = ""func _on_ready() -> void : Flag.hide()func get_key() -> String: return keyfunc 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了,可是一直不对
然后看了题目内容- 题目内容:请找出隐藏的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
所以正确的密钥应该是所以套上脚本就是- from Crypto.Cipher import AESkey = b"FanBglFanBglOoO!"ciphertext = bytes.fromhex("d458af702a680ae4d089ce32fc39945d")cipher = AES.new(key, AES.MODE_ECB)result = cipher.decrypt(ciphertext)print(result)
复制代码
更多网安技能的在线实操练习,请点击这里>>
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |