furryCTF2025赛前热身题(misc方向)

比赛网站:https://furryctf.com/games/5

1.签到题

题目描述:

1
2
3
话说,你们有发现比赛平台上藏有一个flag吗?

注意flag格式哦~

在赛题主页就可以找到:

flag为:

1
furryCTF{Hack_for_fun_not_for_profit}

2.新的一年,新的开始

题目描述:

1
2
3
4
5
6
7
8
9
10
11
Catch The Future

Time to own 2025

Forever young in hacking

furryCTF{h4ppY_n3w_y34r_2o25_w1th_1Ov3}

祝各位师傅:

栈上生花,堆里藏月,逆向不秃,web不坐牢,pwn穿一切,ak全场! 🚩🎉

这种就是问卷题

flag为:

1
furryCTF{h4ppY_n3w_y34r_2o25_w1th_1Ov3}

3.PassDump

1
2
3
4
5
6
7
8
9
作为CTFer,很多时候都会有电脑放一夜跑程序的经历。

但猫猫看着跑一夜碰撞之后蓝屏的电脑,陷入了沉思……

flag格式为furryCTF{出现问题的文件_蓝屏错误代码_该文件的最后一次编译时间_失败事件的缩写_当时正在使用的应用程序的名称}

例如,这是一个合法的flag:

furryCTF{system.exe_0x0000001A_2024.12.31-14:00:00_DPC_Notepad}

这题需要用到windbg,这里链接就不贴了,网上一搜就有

打开windbg后

打开command,也就是命令行

一般会有这个蓝标字体,如果没有就自己输入

1
!analyze -v

这里我把输出结果放一下

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
12: kd> !analyze -v
Loading Kernel Symbols
..

Press ctrl-c (cdb, kd, ntsd) or ctrl-break (windbg) to abort symbol loads that take too long.
Run !sym noisy before .reload to track down problems loading symbols.

.............................................................
................................................................
................................................................
................................................................
..............................................
Loading User Symbols

Loading unloaded module list
.........................................
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************

VIDEO_TDR_FAILURE (116)
Attempt to reset the display driver and recover from timeout failed.
Arguments:
Arg1: ffffe30a4b8cd010, Optional pointer to internal TDR recovery context (TDR_RECOVERY_CONTEXT).
Arg2: fffff8035ce14790, The pointer into responsible device driver module (e.g. owner tag).
Arg3: ffffffffc000009a, Optional error code (NTSTATUS) of the last failed operation.
Arg4: 0000000000000004, Optional internal context dependent data.

Debugging Details:
------------------

Unable to load image nvlddmkm.sys, Win32 error 0n2
*** WARNING: Unable to verify timestamp for nvlddmkm.sys

KEY_VALUES_STRING: 1

Key : Analysis.CPU.mSec
Value: 2140

Key : Analysis.Elapsed.mSec
Value: 13194

Key : Analysis.IO.Other.Mb
Value: 0

Key : Analysis.IO.Read.Mb
Value: 1

Key : Analysis.IO.Write.Mb
Value: 0

Key : Analysis.Init.CPU.mSec
Value: 484

Key : Analysis.Init.Elapsed.mSec
Value: 28118

Key : Analysis.Memory.CommitPeak.Mb
Value: 102

Key : Analysis.Version.DbgEng
Value: 10.0.29482.1003

Key : Analysis.Version.Description
Value: 10.2509.29.03 amd64fre

Key : Analysis.Version.Ext
Value: 1.2509.29.3

Key : Bugcheck.Code.LegacyAPI
Value: 0x116

Key : Bugcheck.Code.TargetModel
Value: 0x116

Key : Dump.Attributes.AsUlong
Value: 0x21808

Key : Dump.Attributes.DiagDataWrittenToHeader
Value: 1

Key : Dump.Attributes.ErrorCode
Value: 0x0

Key : Dump.Attributes.KernelGeneratedTriageDump
Value: 1

Key : Dump.Attributes.LastLine
Value: Dump completed successfully.

Key : Dump.Attributes.ProgressPercentage
Value: 0

Key : Failure.Bucket
Value: 0x116_IMAGE_nvlddmkm.sys

Key : Failure.Exception.IP.Address
Value: 0xfffff8035ce14790

Key : Failure.Exception.IP.Module
Value: nvlddmkm

Key : Failure.Exception.IP.Offset
Value: 0x1854790

Key : Failure.Hash
Value: {c89bfe8c-ed39-f658-ef27-f2898997fdbd}

Key : Faulting.IP.Type
Value: Paged

Key : Hypervisor.Enlightenments.ValueHex
Value: 0x7417df84

Key : Hypervisor.Flags.AnyHypervisorPresent
Value: 1

Key : Hypervisor.Flags.ApicEnlightened
Value: 0

Key : Hypervisor.Flags.ApicVirtualizationAvailable
Value: 1

Key : Hypervisor.Flags.AsyncMemoryHint
Value: 0

Key : Hypervisor.Flags.CoreSchedulerRequested
Value: 0

Key : Hypervisor.Flags.CpuManager
Value: 1

Key : Hypervisor.Flags.DeprecateAutoEoi
Value: 1

Key : Hypervisor.Flags.DynamicCpuDisabled
Value: 1

Key : Hypervisor.Flags.Epf
Value: 0

Key : Hypervisor.Flags.ExtendedProcessorMasks
Value: 1

Key : Hypervisor.Flags.HardwareMbecAvailable
Value: 1

Key : Hypervisor.Flags.MaxBankNumber
Value: 0

Key : Hypervisor.Flags.MemoryZeroingControl
Value: 0

Key : Hypervisor.Flags.NoExtendedRangeFlush
Value: 0

Key : Hypervisor.Flags.NoNonArchCoreSharing
Value: 1

Key : Hypervisor.Flags.Phase0InitDone
Value: 1

Key : Hypervisor.Flags.PowerSchedulerQos
Value: 0

Key : Hypervisor.Flags.RootScheduler
Value: 0

Key : Hypervisor.Flags.SynicAvailable
Value: 1

Key : Hypervisor.Flags.UseQpcBias
Value: 0

Key : Hypervisor.Flags.Value
Value: 55185662

Key : Hypervisor.Flags.ValueHex
Value: 0x34a10fe

Key : Hypervisor.Flags.VpAssistPage
Value: 1

Key : Hypervisor.Flags.VsmAvailable
Value: 1

Key : Hypervisor.RootFlags.AccessStats
Value: 1

Key : Hypervisor.RootFlags.CrashdumpEnlightened
Value: 1

Key : Hypervisor.RootFlags.CreateVirtualProcessor
Value: 1

Key : Hypervisor.RootFlags.DisableHyperthreading
Value: 0

Key : Hypervisor.RootFlags.HostTimelineSync
Value: 1

Key : Hypervisor.RootFlags.HypervisorDebuggingEnabled
Value: 0

Key : Hypervisor.RootFlags.IsHyperV
Value: 1

Key : Hypervisor.RootFlags.LivedumpEnlightened
Value: 1

Key : Hypervisor.RootFlags.MapDeviceInterrupt
Value: 1

Key : Hypervisor.RootFlags.MceEnlightened
Value: 1

Key : Hypervisor.RootFlags.Nested
Value: 0

Key : Hypervisor.RootFlags.StartLogicalProcessor
Value: 1

Key : Hypervisor.RootFlags.Value
Value: 1015

Key : Hypervisor.RootFlags.ValueHex
Value: 0x3f7

Key : WER.System.BIOSRevision
Value: 1.23.0.0


BUGCHECK_CODE: 116

BUGCHECK_P1: ffffe30a4b8cd010

BUGCHECK_P2: fffff8035ce14790

BUGCHECK_P3: ffffffffc000009a

BUGCHECK_P4: 4

FILE_IN_CAB: furryCTF.dmp

DUMP_FILE_ATTRIBUTES: 0x21808
Kernel Generated Triage Dump

FAULTING_THREAD: ffffe30a86af3040

VIDEO_TDR_CONTEXT: dt dxgkrnl!_TDR_RECOVERY_CONTEXT ffffe30a4b8cd010
Symbol dxgkrnl!_TDR_RECOVERY_CONTEXT not found.

PROCESS_OBJECT: 0000000000000004

BLACKBOXACPI: 1 (!blackboxacpi)


BLACKBOXBSD: 1 (!blackboxbsd)


BLACKBOXNTFS: 1 (!blackboxntfs)


BLACKBOXPNP: 1 (!blackboxpnp)


BLACKBOXWINLOGON: 1 (!blackboxwinlogon)


PROCESS_NAME: System

IP_IN_PAGED_CODE:
nvlddmkm+1854790
fffff803`5ce14790 488b05b9f28dff mov rax,qword ptr [nvlddmkm+0x1133a50 (fffff803`5c6f3a50)]

STACK_TEXT:
ffffc982`1c9677d8 fffff803`43a2375d : 00000000`00000116 ffffe30a`4b8cd010 fffff803`5ce14790 ffffffff`c000009a : nt!KeBugCheckEx
ffffc982`1c9677e0 fffff803`43c97be6 : fffff803`5ce14790 ffffe30a`5b80b5a0 00000000`00000004 ffffe30a`4b8cd010 : dxgkrnl!TdrBugcheckOnTimeout+0x101
ffffc982`1c967820 fffff803`43a324be : 00000000`00000000 00000000`00002000 00000000`00000004 00000000`00000004 : dxgkrnl!ADAPTER_RENDER::Reset+0x232
ffffc982`1c967850 fffff803`43a69375 : ffffe30a`00000100 00000000`00000000 ffffc982`00000000 00000000`00000000 : dxgkrnl!DXGADAPTER::Reset+0x59a
ffffc982`1c9678e0 fffff803`43a694d2 : fffff803`b290ce60 00000000`00000000 ffffb881`c42d1100 fffff803`b29cfbc0 : dxgkrnl!TdrResetFromTimeout+0x15
ffffc982`1c967910 fffff803`b1c3072c : ffffe30a`86af3040 ffffe30a`478ddae0 ffffe30a`478dda00 fffff803`44b52750 : dxgkrnl!TdrResetFromTimeoutWorkItem+0x22
ffffc982`1c967950 fffff803`b1ea007a : ffffe30a`86af3040 ffffe30a`86af3040 fffff803`b1c30140 ffffe30a`478ddae0 : nt!ExpWorkerThread+0x5ec
ffffc982`1c967b30 fffff803`b20a5db4 : ffffb881`c42d1180 ffffe30a`86af3040 fffff803`b1ea0020 00000000`0e5f57dc : nt!PspSystemThreadStartup+0x5a
ffffc982`1c967b80 00000000`00000000 : ffffc982`1c968000 ffffc982`1c961000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x34


SYMBOL_NAME: nvlddmkm+1854790

MODULE_NAME: nvlddmkm

IMAGE_NAME: nvlddmkm.sys

STACK_COMMAND: .process /r /p 0xffffe30a476cb040; .thread 0xffffe30a86af3040 ; kb

FAILURE_BUCKET_ID: 0x116_IMAGE_nvlddmkm.sys

OSPLATFORM_TYPE: x64

OSNAME: Windows 10

FAILURE_ID_HASH: {c89bfe8c-ed39-f658-ef27-f2898997fdbd}

Followup: MachineOwner
---------


对比furryCTF{system.exe_0x0000001A_2024.12.31-14:00:00_DPC_Notepad}这个格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
出现问题的文件:IMAGE_NAME:  nvlddmkm.sys
蓝屏错误代码:
VIDEO_TDR_FAILURE (116) <--- 括号里的 116 就是十进制代码
...
BUGCHECK_CODE: 116 <--- 这里确认
提取结果: 116 (十进制) = 0x00000116 (十六进制)
该文件的最后一次编译时间:lm v m nvlddmkm指令输入后运行
Browse all global symbols functions data Symbol Reload
Timestamp: Tue Feb 11 13:40:16 2025 (67AAE2C0) <--- 就在这一行
CheckSum: 05B57FA7
失败事件的缩写:
*******************************************************************************
* Bugcheck Analysis *
*******************************************************************************

VIDEO_TDR_FAILURE (116) <--- 这里是全名
这里用TDR
当时正在使用的应用程序的名称:这里有点坑,需要一点阅读理解,“当前正在使用”,根据题目场景,当前正在进行碰撞,碰撞会想到啥,hash碰撞吧,想想常用的工具,hashcat就是答案

最后整合一下,flag为:

1
furryCTF{nvlddmkm.sys_0x00000116_2025.02.11-13:40:16_TDR_hashcat}

4.IIS服务器

题目描述:

1
2
3
猫猫前段时间闲着没事搭建了一个IIS服务器。

不过,最近猫猫发现,服务器上好像多了个文件……?

用wireshark打开pcap文件

依旧先ctrl+f搜索flag,找到了交了但是是错的因为那只是一个人登录用的password

确实被骗到了哈哈哈,诈骗的小曲

你观察就可以发现,流量大多数是TCP和http流,所以右键追踪流

最后找到一个传了fl2g.txt的流量

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
GET /execute/f12g.txt HTTP/1.1
Host: 26.114.202.3
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9


0.000474s
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Encoding: gzip
Last-Modified: Wed, 10 Jul 2024 03:14:49 GMT
Accept-Ranges: bytes
ETag: "c0c41d5377d2da1:0"
Vary: Accept-Encoding
Server: Microsoft-IIS/7.5
X-Powered-By: ASP.NET
Date: Wed, 10 Jul 2024 04:37:33 GMT
Content-Length: 191


0.000000s
ZnVycnlDVEZ7RGlkX1lvdV9Ob3RlX1RoZV9EaWZmX0luX0Vycm9yX1BhZ2U/fQ==
0.175592s
GET /favicon.ico HTTP/1.1
Host: 26.114.202.3
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8
Referer: http://26.114.202.3/execute/f12g.txt
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9


0.000349s
HTTP/1.1 404 Not Found
Content-Type: text/html
Server: Microsoft-IIS/7.5
X-Powered-By: ASP.NET
Date: Wed, 10 Jul 2024 04:37:34 GMT
Content-Length: 1163


0.000000s
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312"/>
<title>404 - ..................</title>
<style type="text/css">
<!--
body{margin:0;font-size:.7em;font-family:Verdana, Arial, Helvetica, sans-serif;background:#EEEEEE;}
fieldset{padding:0 15px 10px 15px;}
h1{font-size:2.4em;margin:0;color:#FFF;}
h2{font-size:1.7em;margin:0;color:#CC0000;}
h3{font-size:1.2em;margin:10px 0 0 0;color:#000000;}
#header{width:96%;margin:0 0 0 0;padding:6px 2% 6px 2%;font-family:"trebuchet MS", Verdana, sans-serif;color:#FFF;
background-color:#555555;}
#content{margin:0 0 0 2%;position:relative;}
.content-container{background:#FFF;width:96%;margin-top:8px;padding:10px;position:relative;}
-->
</style>
</head>
<body>
<div id="header"><h1>..........</h1></div>
<div id="content">
<div class="content-container"><fieldset>
<h2>404 - ..................</h2>
<h3>......................................................</h3>
</fieldset></div>
</div>
</body>
</html>

这个是不是很像base64啊,虽然不是Zmxh这种标准开头

1
2
0.000000s
ZnVycnlDVEZ7RGlkX1lvdV9Ob3RlX1RoZV9EaWZmX0luX0Vycm9yX1BhZ2U/fQ==

解密之后得到flag(这里用随波逐流)

1
furryCTF{Did_You_Note_The_Diff_In_Error_Page?}

5.盲盒

题目描述:

1
2
3
4
5
来开盲盒吧~nwn

注:本题原本的flag格式为flag{},因为懒得改附件了,所以找到flag后请将里面的“flag”修改为“furryCTF”

比如flag{Hi}修改为furryCTF{Hi}即为正确答案。

像这种word,excel,ppt这种题目,这种隐写的一般的处理方式就是把文件当压缩包去看

直接用随波逐流的binwalk工具去分离

一个一个文件找啊找,看有没有和flag相关的

在sharedStrings里

1
2
3
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="762826" uniqueCount="2"><si><t>来开盲盒吧~</t><phoneticPr fontId="1" type="noConversion"/></si><si><t>flag 不在这</t><phoneticPr fontId="1" type="noConversion"/></si></sst>
<!-- ‬‌‬‍‬‍‍‍‬‌‌‌‌‍‌‍‬‌‌‬‌‍‬‌‍‍‌‬‌‬‬‬‌‬‍‌‍‌‬‍‍‬‬‌‍‬‌‍‍‌‍‍‍‌‌‌‍‍‍‍‌‬‬‌‬‌‍‬‍‬‍‌‍‌‌‌‌‌‌‍‬‬‌‌‌‌‍‍‌‌‌‌‍‬‬‌‌‌‌‍‬‌‌‌‌‌‌‬‬‌‌‌‌‍‬‍‬‌‌‌‌‍‬‌‌‌‌‌‍‬‌‍‌‌‌‌‍‬‍‌‌‌‌‍‬‌‌‌‌‍‍‬‬我也没说‌‌‌‌‌‌‍‌‌‌‌‌‬‍‌‌‌‌‍‍‌‌‌‌‍‍‍‌‌‌‌‌‍‬‬‌‌‌‌‌‍‬‍‍‌‌‌‌‍‍‌‌‌‌‍‍‌‌‌‌‌‍‍‌‌‌‌‌‍‌‬‌‌‌‌‌‌‍‌‌‌‌‍‬‬‌‌‌‌‍‬‍‌‌‌‌‍‍flag‌‌‌‌‍‌‬‍‌‌‌‌‍‬‬‌‌‌‌‍‍‌‌‌‌‍‌‍‍‌‌‌‌‍‬‌在这呀‌‌‌‌‍‬‌‌‌‌‌‌‬‍nwn‌‌‌‌‍‬‌‌‌‌‌‍‍,你不会想在这里找到flag叭~ -->

是不是很明显的零宽隐写

‬‍‌‌‌‌‍‬‬‌‌‌‌‍‍‌‌‌‌‍‌‍‍‌‌‌‌‍‬‌

到网站里,随波逐流的就可以

然后你就看到flag了

flag为:

1
flag{Z19_The_Str1ng_In_Exc9l}

6.丢失的文档

题目描述

1
因为一场停电,猫猫刚写了一半的小说无了……

附件是一个asd文件其实我的第一想法就是改后缀,因为asd是一个不好处理陌生的格式,而这本来就应该是word文件,所以把后缀改为.docx打开,然后发现flag出了

1
2
3
对应的flag为:
furryCTF{How_To_F1x_This_Wor6_D0cument}
123

7.大伙儿好像太无聊了那就整点无聊的东西(?

题目描述:

1
2
3
大概是一些无聊的产物(?

猫猫的一点PS:有谁想读一遍这个玩意喵owo

附件内容是:

1
REOREREREREREOREREREREREREOOOREOREREREREOREREREOREREREOOREOREREREREOREREREREREOREOREREREREOOREOREOREOOREREOOREOOREOREOREREREREOOREOREREREREREOREOOOREOREREREOREOOOOREREOREOREOOOREOREREREREREOREOOREOOREREOOREREREREREOREOREOOOOREREOOREREREOREREOOREREREOREREOREOOOOREREOREREREOREOREOREREOREREREREOOREREOOOREOOOREOREOREOREREOOOOREOREOOREREREREOOREOOREREREOOREOOREREREOREOREOREREREOOREREOOREREO

第一部分:把“贪心切分”聊得明明白白

咱们把“贪心切分”这个词拆开,用大白话和生活中的例子来理解。

  1. 什么是“切分” (Tokenization)?

通俗解释:
“切分”就是把一长串看起来乱糟糟的东西,按照某种规则,切成一个个有意义的“小块”。这个“小块”就叫 Token(记号)。

生活中的例子:
想象一下你读英文句子:thisisasentence
你的大脑不会把它看成一堆字母,而是自动“切分”成单词:this, is, a, sentence。这里的每个单词,就是一个 Token。

在这道题里:
原始密文 REORERER... 就是那串长长的、没空格的句子。
通过观察,我们发现它好像不是由单个字母 R, E, O 构成的,而是由 ORE 这两种“单词”拼起来的。
所以,“切分”就是要把 REORER... 切成 ['RE', 'O', 'RE', ...] 这样一个“单词列表”。

  1. 什么是“贪心” (Greedy)?

通俗解释:
“贪心”是一种非常直白、简单的做事策略,就是“只顾眼前,不看长远”。每一步都做出当下看起来最好的选择。

生活中的例子:
假设你要找零钱 87 分,你手头有面值为 25, 10, 5, 1 的硬币。
“贪心”的做法是:

  1. 先拿出能用的最大面额:25分,还差 62 分。
  2. 再拿一个 25分,还差 37 分。
  3. 再拿一个 25分,还差 12 分。
  4. 25的用不了了,用下一个最大的:10分,还差 2 分。
  5. 10和5的都用不了,用 1分,还差 1 分。
  6. 再用 1分,找零完成。
    你每一步都“贪心地”选了当前能用的最大面额,这就是贪心算法。

在这道题里:
我们从字符串的开头 s = "REORER..." 开始切分:

  1. 指针在第 0 位 (R)

    • 往前看 1 位是 R,它不是一个完整的“单词”(Token)。
    • 往前看 2 位是 RE,它是一个完整的“单词”!
    • 贪心选择:我们选择匹配最长的那个,也就是 RE
    • 于是,第一个 Token 就是 RE。指针向后移动 2 位。
  2. 指针现在在第 2 位 (O)

    • 往前看 1 位是 O,它是一个完整的“单词”。
    • 往前看 2 位是 OR,它不是一个合法的“单词”。
    • 贪心选择:我们选择匹配 O
    • 第二个 Token 就是 O。指针向后移动 1 位。
  3. 如此循环… 直到把整个字符串切完。

  4. 什么情况下我能想到用“贪心切分”?

这是解决问题的关键。你可以通过以下几点来判断:

  1. 特征一:由有限的几种“构件”组成。
    当你看到一长串文本,但翻来覆去就那么几种固定的“模式”或“片段”在重复出现时。比如这道题,看几眼就发现,除了 O,就是 RE 粘在一起。这强烈暗示了基本构件(Token)就是 ORE

  2. 特征二:构件之间没有明显的“分隔符”。
    像摩斯电码,点划之间有短停顿,字母之间有长停顿。但这道题的 REO 是紧挨着的,REOORE 这样,没有空格或特殊符号隔开。这就逼着我们必须自己想办法把它们切开。

  3. 特征三:构件之间没有“歧义”。
    “贪心”策略能成功,是因为它不会“切错”。比如,如果我们的“单词”是 AAB,那么遇到 AB 时,如果贪心切了 A,剩下的 B 就无法处理了。
    但在本题中,ORE 这两个 Token 非常完美:

    • 一个以 O 开头。
    • 一个以 R 开头。
      它们的首字母完全不同,所以从任何位置开始,匹配哪个是唯一的,不存在二选一的困惑。这种没有歧义的情况,就是使用贪心切分最理想的场景。

一句话总结:当你发现一个长字符串可以被一小组“没有歧义的、固定的小模式”完全拼成时,就应该立刻想到用“贪心切分”把它转换成一个“小模式”的序列,为后续解码铺路。


第二部分:CyberChef 复现超详细步骤(带中间结果)

下面我们一步步来,每一步都告诉你为什么这么做,以及做完后输出应该是什么样子。

准备工作:打开 CyberChef 网站,把你的密文原文粘贴到右上角的 Input 框里。

网站地址:https://cyberchef.org/

步骤 1:Find / Replace (把 RE 换成 1)

  • 操作:在左侧 Operations 搜索框里输入 Find,找到 Find / Replace,把它拖到中间的 Recipe 区域。

  • 配置

    • Find 框里,填入 RE
    • Replace 框里,填入 1
    • 选项中,Global match,Case insensitive,Multiline matching全部打开就行
  • 目的:这一步是执行我们的“映射”规则,把我们认定的第一个 Token RE 转换成二进制里的 1

  • 中间结果:此时,右下角 Output 框的内容会变成:

    1
    1O11111O111111OOO1O1111O111O111OO1O1111O11111O1O1111OO1O1O1OO11OO1OO1O1O1111OO1O11111O1OOO1O111O1OOOO11O1O1OOO1O11111O1OO1OO11OO11111O1O1OOOO11OO111O11OO111O11O1OOOO11O111O1O1O11O1111OO11OOO1OOO1O1O1O11OOOO1O1OO1111OO1OO111OO1OO111O1O1O111OO11OO11O

步骤 2:Find / Replace (把 字母 O 换成 0)

  • 操作:再拖动一个 Find / ReplaceRecipe 区域,放在刚才那一步的下面。

  • 配置

    • Find 框里,填入 O
    • Replace 框里,填入 0
  • 目的:完成映射的另一半,把 Token O 转换成二进制里的 0

  • 中间结果:现在,Output 框里的内容就是一串纯粹的 01 了,也就是我们需要的比特流:

    1
    10111110111111000101111011101110010111101111101011110010101001100100101011110010111110100010111010000110101000101111101001001100111110101000011001110110011101101000011011101010110111100110001000101010110000101001111001001110010011101010111001100110

步骤 3:Reverse (反转)

  • 操作:在左侧搜索 Reverse,拖到 Recipe 中,放在第二步之后。
  • 配置:保持默认选项 Mode: Standard (character) 即可。
  • 目的:这是解题的关键一步。直接把二进制转文本会是乱码,我们猜测可能存在“整体倒序”的混淆。这一步就是把整条比特流从头到尾反转过来。
  • 中间结果Output 框里的比特流现在是倒序的了:
    1
    01100110011101010111001001110010011110010100001101010100010001100111101101010111011000010110111001101110011000010101111100110010010111110100010101100001011101000101111101001111010100100110010101001111010111110111101001110111011110100011111101111101

步骤 4:From Binary (从二进制转换)

  • 操作:在左侧搜索 From Binary,拖到 Recipe 的最后。

  • 配置

    • 在下面的选项中,确保 Data formatBinary,并且 Length8
    • 其他选项默认就行。
  • 目的:将反转后的比特流,按照每 8 位一个字节的标准,翻译成 ASCII 字符。

  • 最终结果Output 框里会清晰地显示出 flag:

    1
    furryCTF{Wanna_2_Eat_OReO_zwz?}

最后总结一下像这种有很多重复字符的字符串,你其实应该想到把重复字符看做0和1,只不过字符的整体需要一点经验,比如本题的RE和O,你只要想到O很像0其实就能联想到RE可能是1了。

当然可能还有另一种情况就是摩斯电码,这题很好有这个小彩蛋,但算个迷惑项。

  • RE = . (点)
  • O = - (划)
  • OO = 字母之间的分隔符 (我用单个空格 表示)
  • OOO = 单词之间的分隔符 (我用斜杠/表示)

根据这个规则,得到下面的标准摩尔斯电码:

1
-. . ...- . .-. / --. --- -. -. . / --. .. ...- . / -.-- --- ..- / ..- .--. / -. . ...- . .-. / --. --- -. -. . / .-.. . - / -.-- --- ..- / -.. --- .-- -.

你拖进随波逐流解密一下:

1
NEVERGONNEGIVEYOUUPNEVERGONNELETYOUDOWN

又是经典诈骗。

这题还有一个脚本解法:

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
s = "REOREREREREREOREREREREREREOOOREOREREREREOREREREOREREREOOREOREREREREOREREREREREOREOREREREREOOREOREOREOOREREOOREOOREOREOREREREREOOREOREREREREREOREOOOREOREREREOREOOOOREREOREOREOOOREOREREREREREOREOOREOOREREOOREREREREREOREOREOOOOREREOOREREREOREREOOREREREOREREOREOOOOREREOREREREOREOREOREREOREREREREOOREREOOOREOOOREOREOREOREREOOOOREOREOOREREREREOOREOOREREREOOREOOREREREOREOREOREREREOOREREOOREREO"

def tokenize_oreo(s):
i = 0
tokens = []
while i < len(s):
if s[i] == 'O':
tokens.append('O')
i += 1
elif s[i:i+2] == 'RE':
tokens.append('RE')
i += 2
else:
return None
return tokens

def bits_to_text(bits, width=8, msb_first=True):
out = []
for i in range(0, len(bits) // width * width, width):
chunk = bits[i:i+width]
if not msb_first:
chunk = chunk[::-1]
val = int(chunk, 2)
if 0 <= val < 256:
out.append(chr(val))
else:
out.append('?')
return ''.join(out)

tokens = tokenize_oreo(s)
if not tokens:
print("不能用 {O, RE} 完整切分")
exit()

candidates = []
for zero, one in [('O','RE'), ('RE','O')]:
bits = ''.join('0' if t == zero else '1' for t in tokens)
for width in (8,7):
for msb in (True, False):
txt = bits_to_text(bits, width=width, msb_first=msb)
candidates.append((zero, one, width, msb, txt))

found = [c for c in candidates if "furryCTF{" in c[4]]
if found:
for z,o,w,m,txt in found:
print(f"映射 {z}->0, {o}->1, width={w}, msb_first={m}")
print(txt)
else:
# 额外尝试:反转整体位串再解一次
more = []
for zero, one in [('O','RE'), ('RE','O')]:
bits = ''.join('0' if t == zero else '1' for t in tokens)
bits = bits[::-1]
for width in (8,7):
for msb in (True, False):
txt = bits_to_text(bits, width=width, msb_first=msb)
more.append((zero, one, width, msb, txt))
found2 = [c for c in more if "furryCTF{" in c[4]]
if found2:
for z,o,w,m,txt in found2:
print(f"[rev] 映射 {z}->0, {o}->1, width={w}, msb_first={m}")
print(txt)
else:
# 打印可见文本里最像的几条,方便人工观察
def score(t):
good = sum(c in "furryCTF{}_-:;,.@/=+[]()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 " for c in t)
return good / max(1,len(t))
best = sorted(candidates, key=lambda x: score(x[4]), reverse=True)[:5]
for z,o,w,m,txt in best:
print(f"候选 映射 {z}->0, {o}->1, width={w}, msb_first={m}, 可见率={score(txt):.2f}")
print(txt[:200])

跑出来的结果是:

1
2
[rev] 映射 O->0, RE->1, width=8, msb_first=True
furryCTF{Wanna_2_Eat_OReO_zwz?}

8.Miscode

题目描述:

1
2
3
这些文件好像都是乱码……?
不过,似乎有什么东西混进了.gitattributes?
温馨提示:请注意flag格式哦~

打开附件,.gitattributes的内容如下:

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
*.php linguist-language=7b
*.swift linguist-language=61
*.scala linguist-language=48
*.py linguist-language=66
*.erl linguist-language=76
*.c linguist-language=72
*.f90 linguist-language=47
*.kt linguist-language=37
*.rb linguist-language=53
*.lua linguist-language=57
*.cs linguist-language=69
*.go linguist-language=43
*.m linguist-language=31
*.js linguist-language=74
*.ex linguist-language=36
*.pl linguist-language=31
*.clj linguist-language=68
*.vb linguist-language=5f
*.sh linguist-language=46
*.cpp linguist-language=79
*.r linguist-language=21
*.ml linguist-language=7d
*.ts linguist-language=5f
*.hs linguist-language=6c
*.java linguist-language=75
*.rs linguist-language=54

random file那个文件夹下有3000多个文件但是两种文件对比后可以发现random file下的文件后缀名就是.gitattributes有的,所以是有规律性的,大概率和次数有关,可能需要脚本。

  • 背景:仓库用 .gitattributes 把每种扩展名映射到一个 1 字节十六进制值(linguist-language=xx),这其实就是把扩展名→ASCII 字符的字典藏在 GitHub Linguist 配置里;random_files/ 里放了大量占位文件,通过“每种扩展名出现的次数”来给字符排位。
  • 思路:解析 .gitattributes 得到映射表 ext→char;统计 random_files/ 中每个扩展名的文件数;按“出现次数升序”排序(同次数按 .gitattributes 原顺序稳定打破平手);把对应字符连成串。题面还放了外层干扰前后缀(furryCTF{FTCyruf}),以及可能的空 {},清理后规范输出 furryCTF{...}

代码解释

题目的核心思路隐藏在 Git 仓库的两个部分:

  1. .gitattributes 文件:定义了文件后缀名 (ext) 和一个 ASCII 字符之间的映射关系。

  2. random_files/ 目录:包含了大量不同后缀名的文件,这些文件的数量决定了对应字符的排列顺序。

    !!!注意:要把**.gitattributesrandom_files**和脚本放在同一个目录下。

脚本通过以下步骤来解出 Flag:

  1. 参数解析 (argparse)

    • 脚本可以接受两个命令行参数:
      • path: 挑战仓库的根目录路径,默认为当前目录 (.)。
      • --mode: 拼接字符的顺序,fwd 代表正序(从前到后),rev 代表逆序(从后往前),默认是 rev,因为这正是本题的解法。
  2. 解析映射关系 (parse_mapping 函数)

    • 此函数读取 .gitattributes 文件。
    • 它使用正则表达式 ^\*\.(\w+)\s+linguist-language=([0-9a-fA-F]{2})\s*$ 来匹配类似 *.txt linguist-language=41 这样的行。
    • 正则表达式解析
      • ^\*\.:匹配以 *. 开头的行。
      • (\w+):捕获文件后缀名(如 txt)。
      • \s+linguist-language=:匹配中间的固定文本。
      • ([0-9a-fA-F]{2}):捕获两位十六进制数(如 41)。
      • \s*$:匹配行尾的任意空格。
    • 对于每个匹配行,它将后缀名存为 key,并将两位十六进制数解码为对应的 ASCII 字符(例如 41 -> 'A')作为 value,存入一个字典 mp 中并返回。
  3. 统计文件数量 (count_exts 函数)

    • 此函数递归地遍历 random_files/ 目录下的所有文件。
    • 对于每个文件,它提取其后缀名(如 .txt),去除开头的点号 .,并转换为小写,以保证计数的一致性。
    • 使用 collections.Counter 对象来高效地统计每种后缀名出现的次数,并返回这个计数器。
  4. 主逻辑 (main 函数)

    • 初始化和校验:设置好文件路径,并检查 .gitattributesrandom_files/ 是否存在,如果不存在则退出程序。
    • 调用核心函数:调用 parse_mappingcount_exts 获取映射表和计数值。
    • 排序
      • 将映射表中的后缀名和它们对应的计数值组合成一个元组列表 (ext, count)
      • 最关键的一步:items.sort(key=lambda t: t[1])。这行代码根据元组的第二个元素(也就是文件数量 count)对列表进行 升序 排序。Python 的 sort 是稳定的,意味着如果两个后缀名文件数量相同,它们在排序后的相对位置将保持不变(与它们在 .gitattributes 文件中的顺序一致)。
    • 拼接 Flag
      • 根据 --mode 参数决定拼接顺序。默认是 rev,所以 reversed(items) 会将排好序的列表反转。这意味着,文件数量最多的后缀名对应的字符会排在最前面
      • 最后,它遍历这个序列,从映射表 mp 中取出每个后缀名对应的字符,并将它们拼接成一个原始的字符串 s
  5. 格式化输出 (normalize 函数)

    • 此函数负责清理拼接好的原始字符串 s
    • 它移除了可能存在于字符串开头和结尾的干扰项(furryCTF{FTCyruf})。
    • 它还会移除空的花括号 {}
    • 最后,它提取出花括号内的核心内容,并用标准的 furryCTF{...} 格式包裹起来,打印到控制台。
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
import re  # 导入正则表达式模块,用于文本匹配
import argparse # 导入参数解析模块,用于处理命令行参数
from pathlib import Path # 导入路径处理模块,以面向对象的方式处理文件系统路径
from collections import Counter # 导入计数器类,用于高效地统计元素出现次数

def parse_mapping(gitattributes: Path) -> dict:
"""
解析 .gitattributes 文件,提取文件后缀名到 ASCII 字符的映射。
例如,解析行: *.ext linguist-language=41 -> {'ext': 'A'}

Args:
gitattributes (Path): .gitattributes 文件的路径对象。

Returns:
dict: 一个从后缀名(小写)到对应字符的映射字典。
"""
mp = {} # 初始化一个空字典来存储映射关系
# 编译一个正则表达式,用于高效匹配目标行格式
# 模式解释:
# ^ - 匹配行首
# \*\. - 匹配字面量 "*. "
# (\w+) - 捕获组1: 匹配一个或多个单词字符(即文件后缀名)
# \s+ - 匹配一个或多个空白字符
# linguist-language= - 匹配字面量 "linguist-language="
# ([0-9a-fA-F]{2}) - 捕获组2: 匹配两位十六进制数
# \s* - 匹配零个或多个空白字符
# $ - 匹配行尾
pat = re.compile(r'^\*\.(\w+)\s+linguist-language=([0-9a-fA-F]{2})\s*$')

# 以只读模式打开 .gitattributes 文件,指定 ascii 编码并忽略错误
with gitattributes.open('r', encoding='ascii', errors='ignore') as f:
for line in f:
# 尝试对每一行进行正则匹配
m = pat.match(line.strip())
if m: # 如果匹配成功
ext = m.group(1).lower() # 提取后缀名并转为小写,确保一致性
# 将十六进制字符串转换为字节,再解码为 ASCII 字符
ch = bytes.fromhex(m.group(2)).decode('ascii')
mp[ext] = ch # 将映射关系存入字典
return mp

def count_exts(random_dir: Path) -> Counter:
"""
递归统计指定目录中所有文件的后缀名出现次数。

Args:
random_dir (Path): 要统计的目录路径对象。

Returns:
Counter: 一个包含后缀名及其计数的 Counter 对象。
"""
cnt = Counter() # 初始化一个计数器
# rglob('*') 递归地查找目录下的所有文件和文件夹
for p in random_dir.rglob('*'):
# 确认 p 是一个文件并且有后缀名
if p.is_file() and p.suffix:
# p.suffix 返回带点的后缀(如 ".txt")
# [1:] 切片操作去除开头的点,lower() 转为小写
cnt[p.suffix[1:].lower()] += 1
return cnt

def normalize(raw: str) -> str:
"""
清理原始字符串,移除边缘的干扰项,并规范化为 furryCTF{...} 格式。

Args:
raw (str): 原始拼接出的字符串。

Returns:
str: 格式化后的 Flag 字符串。
"""
t = raw
# 如果字符串以 'furryCTF' 开头,则移除它
if t.startswith('furryCTF'):
t = t[len('furryCTF'):]
# 如果字符串以 '{FTCyruf}' 结尾,则移除它
if t.endswith('{FTCyruf}'):
t = t[:-len('{FTCyruf}')]
# 移除可能存在的空花括号
t = t.replace('{}', '')

# 再次使用正则查找被花括号包裹的核心内容
m = re.search(r'\{([^{}]+)\}', t)
# 如果找到,payload 就是括号里的内容;否则,就去除首尾的括号和空白
payload = m.group(1) if m else t.strip('{}').strip()
# 返回标准格式的 Flag
return f'furryCTF{{{payload}}}'

def main():
"""
主执行函数,协调整个解题流程。
"""
# 创建一个命令行参数解析器
ap = argparse.ArgumentParser(description="Solve the gitattributes CTF challenge.")
# 添加 'path' 参数,nargs='?' 表示可选,default='.' 表示默认当前目录
ap.add_argument('path', nargs='?', default='.', help="Path to the challenge repository root.")
# 添加 '--mode' 参数,限定选择为 'fwd' 或 'rev',默认为 'rev'
ap.add_argument('--mode', choices=['fwd', 'rev'], default='rev',
help='Assembly order: fwd=forward (ascending count), rev=reverse (descending count, default)')
args = ap.parse_args() # 解析命令行传入的参数

# 将路径转换为绝对路径,避免相对路径问题
base = Path(args.path).resolve()
gitattributes = base / '.gitattributes'
random_dir = base / 'random_files'

# 检查所需文件和目录是否存在,如果不存在则打印错误并退出
if not gitattributes.is_file():
raise SystemExit(f'Error: Not found: {gitattributes}')
if not random_dir.is_dir():
raise SystemExit(f'Error: Not found: {random_dir}')

# 调用函数获取映射表和文件计数
mp = parse_mapping(gitattributes)
if not mp:
raise SystemExit('Error: No mappings found in .gitattributes')

cnt = count_exts(random_dir)
if not cnt:
raise SystemExit('Error: No files found in random_files')

# 将映射表中的后缀名和其对应的计数值组合成一个元组列表
# 这样做可以确保排序的稳定性(基于 .gitattributes 中的原始顺序)
items = [(ext, cnt[ext]) for ext in mp.keys() if ext in cnt]
# 核心排序逻辑:根据元组的第二个元素(即文件数量)进行升序排序。
# Python的 sort 是稳定的,计数值相同时,元素原始相对顺序不变。
items.sort(key=lambda t: t[1])

# 根据 --mode 参数决定遍历序列的方向
# 默认是 'rev',所以使用 reversed() 来反转序列,实现按数量降序遍历
seq = items if args.mode == 'fwd' else reversed(items)
# 使用列表推导和 join 方法,高效地拼接出最终的原始字符串
s = ''.join(mp[ext] for ext, _ in seq)

# 打印经过清理和格式化后的最终 Flag
print(normalize(s))

# 当该脚本被直接执行时,调用 main() 函数
if __name__ == '__main__':
main()

最后输出flag:

1
furryCTF{Sia7t_W1!H_G1lhv6}

这题既要统计各个后缀名次数,并把它们进行转换,还要注意得到的字符串是flag的反转形式,要逆回来。