我第一次用 checksec,是跟着一篇 PWN 入门 writeup 抄作业的时候。对方在漏洞分析之前贴了一张终端截图,红红绿绿的英文缩写排成一行:RELRO、STACK CANARY、NX、PIE……我心想这玩意儿不就是读一下 ELF 的段属性吗,readelf 也能干,至于单独装个工具?后来在 ctfshow 的 pwn 题里连续卡了几次才明白,checksec 真正值钱的不是那几行输出,而是它把“这个程序开了哪些安全保护”翻译成了“这个程序应该怎么打”的决策语言。尤其对刚入门 PWN、在 pwn 溢出题里被 NX、PIE、canary 绕晕的朋友,checksec 就是你拿到题目后第一眼该看的说明书。
这篇文章不写那种“下载下来跑一下就行”的废话。我会把最新版 checksec 的完整安装流程、依赖问题、日常用法和自动化技巧都过一遍,重点是讲清楚怎么解读输出、怎么把结果映射成攻击路线,最后附上我在实际使用中踩过的坑。内容同时覆盖入门和进阶两个层次,有经验的朋友可以直接跳到第 4、5 节看进程检测和自动化集成部分。
1. 别把checksec当摆设:它映射的是整个攻击面
1.1 一条命令背后到底检查了哪七件事
先说清楚工具的本质。checksec 是一个用 Bash 写的脚本,它本身不做底层解析,而是调用系统里的 file、readelf、objdump 这些 binutils 工具,从一个 ELF 文件的程序头、动态段、符号表里提取安全属性,再汇总成一个表格给你看。所以它不是一个“检测漏洞”的工具,而是一个“读取二进制安全配置”的工具。理解这一点很重要:checksec 的输出不是最终结论,而是你制定利用策略时的输入参数。
新版本的 checksec 主要检查以下几项,每一项我都用一句话说明它对漏洞利用意味着什么:
- RELRO:重定位段的只读保护。No RELRO 时 .got 和 .got.plt 完全可写;Partial RELRO 时 .got 只读但 .got.plt 仍可写,所以 GOT 覆写依然可行;Full RELRO 时整个 GOT 在程序启动后只读,GOT 覆写这条路基本堵死。
- STACK CANARY:栈保护值。函数序言里从 fs:0x28 取随机值压栈,返回前校验。没有 canary,你的栈溢出可以直接覆盖返回地址;有 canary,就得先想办法泄露它。
- NX:栈和堆等数据段是否可执行。NX enabled 表示不能直接往栈上丢 shellcode 然后跳过去执行。
- PIE:地址随机化。No PIE 的程序加载地址固定,ROP 链和 gadget 地址可以直接写死;PIE enabled 则需要先泄露基址。
- RPATH / RUNPATH:动态库搜索路径。如果这两个字段指向可写目录,攻击者可能通过替换同名动态库实现劫持,平时做 CTF 用得少,但分析恶意样本时值得看。
- Symbols:程序是否被 strip。有符号表的话逆向和调试会轻松很多,这一列也经常被新手忽略。
- FORTIFY:FORTIFY_SOURCE 编译插桩的情况。Fortified 表示实际被插桩的函数数量,Fortifiable 表示理论上还能插桩的数量。如果 Fortified 为 0 而 Fortifiable 很大,说明这程序是在没开 FORTIFY 的情况下编译的。
这些字段单独看都没什么,但组合在一起,就是一个二进制全部的可攻击面。
1.2 从输出到打法:一张组合表
我见过太多新手把 checksec 的输出截图发到群里,然后问“这题怎么做”,但其实答案往往就藏在这张表里。我根据自己的做题经验整理了一份保护组合和攻击路径的映射,不敢说覆盖所有情况,但对付大部分 pwn 入门题足够了:
| 保护组合 | 典型攻击路径 |
|---|---|
| NX disabled | 直接在栈/堆布置 shellcode,想办法跳过去执行 |
| No PIE + No canary + NX enabled | 固定地址 ROP、ret2libc、ret2plt |
| Partial RELRO + No PIE | GOT 表覆写、ret2dlresolve |
| Full RELRO | 放弃 GOT,转向 __malloc_hook、.fini_array、vtable、FSOP 等 |
| Canary found | 先泄露 canary,再走栈溢出;或利用 off-by-one 逐字节爆破 |
| PIE enabled | 先泄露任意代码段地址,算出基址后按偏移修正 ROP 链 |
注意这张表不是单选题。实际题目通常是多种保护的组合,你需要先找最容易突破的一环。比如一个程序同时开了 canary 和 PIE,但没开 NX,那你依然可以优先考虑 shellcode 方案,因为 canary 和 PIE 都拦不住“跳去栈上执行”;反过来,如果 NX enabled 但没开 PIE,那 ret2libc 就是比 shellcode 合理得多的选择。
1.3 新手最常见的三个误读
我在各个群里解答问题的时候,发现有几个误读出现频率极高,必须单独拎出来说。
第一个误读是“PIE enabled 就不能做 ROP”。这是错的。PIE 只意味着地址随机化,你可以通过格式化字符串、read 后输出、UAF 泄露等任意信息泄漏手段拿到一个代码段地址,然后用这个地址减掉该符号在文件中的偏移,算出程序基址。基址一出来,所有 ROP gadget 地址都能还原。
第二个误读是“canary found 就不能栈溢出”。canary 只是让覆盖返回地址变得更难,不是不可能。比较常见的思路是先通过格式化字符串把 canary 的值读出来,然后在 payload 里原样填回去;或者利用 off-by-one 只覆盖 canary 最低位的 \x00,再逐字节爆破剩余位,配合 fork 子进程不断重试。
第三个误读是只看 NX 和 canary,忽略 RELRO。Partial RELRO 和 No RELRO 之间的区别决定了你能不能改 GOT,而 GOT 覆写是 pwn 溢出题里性价比极高的一条路线。新手如果只盯着 stack canary 看,很容易错过这个明显的突破口。
2. 最新版checksec安装实录:从拉源码到全局可用
2.1 为什么我建议你别用发行版自带的checksec
在 Debian 系的系统上直接 apt install checksec,装出来的是一个很老的版本。这里面的历史原因不复杂:checksec 最早是 Tobias Klein 写的一个脚本,后来社区有人在维护新分支,目前比较活跃的维护仓库是 slimm609/checksec.sh。发行版打包的往往是早期版本,功能上缺了很大一块,比如:
- 不支持 --format=json / csv / xml 结构化输出
- 对新版本 GCC / Clang 产物里的 FORTIFY 检测不准
- 对 LLVM 工具链的兼容性差
- 缺少进程级检查、内核检查、FORTIFY 审计这些后续加进来的功能
最坑的一点是,老版本在一些新 binutils 环境下会把 PIE 或 FORTIFY 判错。你要是拿一个错误的检查结果去定 exploit 策略,那比不检查更危险。所以我一直建议从 GitHub 拉最新源码,而不是图省事用系统源。
2.2 克隆、赋权、自检三步走
安装过程非常简单,因为 checksec 就是一个纯 Bash 脚本,不需要编译,核心就三步:
git clone https://github.com/slimm609/checksec.sh.git cd checksec.sh chmod +x checksec然后先跑一下版本号,确认拉下来的是新版本:
./checksec --version这里顺便解释一下为什么我推荐 git clone 而不是下载 zip 包:checksec 更新还算频繁,你只要保留这个 git 仓库,以后想升级就是一句git pull的事。下载 zip 的话,每次都要重新去网页下载、解压、覆盖,还容易搞混版本。另外,脚本本身不要求 root 权限就能跑,前面这几步都可以在普通用户目录下完成。
2.3 依赖清单:缺什么会在哪一步炸
checksec 虽说是脚本,但它依赖系统里的一堆外部命令。如果你在一个精简环境(比如最小化安装的 Docker 容器)里直接跑,大概率会碰到各种各样的 command not found。先看下面的依赖表:
| 依赖 | 用途 | 缺失时的表现 |
|---|---|---|
| bash 4+ | 脚本本身需要数组和正则特性 | 报语法错误或 declare -A 相关报错 |
| binutils(readelf、objdump) | ELF 解析核心 | “readelf: command not found” |
| file | 判断文件类型 | “file: command not found” |
| gcc | --fortify-file 审计 | fortify 相关功能不可用 |
Ubuntu / Debian 下一条命令装齐:
sudo apt update sudo apt install -y file binutils gccmacOS 自带的 bash 是 3.2,跑新版本 checksec 会直接挂,需要先用 Homebrew 装新版 bash:
brew install bash binutils file gcc coreutils装完之后注意确认bash --version是不是 4 以上,如果还是旧版,就需要手动把新版 bash 的路径加到 PATH 前面,或者直接用绝对路径调用:/usr/local/bin/bash ./checksec。
2.4 软链接到全局并验证安装结果
确认脚本能跑之后,我建议把它做成全局命令,这样以后在任何目录下都能直接用:
sudo ln -s "$(pwd)/checksec" /usr/local/bin/checksec这里我用的是软链接而不是把脚本拷贝到 /usr/local/bin,原因和前面用 git clone 一样:以后 gclit pull 更新完,软链接指向的文件自动就是新版本,不用再拷贝一次。如果你不想动系统目录,也可以把 checksec 的路径写进 ~/.bashrc 的 alias,效果差不多。
装完做个自检,拿系统自带的 /bin/ls 试一下:
checksec --file=/bin/ls正常输出会是一个表格,包含 RELRO、STACK CANARY、NX、PIE 等列。如果你发现某一列是空的、或者整个输出只有一行报错,那就回头检查依赖有没有装全。工具本身没问题的话,/bin/ls 这种常规系统程序至少能稳定输出各列结果。
3. 日常用法:读报告、批量扫、接脚本
3.1 单文件体检:逐列看懂输出
拿一个典型的 CTF 入门题二进制举例,假设名字叫 pwn,跑出来是这样:
$ checksec --file=pwn RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 72 Symbols No 0 2 pwn这个输出信息量很大,我建议你养成习惯,按列读一遍,读的同时在脑子里过一遍攻击思路:
第一列Partial RELRO:GOT 的只读保护不完整,.got.plt 仍然可写,说明后续做 GOT 覆写是可行的。第二列No canary found:栈溢出可以肆无忌惮地覆盖返回地址,不需要考虑 canary 泄露和重放。第三列NX enabled:栈不可执行,别考虑 shellcode 方案,老老实实走 ROP。第四列No PIE:程序加载基址固定,所有 gadget 和函数地址都能直接写死。五、六列 RPATH/RUNPATH 都是 No,排除了动态库劫持这条不常见的路。第七列 Symbol 有 72 个,说明这份二进制没被 strip,逆向起来会很舒服。
四列重点读下来,这道题的大方向其实已经出来了:固定地址 ROP,优先考虑 ret2libc 或 GOT 覆写。这就是 checksec 的价值——你不是在“看参数”,你是在“选题目的最优解”。
3.2 目录级批量扫描:一次看完整套题目
有时候你从某个平台一次性下载了几十个题目二进制,或者在做样本分析时拿到一个包含大量程序的目录,这时候单个文件去跑就很低效。checksec 提供了目录级的扫描方式:
checksec --dir=./challs/这个命令会遍历目录下的所有 ELF 文件并依次输出检查结果。我自己的习惯是拿它给题目分类:先扫一遍,把 No canary + No PIE 这类“软柿子”标记为优先练习对象,把 Full RELRO + PIE + canary 全开的留到后面慢慢啃。批量输出里文件很多时,可以直接配合 grep 过滤:
checksec --dir=./challs/ | grep -E "No canary|No PIE" | grep "No PIE"这样能快速筛出符合特定保护组合的目标。不过要注意,--dir 扫描的是目录下的文件而不是递归所有子目录,如果你的题目是按子目录组织的,需要自己在外面套一层 find 循环:
find ./challs -type f -name "pwn*" -exec checksec --file={} \;3.3 JSON 输出:让脚本替你做决策
从维护版本开始,checksec 支持结构化输出,这个功能在自动化场景下太好用了。跑法很简单:
checksec --format=json --file=pwn > pwn.json生成的 JSON 大致长这样,具体的字段名以你实际版本为准:
{ "pwn": { "relro": "partial", "canary": "no", "nx": "yes", "pie": "no", "rpath": "no", "runpath": "no", "symbols": "yes", "fortify_source": "no", "fortified": "0", "fortifiable": "2" } }有了 JSON,你就可以用 jq 或者 Python 批量处理。比如一次性扫描一个目录下所有题目,输出哪些是 No PIE 的:
checksec --format=json --dir=./challs/ | jq 'to_entries[] | select(.value.pie == "no") | .key'如果你更习惯处理表格数据,还有--format=csv和--format=xml可选,用--output=result.csv可以直接把结果写入文件。我自己写自动打题脚本的时候,会在第一步调用 checksec 的 JSON 输出,根据返回的保护组合决定走 ret2libc 还是 shellcode 分支,这让脚本在不同难度题目之间的通用性提高了不少。
4. 进阶功能:进程级检查与FORTIFY审计
4.1 对正在运行的进程做体检
除了静态分析文件,checksec 还能对正在运行的进程做检查,这在分析服务器程序或者应急响应场景里特别实用。相关参数有三个:
checksec --proc=nginx checksec --proc-all checksec --proc-libs=1234--proc指定进程名,--proc-all检查当前系统所有可访问的进程,--proc-libs后面跟进程 PID,可以进一步检查该进程加载的动态库的保护情况。原理其实不复杂:脚本通过 /proc/ /exe 读取对应二进制的路径,然后对那个文件做 ELF 解析。
这个功能在 CTF 里的一个典型用法是:题目已经启动了漏洞进程,但只给你一个 netcat 连接,没有给本地二进制。这时候你如果能在目标机器上执行命令(通常拿到简单 shell 后),就可以用checksec --proc直接看实际运行的二进制保护情况,不用再去翻文件系统找 ELF。需要提醒的是,对 /proc 的访问权限受用户限制,跨用户查看别人的进程通常会失败,这在 Docker 容器场景尤其明显。
4.2 FORTIFY审计:从编译插桩到运行验证
FORTIFY_SOURCE 很多人不重视,但它是现代 Linux 程序防御体系里很关键的一环。简单说,编译器在开启-D_FORTIFY_SOURCE并且优化级别在 O1 以上时,会把某些危险函数调用替换成带边界检查的__*_chk版本,比如 memcpy 变成 __memcpy_chk,在运行时会校验目标缓冲区大小。checksec 可以用--fortify-file参数审计一个二进制的插桩情况:
gcc -O2 -D_FORTIFY_SOURCE=2 -o prog prog.c checksec --fortify-file=prog输出里会给出 Fortified 和 Fortifiable 两个数字。前者表示二进制里实际出现多少个带 _chk 后缀的安全函数,后者表示通过静态分析还能替换多少个。如果 Fortified 是 0 而 Fortifiable 很大,说明编译参数里没有开 FORTIFY,这类程序在面对溢出类漏洞时会少一层拦击。对出题人来说,这个功能也能用来确认题目是否意外被插桩了——有些在线编译器默认开了 FORTIFY,出的题会比预期难打很多。
对应的还有--fortify-proc=<PID>,检查运行中进程的 FORTIFY 状态,适合配合前面的 --proc 一起用。
4.3 接入 pwntools 和调试器,让检查结果直接驱动脚本
做 pwn 题的人电脑上基本都有 pwntools,它自己也带了一个 checksec 实现:
pwn checksec ./pwn输出格式是 pwntools 风格的 summary,比如 Arch、RELRO、Stack、NX、PIE 等。pwntools 的好处是,它不光是显示给人看的,还能在 Python 脚本里直接读取:
from pwn import * e = ELF('./pwn') if e.checksec.nx: log.info('NX enabled, going ret2libc') else: log.info('NX disabled, shellcode possible') if e.checksec.pie: log.warning('PIE enabled, need a leak first')我写 exploit 前会先用它做一次环境判断,让脚本根据保护组合自动选策略。比如同一个模板脚本,No PIE 的时候 gadget 地址直接写死,PIE enabled 的题目则要求先从题目进程里泄露出基址再拼接 ROP 链。
如果你平时用 pwndbg 或 GEF 调试,它们加载程序后也会自动显示 checksec 信息,省去手动敲命令。但要注意,这些调试器内建的 checksec 和独立版 checksec 的检测逻辑并不完全一样,个别情况下结果会打架。遇到这种情况不用慌,优先相信独立版的结果,再用 readelf 核对原始数据,具体核对方法我在最后一节会讲。
5. 实战:拿到题目后checksec怎么帮你定打法
5.1 四类典型保护组合的攻防思路
下面这四类组合,覆盖了我在 ctfshow pwn 入门题和各类新手题里见过的绝大多数情况。
组合一:No canary + No PIE + Partial RELRO + NX enabled
这是最经典的入门栈溢出组合,也是 ret2libc 的标准舞台。因为 NX 开了,不能执行栈上代码;因为没开 canary,溢出时不需要处理栈保护值;因为没开 PIE,ROP 里的 gadget 地址全都可以写死。打法通常是两段式:第一段用 puts@plt 泄露 puts@got 的真实地址,然后回到 main;第二段根据泄露的 libc 地址算出 system 和 /bin/sh 的地址,重新触发溢出,调用 system("/bin/sh")。Partial RELRO 还给了你另一个选择,直接改 GOT 表里某个函数的地址到 system,但入门阶段先掌握 ret2libc 更稳。
组合二:NX disabled
看到这一列是 NX disabled,你第一反应应该是:栈或者堆上能不能放 shellcode?能的话,这就是一道送分题。你需要把 shellcode 写进缓冲区,把返回地址覆盖成栈地址。注意栈地址在 ASLR 下每次都会变,所以通常还要配合一个栈地址泄露,或者在 x86 上用 jmp esp 这类 gadget 直接跳到栈顶。如果题目给了你一个输出缓冲区地址的功能,那 shellcode 方案的成功率会非常高。
组合三:Canary found + PIE enabled + NX enabled + Full RELRO
这组合全开,属于典型的“地狱难度”入门题。看见它你的第一反应不是去硬怼溢出,而是先找信息泄露的入口。格式化字符串漏洞在这个组合下几乎是必用的:通过 %p 或 %n$p 泄露栈上已有的 canary、PIE 基址、libc 地址,然后才能构造 ROP。思路很清晰但步骤多,适合作为进阶练习。
组合四:Partial RELRO + No PIE + 没有可用 libc 泄露
这种题其实也很常见,尤其是那种只给一个没有输出函数的程序,你想 ret2libc 但根本没有泄露的手段。这时 Partial RELRO 反而成了突破口:用 ret2dlresolve 在运行时伪造重定位项,让动态链接器帮你解析任意函数。这个技术对 ELF 重定位机制的依赖很深,但有 checksec 告诉你 Partial RELRO,至少你能第一时间把 ret2dlresolve 放进候选方案池。
5.2 走一遍经典题的读题过程
以我在 ctfshow pwn 入门模块里很常见的一类题为例。拿到二进制后第一件事,跑 checksec:
$ checksec --file=pwn RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 72 Symbols No 0 2 pwn然后正常流程是 file 看架构、strings 看提示、IDA 或 readelf 找漏洞函数。这里假设反编译出来是一个经典的 64 位栈溢出:函数里调用 gets 读入 0x50 字节的缓冲区,没有其他输出函数。结合 checksec 的结果,我能直接列出该题的约束条件:
- No canary,说明溢出可以直接打到返回地址;
- No PIE,说明 main、puts@plt、puts@got 这些地址都是固定的;
- NX enabled,排除 shellcode;
- Partial RELRO,保留 GOT 覆写的可能性。
于是打法确定为:第一阶段调用 puts@plt 打印 puts@got 的内容,泄露 libc 中 puts 的地址,然后返回 main;第二阶段用泄露值减去 libc 里 puts 的偏移,得到 libc 基址,再算出 system 和 /bin/sh 的地址,第二次溢出调 system("/bin/sh")。整个过程里 checksec 的每一列都在帮你排除错误路线,避免你在 shellcode 或者 GOT 覆写这些方向上浪费时间。
5.3 动态容器环境下checksec的边界
现在的 CTF 平台基本都用 pwn 动态容器来分发题目,每个用户一个 Docker 容器。这种环境下 checksec 依然有用,但它有非常明确的边界。
checksec 能告诉你的,是二进制文件本身的安全属性。它不能告诉你的,是运行时的内核防护状态,比如内核有没有开 SMEP、SMAP,KASLR 的实际随机化程度;也不能告诉你容器里 glibc 的确切版本。而这些恰恰是堆题目和内核题目的关键变量。
所以我的建议是:本地用 checksec 定攻击方向,远程环境单独确认 libc 版本。泄露一个 libc 函数地址后,去 libc 数据库比对偏移,或者直接看题目附带的 Dockerfile,把远程镜像 pull 下来本地测,用LD_PRELOAD加载远程同款 libc 来保证本地调试环境和远程的一致性。checksec 解决的是“怎么打”的问题,libc 版本解决的是“地址是多少”的问题,两件事别混为一谈。
6. 踩坑记录:我在安装和使用checksec时遇到的七个问题
6.1 依赖缺失导致的“command not found”
最基础也最常见的坑。在精简容器里跑 checksec,经常弹出来的是:
./checksec: line 58: file: command not found readelf: command not found原因很简单,系统里没装 binutils 或 file。别急着怀疑脚本坏了,先补依赖:
sudo apt install -y binutils file补完再跑,90% 的情况直接正常。剩下的 10% 是 gcc 缺失导致 --fortify-file 无法使用,补装 gcc 即可。
6.2 Windows 下克隆导致 CRLF 行尾符问题
如果你是在 Windows 上用 Git 克隆的仓库,然后放到 WSL 或者 Linux 里跑,很可能报这样的错:
/usr/bin/env: 'bash\r': No such file or directory这是 Git 在 Windows 下默认把文本文件的行尾符转成了 CRLF 导致的。修复也很简单:
sed -i 's/\r$//' checksec更彻底的办法是设置 Git 的 autocrlf,避免以后再遇到同类问题:
git config --global core.autocrlf input这个坑我身边好几个朋友都踩过,看到 “bash\r” 别懵,就是行尾符的问题。
6.3 macOS 自带 bash 3.2 跑不出结果
macOS 上默认的 bash 还是 3.2,而新版 checksec 用了declare -A这类关联数组语法,bash 3 解析到相关代码就会报错。现象是脚本跑一半卡住或者直接退出,报错信息指向数组相关语法。解决办法是装新版 bash:
brew install bash /usr/local/bin/bash ./checksec --version确认新版 bash 没问题后,别忘了改 PATH 或软链接,让全局命令 checksec 也走新版 bash。否则你明明装好了,在别的目录一敲 checksec 还是报错。
6.4 老版本脚本在新 binutils 下误判
我前面强调过不要用发行版自带的老版本 checksec,这里给一个具体场景。在我用 GCC 12 编译的测试程序上,老版本脚本里对 FORTIFY 的检测逻辑会漏判插桩函数,导致 Fortified 显示为 0。还有一些老版本对较新的 ELF 段布局解析不到位,会把 PIE 误判成 No PIE。这类误判在平时可能无感,但你要是基于错误结果去打题,payload 构造方向直接跑偏。建议装完确认一下版本号,确保自己用的是维护仓库的最新版。
6.5 彩色输出在管道解析时捣乱
checksec 默认输出会根据终端类型决定是否加颜色,但你在脚本里管道抓取输出时,颜色控制符会成为解析干扰。最省心的方案是直接用结构化格式:
checksec --format=json --file=pwn或者用--format=csv拿纯文本表格。尽量不要去 sed 抓取默认输出里的关键字,因为不同版本输出列的顺序可能微调过,解析逻辑很容易因为一次升级而失效。
6.6 静态链接和 musl 二进制的特殊表现
checksec 是围绕 glibc 生态设计的,遇到静态链接或 musl 编译的二进制时,部分字段会失真。比如 FORTIFY 检测依赖 glibc 的__*_chk符号,而 musl 编译出的程序里根本没有这些符号,Fortified 自然显示 0,但这不代表程序没用安全编译选项。再比如静态程序没有动态段,RUNPATH 那一列显示 No 也就没太多参考意义。遇到这类二进制,我的建议是 checksec 的结果只做参考,关键字段用 readelf 原始输出二次确认。
6.7 不同工具结果打架时以谁为准
实操中经常出现这种情况:pwndbg 加载程序时显示 RELRO: Partial,独立版 checksec 显示 Full RELRO,pwntools 显示的结果又不一样。三个工具结论不同,不是因为谁有 bug,而是它们判定 RELRO 的“判据”不完全一致。有的看 PT_GNU_RELRO 段是否存在,有的还要看动态段里有没有 BIND_NOW 标志,判据组合不同结果就不同。这时候以 readelf 原始数据为准:
readelf -l ./pwn | grep GNU_RELRO readelf -d ./pwn | grep BIND_NOWPT_GNU_RELRO 存在代表至少是 Partial RELRO,BIND_NOW 存在代表启动时已全部重定位,两者都有才是 Full RELRO。学会这两条 readelf 命令,你不仅能解决工具打架的问题,也能更深入理解 checksec 每个字段背后的判断逻辑。
最后分享一个我自己的使用习惯:拿到任何 pwn 题目,先用 checksec 跑一遍,然后把输出复制到题目的备注文件里。调试的时候如果发现某个利用思路反复失败,回头看一眼 checksec 结果,多半是漏了某一列。比如我已经不知道多少次因为忽略了一行 “Full RELRO”,白写了半天 GOT 覆写 payload。工具简单,但它是整个漏洞利用流程里最不该被跳过的一步。