很多人第一次打开CTF比赛的题目列表,反应是一样的:Web、Pwn、Reverse、Crypto、Misc五个分类摆在那儿,每个分类下面几道题,分值从100到500不等,点进去看一眼,要么是一串看不懂的流量包,要么是一个黑框框的二进制文件,要么是一个只有输入框的网页。然后关掉页面,心想这玩意儿跟我没关系。我当年也是这样,第一次参赛全程零解题,坐在电脑前看别人榜单往上刷。
后来打得多了才明白,CTF的题型其实高度套路化,五大方向各自有一套非常稳定的考点分布和解题范式。真正卡住新手的不是技术深度,而是没人告诉他"这个方向到底在考什么、拿到题第一分钟该干什么"。这篇就把CTF的题型从入门到实战彻底捋一遍,从每个方向的典型题目长什么样、考点怎么分布,到实际操作时的排查顺序和脚本写法,都摊开讲。适合完全没打过比赛的新手建立全景认知,也适合打过几场但一直卡在低分区的朋友查漏补缺。
1. 先把题型地图铺开,别急着埋头刷题
新手最常见的错误是一上来就扎进某个方向死磕,磕了两周发现这个方向的题在自己参加的比赛里占比很小,或者难度曲线陡到自己根本够不着。CTF的五个方向不是平权的,它们的分值分布、入门门槛、上手速度差异非常大,先看清地图再决定主攻哪个,能省下大量试错时间。
1.1 解题赛和攻防赛是两套完全不同的游戏规则
大多数人接触的第一场比赛都是解题赛(Jeopardy模式),给你一堆独立的题目,每道题藏着一个flag,找到就得分。这种赛制的核心是"广度+深度"的组合:你需要在有限时间内判断哪些题是自己能做的,然后快速做掉。它考验的是知识面的宽度和单点突破的速度,跟真实攻防关系不大,本质是一场解谜竞赛。
攻防赛(Attack-Defense模式)就完全不一样了。每支队伍维护一台或多台靶机,既要修自己机器上的漏洞防止被扣分,又要写exp去打别人的机器拿分。这种赛制下,你会写exp只是及格线,能不能在几十分钟内定位漏洞、写出稳定的利用脚本、并且扛住对手的持续攻击,才是分水岭。它对工程能力、团队协作、应急响应的要求远高于解题赛。
还有一类混合赛制,前半程解题拿初始分数,后半程用解题获得的权限去打真实靶标,本质是解题赛的延伸。新手建议从纯解题赛入手,因为它的反馈最直接——做出来就是做出来,做不出来看writeup也能学到东西。攻防赛在没有稳定队伍和一定代码能力之前参加,体验会非常差,基本就是全程被刷分。
提醒一句:不同赛制的备赛方式完全不同。解题赛靠刷题量和知识面,攻防赛靠脚本工程化和应急演练。别拿刷解题赛的方法去准备攻防赛,会翻车。
1.2 五个方向的难度曲线和分值性价比
把五个方向的特性摊开对比,你会发现问题不是"哪个方向最好",而是"现阶段哪个方向最适合我"。下面这张表是我按自己和身边人的经验整理的,分值是常见区间,不同比赛会有浮动。
| 方向 | 典型分值 | 入门门槛 | 上手速度 | 知识依赖 | 新手性价比 |
|---|---|---|---|---|---|
| Misc | 50-200 | 低 | 快 | 工具熟练度、编码常识 | 极高 |
| Crypto | 100-300 | 中 | 中 | 数学基础、脚本能力 | 中高 |
| Web | 100-500 | 中 | 中 | HTTP协议、常见漏洞原理 | 高 |
| Reverse | 150-400 | 中高 | 慢 | 汇编、编译原理、调试 | 中 |
| Pwn | 200-500 | 高 | 慢 | 内存布局、系统调用、ROP | 低(初期) |
Misc之所以性价比最高,是因为它的题目大多围绕编码转换、文件格式、流量分析、隐写这些内容,工具链成熟,很多时候靠耐心和细心就能拿分。一场比赛里Misc的基础分如果能全吃下来,名次就不会太难看。
Web的性价比排第二,原因是它的考点极其集中。SQL注入、文件包含、反序列化、模板注入、SSRF这几类占了绝大多数题目,只要把这几个套路吃透,中等难度的Web题基本能拿下。而且Web题的调试过程是可视化的,改个参数看回显,反馈非常快,适合新手建立信心。
Crypto的门槛在于数学。不是说要你数学多好,而是很多题目需要你能看懂RSA的数学结构、知道格基约减大概在干什么、能识别出这是AES的哪种模式误用。入门阶段的Crypto题其实很套路,脚本一抄就能出,但往上一档就会突然变难。
Reverse和Pwn放在最后说,是因为它们共享同一个底层知识体系——你需要理解程序在内存里长什么样。Reverse是静态和动态地分析程序逻辑,Pwn是利用程序的内存错误。这两个方向的前期投入很大,需要熟悉汇编、调试器、内存布局,但一旦跨过门槛,后续的成长曲线反而比Web更平滑,因为底层知识是通用的,不像Web那样每年都有新框架新漏洞。
我的建议是:新手第一场比赛主攻Misc和Web,把能拿的分全拿到;第二场开始接触Crypto的简单题;Reverse和Pwn放在有了一定调试基础之后再系统学。不要因为Pwn听起来酷就先啃它,容易劝退。
1.3 每个方向的题目长什么样,先建立视觉记忆
这一点很少有人讲,但特别重要。新手看到题目不知道从哪下手,很大一部分原因是"没见过"。你脑子里没有这个方向题目的样子的模板,自然不知道第一步做什么。
Web题通常给你一个网址,或者一段源码,或者一个附件压缩包。打开网址可能是一个登录框、一个文件上传页面、一个显示"Hello World"的页面。给你源码的题,往往是让你审计出漏洞然后构造请求打远程。
Reverse题给你一个可执行文件,或者一个apk,或者一段汇编。运行它,可能提示你输入flag,输错了就退出。你需要逆推出正确的输入。
Pwn题给你一个二进制文件和远程地址端口。本地跑起来可能是一个菜单程序,让你输入名字、选择功能。你需要找到内存溢出点,构造payload拿到shell或者读出flag。
Crypto题给你一段加密脚本和密文,或者只给密文和一个加密方式的描述。你需要还原密钥或者直接解密。
Misc题五花八门,可能是一个图片、一个压缩包、一个pcap流量包、一段音频、一个二维码、一串奇奇怪怪的字符。
这五种形态你先在脑子里过一遍,比赛时看到题目就不会慌,因为你已经知道"这类东西大概要往哪个方向想"。
2. Web题:新手最容易在入口处就走进死胡同
Web题是很多人入门CTF的第一站,也是我见过最多人卡住的地方。卡住的原因通常不是技术不够,而是顺序错了——拿到题目不看功能、不看源码,上来就开扫描器扫目录,扫了半天没结果,然后开始怀疑人生。这一节讲的是怎么把Web题的解题顺序理顺,以及几个高频考点的具体处理方式。
2.1 拿到Web题的前五分钟该做什么
我的固定流程是这样的,顺序不能乱:
第一步,把题目给的地址打开,把所有能点的按钮、能填的表单、能访问的链接都过一遍。这一步的目的是搞清楚这个应用对外暴露了哪些功能点。登录、注册、上传、查询、留言、下载,每一个功能点都是一个潜在的入口。很多题目的关键线索就藏在某个不起眼的页面里,比如一个robots.txt,或者一个页脚写着版本号。
第二步,看源码。不是看服务端源码(通常拿不到),而是看浏览器里的HTML、JS文件。很多题会把提示写在注释里,比如<!-- 备份文件在/backup.zip -->这种。JS文件里也经常藏着接口路径、加密逻辑、参数校验规则。F12打开开发者工具,把Network面板里的所有请求过一遍,看看有没有隐藏的API。
第三步,看响应头。Server头会告诉你这是什么中间件,比如nginx、Apache、Tomcat。X-Powered-By会告诉你后端语言,比如PHP、Express。这些信息决定了你后面该试哪些漏洞——PHP的题优先想反序列化和文件包含,Java的题优先想反序列化和SSTI,Node.js的题优先想原型链污染。
第四步,才轮到目录扫描。而且扫描的前提是你在前三步里发现了"可能有东西但访问不到"的迹象,比如页面报404但路径结构很规律,或者提示里提到了某个不存在的文件。无脑扫目录是最低效的做法。
第五步,如果是给源码的题,直接读代码。读代码的顺序是:先看路由(哪些URL对应哪些处理函数),再看入口(有没有统一的过滤或鉴权),最后逐个看处理函数里的危险操作(拼SQL、读文件、执行命令、反序列化)。
一个很实用的习惯:把题目地址、发现的接口、可疑参数都记在一个文本文件里。Web题的线索是网状的,不记下来很容易乱。
2.2 高频考点和对应的构造思路
Web题的考点集中度非常高,我统计过自己做过的题目,大概八成落在下面这几类里。
SQL注入。识别特征是参数直接进查询,回显异常或者有报错。最基础的判断是输入单引号看是否报错,输入and 1=1和and 1=2看页面是否不同。现在的题目多数有过滤,需要绕WAF,常见手法是大小写混写、内联注释、编码绕过。盲注的话写脚本跑,别手工。
# 布尔盲注的通用框架,改一下判断条件和取值逻辑就能用 import requests url = "http://target/index.php" result = "" for i in range(1, 50): low, high = 32, 126 while low < high: mid = (low + high) // 2 payload = f"1' and ascii(substr((select database()),{i},1))>{mid}-- -" r = requests.get(url, params={"id": payload}) if "正常回显的特征" in r.text: low = mid + 1 else: high = mid if low == 32: break result += chr(low) print(result)文件包含与路径穿越。特征是参数里出现文件名,比如?page=about.php。尝试替换成../../../../etc/passwd看是否能读出内容。如果能包含但读不出系统文件,可能是协议限制,试试php://filter/convert.base64-encode/resource=index.php把源码读出来。这类题的突破口往往是读源码,而不是直接拿shell。
反序列化。特征是源码里出现unserialize()、readObject()、pickle.loads()这类调用。这类题的难点不在构造payload,而在找利用链。PHP的题看有没有__destruct、__wakeup这类魔术方法;Java的题看依赖里有没有已知的利用链组件;Python的题看有没有能触发__reduce__的类。构造payload通常要写脚本,因为长度、转义、编码都很讲究。
模板注入(SSTI)。特征是用户输入被拼进模板渲染,常见于render_template_string、freemarker、velocity这类调用。探测方式是在输入框里填{{7*7}},如果回显49,说明模板引擎执行了表达式。接下来就是按引擎类型找对应的命令执行payload。
SSRF。特征是参数里出现URL,比如?url=http://example.com/image.png。尝试把URL改成内网地址,比如http://127.0.0.1:8080/admin,看能不能访问到。很多题会过滤127.0.0.1,用0.0.0.0、[::1]、十进制IP、DNS重绑定等方式绕过。
文件上传。特征是上传接口。绕过方式按难度递进:改Content-Type、改扩展名(php3、phtml、php5)、加特殊字符(shell.php.、shell.php%00.jpg)、改文件头加图片魔术字节、利用解析漏洞。上传成功的判断标准是你能访问到文件并且它被执行了,不是上传返回200就完事。
2.3 工具会用了但不知道什么时候用,这才是真问题
我见过太多人装了一堆工具,Burp、sqlmap、dirsearch、nuclei全都有,但比赛时不知道什么时候该打开哪个。这个问题的根源是把工具当成了答案,而不是手段。
正确的思路是:工具是用来验证假设的,不是用来找假设的。你先通过手工观察形成假设——"这个参数可能有注入",然后用sqlmap去验证;你先看到路径结构规律,然后用dirsearch去验证是否存在备份文件;你先发现请求可以被拦截修改,然后用Burp去重放和改包。没有假设就开工具,等于在没有地图的情况下开车。
举个具体例子。你拿到一个登录页面,看到响应头是X-Powered-By: PHP/7.4,登录失败时返回"用户名或密码错误",登录成功时跳转。这时候你的假设应该是:后端可能是select * from users where username='$u' and password='$p'这种拼接查询。验证方法是在用户名里输入admin'-- -,如果直接登录成功,说明注释掉了密码校验部分。这个验证过程只需要改一个输入框,不需要开任何工具。
再比如你发现页面有一个?file=readme.txt的参数,返回了文件内容。你的假设应该是可能存在本地文件包含。验证方法是把参数改成../../../../etc/passwd,如果返回了系统用户列表,假设成立。接下来才是思考怎么进一步利用——读源码、找日志文件、尝试包含临时文件。
工具的定位是:手工验证成本太高的时候,用工具批量试。比如盲注需要几千次请求才能跑出一个字符,手工不可能,这时候写脚本或者用sqlmap。比如目录扫描需要考虑几百个路径,手工点不过来,用dirsearch。判断标准很简单——这一步如果手工做要超过五分钟,就上工具。
还有一个常被忽略的点:工具的默认配置往往不适用于CTF题。sqlmap默认的payload很多会被WAF拦掉,需要调--tamper;dirsearch默认的字典太大,跑起来太慢,需要用精简字典加上特定的扩展名列表。我在比赛时的习惯是提前准备三套字典:小字典(常见路径,几百条,用于快速探测)、中字典(常见备份文件和配置文件,几千条)、大字典(只在确定有东西但找不到的时候用)。这个准备工作在赛前做,比赛时直接用。
3. Reverse和Pwn:门槛不在技术,在你没建立的内存直觉
这两个方向经常被新手直接跳过,理由是"看不懂汇编"。但我观察下来,真正拦住新手的不是汇编语法,而是缺少一个东西——对"程序在内存里是怎么跑的"这件事的直觉。你脑子里如果有一张图,知道栈是怎么增长的、函数调用时发生了什么、变量存在哪里,那Reverse和Pwn的很多概念会一下子变得直观。
3.1 Reverse的递进路径:从字符串搜索到算法还原
Reverse题的难度是分层的,每一层对应一种不同的分析方法。搞清楚自己在哪一层,比盲目刷题有用。
第一层:字符串直接暴露。最简单的一类题,flag直接以明文形式存在二进制里,或者经过一次简单的编码(base64、十六进制、异或固定值)。处理方法是用strings命令把二进制里的可打印字符串全拉出来,肉眼扫一遍。
strings -a -n 6 challenge | grep -iE "flag|ctf|\{"如果字符串被编码了,就看编码特征。base64的字符串只包含字母数字和+/=,十六进制全是0-9a-f,异或过的通常看不出来但长度和原文一致。这一层的题占比不高,但每年都有,先花两分钟试一下不吃亏。
第二层:逻辑简单但需要动态调试。程序会读取你的输入,经过若干次变换,然后和一个内置的常量比较。处理方法是用调试器(gdb、x64dbg、IDA的调试功能)在比较指令处下断点,看比较的两个值分别是什么。
具体操作:先找到比较的地方。在IDA里按F5反编译成伪代码,找strcmp、memcmp或者手写的逐字节比较循环。在比较指令处下断点,运行程序,输入任意字符串,程序断下来之后看寄存器里的值。如果一边是你的输入变换后的结果,另一边是期望值,那期望值就是你要的目标,反推变换过程就能得到flag。
这一步的关键是"反推"。比如变换是逐字节和0x13异或,那期望值再异或一次0x13就是flag。如果变换是加减乘除,那就逆运算。如果变换涉及查表,那就把表dump出来反查。
第三层:算法还原。程序里实现了一个自定义的加密算法,你需要读代码理解算法,然后写出解密脚本。这一层的难点在读懂控制流,尤其是编译器优化之后的代码,循环展开、内联、跳转表这些会让伪代码看起来很乱。
处理技巧有几个:遇到大的switch语句,多半是状态机或者查表,把跳转表dump出来;遇到大量位运算,警惕是哈希或者CRC;遇到循环里对固定长度数组的操作,多半是分组加密;遇到递归,先看终止条件。实在读不动就动态跑,把中间结果打印出来,对照着猜算法。
第四层:反调试和混淆。程序检测调试器、检测运行时间、代码自修改、控制流平坦化。这一层需要一些对抗技巧,比如打补丁绕过检测、用脚本自动化dump解密后的代码、用符号执行工具辅助求解。新手不建议在这里耗时间,性价比太低。
3.2 Pwn:栈溢出只是入场券,真正的门槛在后面
Pwn题的核心是利用程序的内存错误来控制执行流。入门的栈溢出题确实不难,但很多人在栈溢出之后就走不下去了,因为后面的东西需要更系统的理解。
先讲最基础的栈溢出,把整个链路走一遍,你就知道每一步在干什么。
程序里有个函数,用read(0, buf, 0x100)读取输入,但buf只有64字节。多出来的字节会覆盖栈上的返回地址。函数返回时,CPU会跳到被覆盖的地址去执行。如果我们把这个地址改成某个能拿到shell的函数地址(比如程序里本来就有的system("/bin/sh")或者后门函数),就拿到了shell。
利用脚本用pwntools写:
from pwn import * context.log_level = 'debug' # 本地调试用process,远程用remote p = process('./challenge') # p = remote('target', 9999) elf = ELF('./challenge') backdoor = elf.symbols['backdoor'] # 假设程序里有后门函数 payload = b'A' * 72 # 填充到返回地址,偏移量要实测 payload += p64(backdoor) # 覆盖返回地址 p.sendlineafter(b'name:', payload) p.interactive()这里面最关键的一步是确定偏移量。方法有两种:一种是用cyclic生成有规律的字符串,程序崩溃后看崩溃地址对应字符串的哪一段;另一种是在gdb里看栈布局,直接数。第一种方法更通用:
from pwn import * payload = cyclic(200) # 让程序崩溃,看报错里的地址,比如 0x6161616c offset = cyclic_find(0x6161616c) print(offset)拿到shell之后,有时候题目不给直接的shell,而是要求你执行cat flag。这时候如果程序里有system函数但没有/bin/sh字符串,就需要自己往内存里写/bin/sh,或者用ROP链调用execve。
栈溢出之上是ROP(返回导向编程)。当程序开启了NX保护(栈不可执行)时,你不能直接跳到栈上执行shellcode,只能把已有的代码片段(gadget)串起来用。找gadget用ROPgadget或者pwntools自带的ROP模块:
rop = ROP(elf) rop.call('write', [1, elf.got['read'], 8]) # 泄露read的真实地址 rop.call('main') # 返回主函数再打一次泄露libc地址是ROP题的标准套路,因为你需要知道libc的版本才能算出system和/bin/sh的偏移。算出之后第二次发送payload,先返回到main,再构造system("/bin/sh")。
再往上是堆利用。堆题的难度是断崖式上升的,因为它涉及到内存分配器的内部结构。你需要理解chunk的布局、bins的分类、free之后发生了什么。入门堆题通常是最简单的UAF(释放后使用)或者double free,进阶的是House of系列。
我给新手的建议是:栈溢出和基础ROP必须会,这是Pwn的及格线;堆利用可以放到后面,先把栈上的东西打熟。
3.3 二进制题上三个典型的误区
误区一:拿到二进制先扔进IDA看伪代码。伪代码是编译器视角的产物,很多时候它比汇编更难读。正确的顺序是先跑起来看行为,再用strings看线索,再用ltrace/strace看库调用和系统调用,最后才进IDA。行为观察能告诉你这个程序大概在干什么,比硬读代码效率高得多。
误区二:不确认保护机制就开始写exp。拿到Pwn题的第一件事应该是checksec,看开了哪些保护。开了Canary就要想办法泄露canary或者绕过;开了PIE就不知道代码基址,需要先泄露;开了Full RELRO就不能改GOT表。这些保护直接决定了你的利用思路,不看就写exp等于蒙眼开车。
checksec --file=./challenge # 或者用pwntools里的 python3 -c "from pwn import *; print(ELF('./challenge').checksec())"误区三:本地能通就直接打远程。本地和远程的libc版本经常不一样,本地算出来的偏移在远程上会偏移,导致exp打不通。正确的做法是先泄露远程的某个函数地址,确定远程libc版本,再用对应的偏移。或者用题目提供的libc文件。
有个细节很容易踩:栈溢出题里的偏移量在本地和远程是一样的,但如果涉及栈对齐(比如调用
system时要求16字节对齐),本地能跑远程可能崩溃。遇到这种情况在payload里加一个retgadget调整栈就能解决。
4. Crypto和Misc:看起来最杂,其实最讲套路
这两个方向放在一起讲,是因为它们的共同点是"套路感强"。你不需要临场创造,只需要识别出这是哪一类问题,然后套对应的解法。识别能力比数学能力更重要。
4.1 Crypto:先识别题型,再动手推导
Crypto题的解题流程是固定的三步:识别加密方式、找到弱点、写脚本求解。绝大多数人卡在第一步,因为题目不会告诉你用的是什么算法。
识别的方法靠特征。给你一段密文,先看长度和字符集。全是可打印ASCII的,可能是古典密码或者异或;只包含十六进制字符的,可能是AES的密文或者大整数的十六进制表示;看起来像base64的,先解一下试试。给你一段代码的,直接读代码里的函数名和常量,pow(m, e, n)就是RSA,AES.new(key, AES.MODE_ECB)就是AES的ECB模式。
我列一下常见题型的识别特征和攻击方向:
| 题型特征 | 可能的问题 | 攻击方向 |
|---|---|---|
| 给了n、e、c三个大整数 | RSA | 分解n、小指数攻击、共模攻击 |
| e=3,密文很短 | RSA小指数 | 直接开三次方 |
| 两个密文共享同一个n | RSA共模 | 扩展欧几里得求gcd |
| 密文长度是16的倍数 | 分组加密 | 看模式、看padding |
| ECB模式下相同的明文块产生相同密文块 | ECB模式 | 字节翻转、块重排 |
| 给了随机数生成器的输出 | 伪随机数预测 | 分析种子或线性关系 |
| 大量密文对 + 明文对 | 已知明文攻击 | 分析加密函数的线性性 |
RSA是Crypto题里的绝对主力,我拿小指数攻击举个例子,把完整流程走一遍。
题目给了n、e=3、c,且m很短。RSA的加密是c = m^e mod n。如果m^e < n,那模运算就没有起作用,c就是m^3本身。直接对c开三次方就能拿到m。
from gmpy2 import iroot from Crypto.Util.number import long_to_bytes n = 0x... # 题目给的 e = 3 c = 0x... # 题目给的 m, exact = iroot(c, e) if exact: print(long_to_bytes(int(m)))如果不是完全立方,说明m^e稍微超过了n,可以尝试c + k*n对每个小的k开三次方。这个技巧叫"低加密指数广播攻击"的变体,写个循环就能搞定。
再举一个AES的例子。题目用ECB模式加密,你可以控制明文的一部分,目标是解出未知部分。ECB的特性是相同的明文块加密后得到相同的密文块,块与块之间没有关联。利用方法是通过控制输入长度,让未知内容刚好落在一个块里,然后逐字节爆破。
# ECB字节逐字节恢复的核心思路 # 1. 构造输入,让目标字节成为某个块的最后一个字节 # 2. 枚举这个字节的256种可能,看哪个产生的密文块和真实的一致 # 3. 逐个字节往前推 def recover_byte(oracle, known, block_size=16): # 补齐,让目标字节位于块末尾 pad_len = block_size - 1 - (len(known) % block_size) prefix = b'A' * pad_len target_block_idx = len(known) // block_size for guess in range(256): payload = prefix + known + bytes([guess]) cipher = oracle(payload) block = cipher[target_block_idx*block_size:(target_block_idx+1)*block_size] # 和真实密文对应的块比较 ...这类攻击的关键在于理解"块"的概念。很多新手写脚本时卡在偏移计算上,原因是没有在纸上画出块的对齐关系。我建议遇到分组加密的题,先在纸上画格子,把每个字节落在哪个块、哪个位置标清楚,再写代码。
Crypto还有一个隐性的门槛是脚本能力。你需要熟练用sage做有限域运算、用gmpy2做大整数开方和求逆、用pycryptodome做对称加密。这些库的常用函数就那么几十个,提前过一遍,比赛时不会因为查文档浪费时间。
4.2 Misc:体力活背后的工程化方法
Misc题被很多人当成"垃圾题",因为它的题目五花八门,看起来没有体系。但实际上Misc是最考验工程化习惯的方向,因为它的解题过程往往涉及大量的重复操作,能不能把这些操作脚本化,直接决定了你在比赛里能不能快速拿分。
Misc的常见子类型有这几类:
编码与转换。给一串莫名其妙的字符,需要识别出它是什么编码。常见的坑有:base64的变种(换字符表、加盐)、十六进制和ASCII的互相转换、URL编码、Unicode编码、摩斯码、培根密码、栅栏密码。处理方法是用CyberChef这类工具手动试,试出规律后写脚本批量处理。判断依据是字符集和长度——base64长度一定是4的倍数,十六进制只含0-9a-f,摩斯码由点和划组成。
文件格式分析。给一个打不开的图片、一个损坏的压缩包、一个改了扩展名的文件。处理方法是用file命令看真实类型,用binwalk看有没有隐藏的嵌套文件,用十六进制编辑器看文件头尾是否被篡改。
file unknown_file binwalk -e unknown_file # 看文件头,正常PNG是 89 50 4E 47 xxd unknown_file | head -5图片隐写。图片里藏了信息,可能是LSB隐写、EXIF信息、附加数据、调色板隐写。处理顺序是:先看EXIF(exiftool),再看文件末尾有没有附加数据(binwalk或者直接看文件大小是否异常),再用zsteg跑一遍LSB,最后考虑频域隐写(这个用工具stegsolve看各个通道)。
流量分析。给一个pcap文件,需要从中提取出传输的数据或者找到异常流量。处理工具是Wireshark和tshark。先用Wireshark打开看协议分布(统计→协议分级),再看有没有可疑的流(跟踪TCP流)。常见的考点是:从HTTP流里提取上传的文件、从DNS查询里提取隐蔽传输的数据、从ICMP包里还原被拆分的数据。
# 用tshark快速提取所有HTTP请求的URI tshark -r capture.pcap -Y "http.request" -T fields -e http.request.uri # 提取所有DNS查询名 tshark -r capture.pcap -Y "dns.qry.name" -T fields -e dns.qry.name | sort -u # 导出所有HTTP对象 tshark -r capture.pcap --export-objects http,./exported内存取证。给一个内存镜像,需要从中找到进程、密码、文件。工具是Volatility。常用命令是volatility -f image.raw imageinfo确定profile,然后用pslist看进程、filescan找文件、hashdump导密码。这类题在Misc里属于偏难的,但套路极其固定,把几个常用插件跑一遍基本就有线索。
压缩包和密码。给了加密的压缩包,需要破解密码或者绕过。处理思路是:先判断是真加密还是伪加密(ZIP的加密标志位被改),伪加密直接改标志位就能打开;真加密就试字典和暴力破解,用fcrackzip或者john;如果是已知明文攻击(有部分明文),用bkcrack。RAR和7z的处理类似,注意不同格式的伪加密标志位位置不同。
Misc的工程化习惯体现在几个方面。第一,建一个自己的工具脚本库,把常用的编码转换、文件头修复、批量提取写成函数,比赛时直接调用。第二,养成"先看后猜"的习惯,不要一上来就上工具,先用file、xxd、strings这些基础命令看一遍,很多题看一眼就知道是什么套路。第三,遇到需要大量重复操作的题,立刻停下手工,写脚本。手工做十遍的时间足够你写一个脚本了。
Misc题的flag经常藏在你想不到的地方。养成"全都要看"的习惯:文件属性、文件末尾、图片的每一个通道、压缩包的注释、流量的每一个协议层。我做过一道题,flag藏在PNG的tEXt块里,用
exiftool一眼就能看到,但很多人直接跳过了这一步去跑隐写工具。
5. 把题型认知变成实战能力:训练安排和场上策略
把五个方向的题型都过一遍之后,真正的问题变成了:怎么练、怎么在比赛里分配时间、怎么复盘。这三件事决定了你的水平提升速度,比多刷几道题重要得多。
5.1 选题策略:难度、方向、时间的三角平衡
一场比赛开始后,第一件事不是挑一道题猛做,而是花五到十分钟把所有题目浏览一遍。看什么?看题目描述、看附件类型、看分值、看通过人数。通过人数是最有价值的信号——一道300分的题如果已经有50支队伍做出来,说明它不难,只是你还没找到思路;一道100分的题如果只有3支队伍做出来,说明它可能有坑,先放着。
浏览完之后,按"低分高通过率"的顺序做。先把所有简单题扫一遍,拿到基础分。这个过程要快,每道题给自己设一个时间上限,比如15分钟没思路就跳过。新手最容易犯的错是在一道题上死磕两个小时,最后其他简单题都没时间做,分数反而低。
具体的优先级排序,我自己的习惯是这样:
第一优先级是Misc和Web的简单题。这两类题的入门题通常十几分钟能出,先拿稳。
第二优先级是Crypto的简单题和Reverse的简单题。Crypto的简单题往往是一个经典攻击,脚本一抄就出;Reverse的简单题是strings一把梭或者简单调试。这两类题的简单题性价比也很高。
第三优先级是中等难度的Web和Misc。这些题需要一定的分析时间,但套路明确,做出来的概率不低。
最后才考虑Pwn和Reverse的中等题、以及各方向的难题。这些题的前期投入大,回报不确定,放在最后做。
时间分配上,如果比赛是8小时,我的分配大概是:前1小时扫题和做简单题,中间4小时攻中等题,最后2小时集中攻一两道有把握的难题,最后1小时留出来整理flag和检查。这个分配不是死的,要根据场上榜单动态调整——如果发现某个方向被大量队伍拿下,说明这个方向的题不难,应该优先投入。
5.2 赛后复盘才是真正涨水平的环节
比赛结束后的48小时是学习的黄金窗口。这个时间点你对题目的印象还新鲜,看writeup的效率最高。错过这个窗口,题目就变成了陌生题,学习效果大打折扣。
复盘的正确做法不是"看一遍writeup就完事",而是分三步走。
第一步,把自己做出来的题重新梳理一遍。重点不是重复操作,而是问自己:我是怎么想到这个思路的?中间走了哪些弯路?如果下次遇到类似的题,我能不能直接跳到正确路径?这个反思的过程会在你脑子里建立"题型-思路"的映射,比单纯积累经验更有效。
第二步,把没做出来的题按"差什么"分类。差工具、差知识点、差思路,这三类的补法完全不同。差工具就去装、去练;差知识点就去补对应的基础,比如不了解反序列化的原理就去读相关文档;差思路就需要看writeup里的思考过程,看别人是怎么从题目信息推导到攻击点的。第三类最难补,但价值也最高。
第三步,动手复现。看着writeup把exp敲一遍,跑通,然后关掉writeup自己再写一遍。这一步很多人跳过,觉得浪费时间,但实际动手和看懂了之间的差距非常大。我见过太多人看writeup觉得"就这么简单",自己写的时候卡在环境配置、版本差异、偏移计算上。
复现的时候有个技巧:不要在原来的目录里复现,新建一个干净的目录,重新下载环境和附件,模拟真实的解题环境。这样能暴露很多你平时忽略的细节问题,比如依赖没装、脚本路径写死、工具版本不同的行为差异。
5.3 队伍分工和场上协作的几个实际问题
如果打团队赛,分工方式直接决定了整体效率。常见的分工有三种:按方向分、按难度分、动态分配。
按方向分是最常见的,一个人负责Web,一个人负责Pwn,一个人负责Misc。优点是每个人在自己的领域里能深入,缺点是容易不均衡——某个方向的题多,那个人就忙不过来,其他人闲着。
按难度分是所有人都看所有方向的简单题,难题再按方向分给专人。这种分法适合新手队伍,能保证基础分拿满。
动态分配是最灵活的,没有固定分工,谁有思路谁上,做不出来就换人。这种分法对队员的沟通能力要求高,需要有一个人做协调,知道每道题的进展,避免重复劳动。
我在实际队伍里比较推荐"主方向+副方向"的模式:每个人有一个主攻方向和一个副攻方向,主攻方向的题优先做,主攻方向没题或者卡住的时候去副攻方向帮忙。这样既能保证深度,又不会出现大量闲置。
协作上有几个实际问题要提前约定好:
flag提交要统一。指定一个人负责提交,其他人做到题之后把flag发给他。避免多人重复提交,也方便统计进度。
思路要共享。做不出来的时候主动喊人来看,别人可能一眼就看出问题。但要约定一个规则——不要在被求助的人正在攻题的时候打断他,除非是紧急情况。
脚本和工具要共享。建一个共享目录,把每个人写的脚本放进去,别人可以直接复用。这个习惯在攻防赛里是必须的,在解题赛里也能省很多时间。
场上遇到争议怎么办?比如两个人对同一道题有不同的思路,都觉得自己对。这时候不要争论,两个思路并行做,谁先出结果算谁的。争的时间足够你们各试一遍了。
6. 写在最后的一点个人体会
打了这么多场比赛,我最大的感受是:CTF的题型是固定的,但人的成长路径不是。有人从Misc入门,三个月就能在Web上稳定拿分;有人在Pwn上死磕半年才开窍,之后一路突飞猛进;也有人什么都学一点,最后靠广度在比赛里稳定拿分。没有哪条路是标准答案。
如果非要给一个建议,我的建议是:先把Misc和Web的基础分吃透,保证每场比赛都有稳定的分数产出,这个正向反馈能让你坚持下去。然后在某一个方向上做深度,做到看到题目就能判断"这题我能不能做、大概要多久",这种判断力比多会一个漏洞更有价值。
还有一件事想提醒:别把CTF当成安全能力的全部。CTF的题目是人为设计的,条件比真实环境干净得多,很多时候你只需要在特定条件下打穿一个点。真实环境里的系统有复杂的业务逻辑、有历史遗留的代码、有各种中间件和网络架构,攻击面远比CTF题宽,但也远比CTF题难利用。CTF练的是拆解问题的思路和动手能力,这两样东西是通用的,但具体的技术细节要随时更新。
我自己的习惯是每打完一场比赛,把做出来的题对应的知识点写一个小小的笔记,记录"这类题的识别特征、常用工具、容易踩的坑"。这个笔记现在有几百条了,比赛前翻一遍,比看任何教程都管用。你也可以从今天开始建自己的这份笔记,第一场可能只有三条,第二场就有十条了。