做PWN题,拿到一个二进制文件,第一步你会做什么?很多人习惯直接丢进IDA按F5看伪代码,但我的习惯是先跑一遍checksec。这个工具两三秒钟输出的一堆字段,直接决定了这道题是开胃菜还是硬骨头。checksec是检查ELF可执行文件安全特性的脚本,在CTF和二进制安全研究里几乎是标配。今天这篇文章,我就把最新版checksec的安装、常用姿势,以及我平时怎么根据它的输出决定解题思路,一次性讲清楚。适合刚入门PWN的选手,也适合那些一直抱着旧脚本没升级的朋友。
1. 认识checksec:PWN入门必须搞懂的第一道门槛
1.1 checksec到底在查什么
checksec的名字由check和security组成,干的活就是检查一个Linux可执行文件开启了哪些安全缓解机制。现代Linux编译器和内核默认会开启不少防护,比如栈保护、地址随机化、只读重定位表等。这些机制本意是提高系统安全性,但在CTF的约定规则下,题目二进制会刻意关闭或保留某些防护,用来控制题目难度,逼迫选手掌握对应的绕过技巧。
所以我常说,checksec不是用来“看”的,而是用来“定策略”的。它告诉你的不是“这个文件能不能打”,而是“这道题应该往哪个方向想”。比如栈上有没有Canary,决定了你是直接溢出还是需要先泄露随机值;PIE开没开,决定了你的ROP链里的地址能不能写死;RELRO是不是Full,决定了GOT表能不能被改写。这些信息在PWN解题前就必须明确,而不是等你把exp写卡壳才回头查。
对刚入门的朋友来说,checksec的另一个价值在于帮你建立“防护意识”。当你看到NX开启时,就会下意识想到栈不可执行,shellcode不能直接放在栈上跑;看到Canary存在时,就会提醒自己注意信息泄露的利用点。久而久之,你对编译选项和二进制内部结构的理解会越来越深,而不只是会背“有canary就绕过”这句话。
1.2 看懂输出字段:RELRO、Canary、NX、PIE与FORTIFY
一个典型的checksec输出长这样,我以Ubuntu 22.04上自带的/bin/ls为例:
$ checksec --file=/bin/ls RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 75) Symbols Yes 5 17 /bin/ls这一行看似密集,其实每一项都有明确指向。
RELRO,全称Relocation Read-Only,控制程序的重定位表是否只读。Partial RELRO意味着GOT表前半部分只读但GOT.plt仍可写,这是比较常见的情况,可以利用GOT覆写来劫持控制流。Full RELRO则把整个GOT都设为只读,代价是程序启动时会做一次完整重定位,性能略降,但攻击面小很多。做PWN题遇到Full RELRO,就要尽早放弃改GOT的想法,把注意力转向堆管理、钩子函数或者FSOP这类路径。
STACK CANARY,即栈保护值。程序在函数的栈帧中放入一个随机值,函数返回前检查它是否被改写。Canary found意味着栈溢出后直接覆盖返回地址的做法会被立刻发现。绕过思路通常是先通过格式化字符串、越界读等漏洞把Canary值泄出来,然后在溢出payload里原样填回。
NX,No-eXecute,把栈和堆标记为不可执行。NX enabled时,栈上的shellcode无法直接执行,必须转为ROP、ret2libc、SROP等基于代码复用的思路。如果NX disabled,同时题目又给了可写可执行的内存段,那直接布置shellcode往往是最快的路径,很多新手入门的栈题就是这种配置。
PIE,Position Independent Executable,地址随机化。PIE enabled表示程序每次加载的基址都不同,代码段、数据段里所有固定的绝对地址都不能在exp里写死。没有PIE时,函数地址、GOT地址都是固定的,ROP链构造起来会轻松太多。现代Ubuntu默认全开PIE,所以CTF里不少老题反而成了“练习固定地址利用”的经典素材。
RPATH/RUNPATH,动态库搜索路径。正常发行版的二进制基本都是No RPATH、No RUNPATH,如果出现RPATH,说明程序会优先从某个固定路径加载动态库,这种场景在CTF里偶尔会结合“伪造同目录libc”来考。
Symbols,符号信息数量。保留符号意味着IDA里能看到函数名,调试起来非常舒服。strip过的程序符号很少,需要靠字符串交叉引用和函数签名去猜。
FORTIFY,FORTIFY_SOURCE编译加固。开启后编译器会对strcpy、sprintf这类危险函数插入边界检查代码,部分溢出利用会失效。这项在CTF里不算最常见的考点,但输出里有Fortified和Fortifiable两组数字时,值得留意哪些函数被加固了。
字段本身不难记,难的是拿到结果后能快速形成判断。我的建议是不要死记硬背,而是每做一道题都先跑checksec,再打开IDA对照着看。看多了,这些字段自然就长在脑子里了。
2. 装对版本:源码安装最新checksec的全流程
2.1 为什么不用apt里的旧版
很多人在Kali或者Ubuntu上直接apt install checksec,装完也能跑,但这里有个坑:Linux发行版仓库里的checksec通常停留在很老的版本,有的还停留在2015年前后的状态。老版本缺少对FORTIFY_SOURCE的检测,不支持JSON输出,对新版内核的安全特性也识别不全,甚至在遇到部分新版编译器生成的ELF时会出现解析错误。
更麻烦的是,有些发行版会把checksec作为pwntools的依赖一起装进来,但那个版本同样可能偏老。如果你刚接触PWN,照着老博客的命令操作,大概率不会报错,但输出的内容和新版有细微差异,等你把脚本写进自动化工具链时,差异就会被放大。
所以我一直建议,要么直接用最新源码编译,要么至少定期从项目仓库拉一次最新脚本。checksec本质上只是一个脚本,更新成本极低,没必要在工具版本上拖后腿。
2.2 环境准备与依赖
checksec.sh本身是bash脚本,运行时依赖readelf、objdump、file、grep等命令。readelf和objdump属于binutils,file是独立的file命令。绝大多数Linux发行版默认都带,但精简容器里可能没有,建议提前确认一下。
# Debian/Ubuntu系 sudo apt update sudo apt install -y binutils file # CentOS/RHEL系 sudo yum install -y binutils file另外,checksec需要bash 4及以上版本。macOS自带的bash一般都在3.2左右,直接用脚本容易报语法不支持,所以如果你是mac用户,建议装完bash再指定/usr/local/bin/bash checksec.sh来执行,或者直接在Linux容器里跑。这个细节很多教程都没提,但实际使用时一旦碰上,排查起来很浪费时间。
如果你打算用--fortify-file之类的功能,最好还装上对应架构的交叉编译工具链。毕竟CTF里经常出现arm、mips架构的题目,本地没有对应binutils时,checksec会报找不到readelf。
2.3 从源码拉取并安装
最新版checksec维护在GitHub仓库slimm609/checksec.sh中。安装方式很简单,把checksec.sh这个单文件下载下来,放到PATH目录即可。
# 方式一:完整克隆仓库 git clone https://github.com/slimm609/checksec.sh.git cd checksec.sh sudo cp checksec.sh /usr/local/bin/checksec sudo chmod +x /usr/local/bin/checksec # 方式二:直接下载单文件 wget https://raw.githubusercontent.com/slimm609/checksec.sh/main/checksec.sh sudo install -m 755 checksec.sh /usr/local/bin/checksec这里我把脚本改名为checksec,去掉了.sh后缀。这样在命令行里敲起来更顺手,而且新版脚本对执行路径没有特殊依赖,整个文件可以直接拷贝。
安装完成后验证版本:
checksec --help正常会输出一长串使用说明,包含--file、--dir、--proc、--kernel、--output等参数。如果你看到的是很早以前的短帮助,说明装的还是旧版。顺便说一句,直接checksec --version在新版里也能看到版本号和构建信息,可以用来快速确认。
在Docker容器里做PWN题的朋友也注意,基础镜像如果用了python:3.x-slim这类精简镜像,读ELF可能缺file命令,拉完脚本记得顺手apt install file。这是我在动态容器题目环境里踩过的真实问题,后面专门展开讲。
3. 实战用法:命令行与Python调用的两套玩法
3.1 命令行高频参数速览
checksec的命令行参数不少,但日常PWN解题真正高频的并不多。我按使用频率列一下:
| 参数 | 作用 | 实际场景 |
|---|---|---|
--file=<路径> | 检查单个ELF文件 | 拿到题目文件后第一件事 |
-f <路径> | 同上,短参数写法 | 同上 |
--dir=<目录> | 批量检查目录下所有ELF | 分析固件、批量扫附件 |
--proc=<PID> | 检查指定进程 | 调试中确认运行态防护 |
--proc-all | 检查所有进程 | 系统排查、验收环境 |
--kernel | 检查内核防护 | 题目涉及内核或模块时 |
--output=json | JSON格式输出 | 写自动化脚本时集成 |
--fortify-file=<路径> | 检查FORTIFY加固细节 | 确认哪些函数被加固 |
单个ELF最常用:
checksec --file=./pwn批量检查一个目录里的所有文件:
checksec --dir=./attachments/在调试器中确认当前进程的防护状态,避免本地和远程环境不一致:
checksec --proc=$(pidof pwn)写自动化批量测试脚本时,JSON输出比纯文本好解析得多:
checksec --file=./pwn --output=json输出大概是这样的结构:
{ "file": { "relro": "full", "canary": true, "nx": true, "pie": true, "rpath": false, "runpath": false, "symbols": true, "fortify_source": false, "fortified": 0, "fortifiable": 5 } }拿到JSON后,你就可以在exp构建脚本里根据防护字段自动选择利用模板。比如检测到canary为true就自动附加一个“先泄露canary”的流程注释,或者检测到PIE开启时自动想办法从某个输出点读取地址。这些自动化虽然不能替代人工分析,但在批量复现多个题目场景时很省事。
3.2 在pwntools里调用checksec
做PWN题绕不开pwntools,pwntools自身也封装了checksec功能,有两种常见用法。
第一种是直接查看已加载ELF的防护信息:
from pwn import * elf = ELF('./pwn') print(elf.checksec())这种方式返回一个有序字典,形式类似:
Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled第二种是读取checksec的原始文本输出:
from pwn import * print(checksec('./pwn'))这两种写法的区别在于,elf.checksec()解析的是pwntools内部通过readelf解析的结果,而checksec()函数会实际调用系统命令。我平时更推荐前者,因为它不依赖外部脚本,在隔绝环境下也能稳定运行。但如果你已经把checksec.sh更新到最新版,checksec()函数会直接调用新版脚本,能拿到比pwntools内置解析更完整的FORTIFY字段。
一个很实用的组合是:在exp开头先打印防护状态,验证自己拿到的文件和出题人预期一致。很多动态容器题目每次连接分配的实例可能不同,先确认再打,能省掉很多“为什么本地能通远程不行”的排查时间。
from pwn import * context.arch = 'amd64' elf = ELF('./pwn') # 开局确认防护 sec = elf.checksec() log.info(f"RELRO: {sec['RELRO']}") log.info(f"Canary: {sec['canary']}") log.info(f"NX: {sec['nx']}") log.info(f"PIE: {sec['pie']}") # 后续利用流程 ...3.3 动态容器题目的使用习惯
现在很多CTF平台提供动态容器,题目给你一个ip:port,背后是出题人用Docker部署的靶机实例。和本地文件不同,远程实例的防护状态和libc版本不一定和附件里完全一致。这时候checksec反而要“反着用”。
我第一次打动态容器题时,天真地以为远程环境就是本地附件编译生的。结果exp本地全通,远程一打就崩。后来才明白,很多动态容器都是套了一个标准模板,libc版本、ASLR开关甚至编译选项都可能不同。所以拿到远程地址后,我养成了一个习惯:先连上去,通过程序的输出判断远程环境和本地附件的差异,同时用checksec确认本地基础版本,再决定要不要更换libc或者调整地址。
举一个具体场景:题目给了libc-2.31.so,本地系统是glibc 2.35。你用本地系统跑exp,puts@got指向的偏移和2.31完全不同。这时必须先checksec --file=./pwn确认防护,再用pwninit或patchelf把题目程序的解释器和libc替换成本地可复现的版本。替换完再次checksec --file=./pwn,确认替换后RELRO、NX、PIE这些关键属性没变,才开始调试。这个流程虽简单,却能规避掉动态容器环境里最让人头大的“环境不一致”问题。
另外,动态容器的远程端口一般只对特定题型开放,连接后可能有时间限制。建议所有需要人工确认的信息都在本地确认完,远程只做最终验证。checksec这种耗时极短的命令放本地跑,别浪费远程连接时间。
4. 看懂结果才能上手:checksec输出与PWN利用策略
4.1 常见防护组合对应的利用节奏
checksec输出是一张“体检报告”,真正的价值在于指导解题方向。我整理了CTF里最常见的几种组合和对应的思考路径,供参考。
组合一:Canary found + NX enabled + PIE enabled + Full RELRO
这是现代Linux默认配置,也是中等以上栈题的常见环境。这个组合下,直接栈溢出覆盖返回地址的行不通,因为Canary会拦;ROP的地址没法写死,因为PIE开启,程序基址随机;GOT不能改,因为Full RELRO。所以这类题的核心突破口几乎都在“泄露”上:先通过漏洞读出Canary,再通过格式化字符串或栈迁移读出程序基址,最后用one_gadget或ROP完成利用。整体节奏是“泄露-计算-构造-发送”,一步都不能跳。
组合二:No canary + NX enabled + No PIE + Partial RELRO
这是入门栈溢出的经典设置。没有Canary,栈溢出可以直接覆盖返回地址;没有PIE,函数地址固定,可以直接构造ROP链;Partial RELRO下GOT可写,还能退回到GOT覆写或者ret2dlresolve。如果你刚入门,遇到这种题目应该感到幸运,它是练习ret2text、ret2syscall、ret2libc的最好素材。
组合三:Canary found + NX disabled + No PIE
NX关闭意味着栈可执行,如果同时能拿到Canary或者题目有现成的信息泄露,就可以在栈上布置shellcode。这个组合比较少见,但出现时往往意味着出题人想考“栈迁移+shellcode”的思路。
组合四:Static + No PIE + No canary
完全静态编译的题目,近期在各类CTF里出现频率不低。没有动态库可以泄露,但也不需要泄露,因为系统调用号固定,syscall地址固定,常用gadget都在程序里。checksec输出里如果看到No PIE、No canary,同时文件大小又很大,大概率是静态链接,可以直接用ROPgadget搜gadget构造execve('/bin/sh', 0, 0)。
组合五:FORTIFY开启时的陷阱
遇到FORTIFY为Yes时,strcpy、sprintf这类函数会变成带边界检查的版本。如果你还在用老套的strcpy溢出,会发现payload发出去程序直接abort。这时候要么找不受FORTIFY影响的函数,比如read、memcpy,要么考虑堆漏洞路径。checksec能提前暴露这个问题,避免你在调试器里怀疑人生。
4.2 从ctfshow pwn 074这类题目看实战
我以ctfshow平台里比较有代表性的栈溢出题型来举例,比如pwn 074这一类偏入门到进阶过渡的题目。这类题通常给出一个编译好的可执行文件,保护设置有一定迷惑性,不是全开也不全关,需要选手自己判断。
假设你拿到题目,第一步:
file pwn checksec --file=./pwnfile告诉你架构和链接方式,checksec告诉你防护。假设输出是:
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 83) Symbols No 0 3 ./pwn这个组合下,PIE没开,函数地址是固定的,GOT可写,但Canary和NX都开着。这意味着你不能直接栈溢出打ret地址,必须先想办法泄露Canary。通常题目会在某个地方提供一个漏洞函数,可能是格式化字符串,也可能是一次越界读。拿到Canary之后,因为No PIE,你可以放心地构造ROP链,调用puts@plt泄露libc地址,再回到main进行第二轮控制,最后执行system('/bin/sh')。
整个流程用checksec串起来就是:Canary提示你要先做信息泄露,No PIE提示你可以放心使用固定地址的ROP,Partial RELRO提示如果中途卡住还能考虑GOT覆写作为备选方案。一个工具的输出直接串起了整道题的解题链。
再说动态容器场景。这类题目在远程跑起来后,你连上靶机,发送payload,程序返回结果。远程系统的libc版本可能和本地不一致,所以泄露libc地址后,建议直接用LibcSearcher或one_gadget适配远程环境。同时检查远程返回的地址是否低于0x7fffffffffff,判断ASLR是否生效。如果远程关闭了ASLR,很多地址可以直接爆破,那就根本不用走泄露流程,直接构造ROP。
4.3 多文件与libc判断
PWN题除了单个可执行文件,经常还会附赠一个libc.so。这时候checksec的价值体现在两个地方。
一是确认目标程序使用哪个解释器。通过file可以看到动态链接器路径:
file ./pwn如果显示interpreter /lib64/ld-linux-x86-64.so.2,而附件里有专门给的libc,你就需要用patchelf --set-interpreter和patchelf --set-rpath把本地复现环境切到目标libc。
二是确认libc自身的安全属性,不过更准确地说,libc的RELRO、NX等通常跟着系统走,你在调试时更需要注意的是libc的版本和符号偏移。checksec在这里更像一个辅助确认工具。我自己的习惯是,打完libc地址后,优先用LibcSearcher或者libc-database匹配偏移,然后回头用checksec输出里的RUNPATH字段确认程序是不是真的会加载附件里的libc。
这个细节很多人忽略。如果程序全局RUNPATH指定了某个目录,动态链接器会优先从那里加载libc,而不是系统默认路径。这时你泄露出来的libc地址可能属于附件libc,而你在本地复现用的却是系统libc,地址当然对不上。先看checksec的输出,能少走很多弯路。
5. 踩坑记录与常见问题速查
5.1 安装与执行报错
问题一:command not found
脚本明明下载了,执行checksec --file=./pwn却说找不到命令。大概率是PATH里没有/usr/local/bin,或者文件没有可执行权限。先确认:
ls -l /usr/local/bin/checksec which checksec如果没有执行权限,补上chmod +x即可。
问题二:bash版本过低
在新版脚本里用到了关联数组和部分新特性,bash 3.x会报错。Linux发行版一般没问题,macOS用户尤其容易踩这个坑。最简单的办法是在Linux容器里运行,或者安装新版bash后调整shebang行。
问题三:readelf not found
精简镜像或最小化安装的系统里没有binutils。装一下就好:
sudo apt install binutils问题四:Python环境里调用checksec()抛异常
pwntools的checksec()函数依赖外部脚本,如果你更新了checksec.sh但没放对路径,或者新旧版本输出格式不兼容,就会报解析错误。建议更新后先手动跑一遍checksec --file=/bin/ls,确认输出正常,再用pwntools调用。
5.2 识别失败与误报
问题一:非本机架构的文件识别失败
本地是x86_64,但题目是arm或mips的ELF,checksec会尝试调用对应架构的readelf,如果没有安装交叉工具链,就会报错或用错误的方式解析。解决办法是安装对应架构的binutils。Debian系:
sudo apt install binutils-arm-linux-gnueabihf binutils-mips-linux-gnu问题二:静态编译的CANARY误报
有些静态编译的二进制会包含Canary相关的符号和代码,checksec可能认为开启了Canary,但实际栈保护逻辑并没有生效或者只影响部分函数。看到输出后要结合逆向分析确认,不能全信工具。
问题三:strip后的符号信息不准确
Symbols字段显示的数字只能说明符号表的多少,不代表程序复杂度。完全strip的二进制符号数可能为0,但功能依然很复杂。不要因为Symbols少就轻视题目难度。
5.3 新旧版本差异与确认
旧版checksec无法显示FORTIFY信息,JSON输出也没那么丰富。如果你在自动化脚本里依赖--output=json,先确认脚本版本支持。新版还增加了对Linux内核配置的检测,checksec --kernel在分析内核题目时很有用。
另外,pwntools内置的checksec()和独立最新脚本在字段命名上略有出入。比如独立脚本输出Canary found,而pwntools的字典键是'canary',布尔值为True。写脚本时要注意键名差异,别照抄一个就用在另一个环境里。
我在实际使用中还有一个体会:checksec不只是在解题开头有用,在打完exp做验证时也很有价值。比如你构造了一个ROP链,但远程始终打不通,回头用checksec --file=./pwn确认一下题目有没有改动、远程是不是换成了更严格的防护,往往能快速定位问题。工具虽小,却贯穿了PWN题从分析、调试到验证的全流程。建议所有做PWN的朋友,都把这个习惯刻进日常流程里。