构建产物安全编译选项指标统计解决方案
1. 功能入口
构建管理 → 构建产物(beta) 页面,展示 ELF 二进制文件的 14 项安全编译选项开启率概览数据。
2. 安全编译选项概览
| 选项名称 | 英文键名 | 安全含义 | 合规判定 |
|---|---|---|---|
| BIND_NOW | bindNow |
立即绑定(全 RELRO),在程序加载时完成所有符号绑定,防止 GOT 表被覆写 | YES = 已开启(合规) |
| NX | nx |
堆栈不可执行(No-eXecute),防止栈区代码注入攻击 | YES = 已开启(合规) |
| PIC | pic |
地址无关代码(Position Independent Code),共享库代码可在任意地址加载 | YES = 已开启(合规) |
| PIE | pie |
位置无关可执行文件(Position Independent Executable),可执行文件加载地址随机化,增强 ASLR 效果 | YES = 已开启(合规) |
| RELRO | relro |
GOT 表只读保护(RELocation Read-Only),防止 GOT 覆写攻击 | YES = 已开启(合规) |
| Run-time Search Path | rpath |
动态库运行时搜索路径,存在时不安全(可能导致恶意库优先加载) | NO = 无不安全路径(合规) |
| Stack Protector | sp |
栈保护(Stack Canary),在函数返回前检测栈缓冲区溢出 | YES = 已开启(合规) |
| Strip | strip |
符号表剥离,移除 .symtab 节,防止逆向工程泄露函数名和地址信息 |
YES = 已剥离(合规) |
| FORTIFY | fortify |
缓冲区溢出检测(-D_FORTIFY_SOURCE=2),编译器/运行时库对固定大小缓冲区函数调用进行检查 |
YES = 已开启(合规) |
| FVISIBILITY | fvisibility |
符号可见性控制(-fvisibility=hidden),隐藏动态库非导出符号,减小攻击面 |
YES = 已开启(合规) |
| FTRAPV | ftrapv |
有符号整数溢出陷阱(-ftrapv),将整数运算替换为安全函数,检测溢出 |
YES = 已开启(合规) |
| Stack Clash | stackClash |
栈冲突保护(-fstack-clash-protection),逐页分配栈空间并立即探测,防止栈与堆冲突(依赖编译器记录段,未开启记录或无记录段返回 N/A) |
YES = 已开启(合规) |
| Stack Check | fstackCheck |
栈空间越界检查(-fstack-check),探测函数序言栈空间分配是否越界(依赖编译器记录段,未开启记录或无记录段返回 N/A) |
YES = 已开启(合规) |
| ASLR | aslr |
地址空间布局随机化,系统级配置(randomize_va_space=2),随机化堆/栈/mmap 基址 |
YES = 已开启(合规) |
⚠️ stackClash / fstackCheck 为什么可能同时显示 N/A:这两个是编译期代码生成选项,产物没有符号级标记,需读取编译器写入的记录段(GCC
-frecord-gcc-switches生成.GCC.command.line、Clang-frecord-command-line生成.llvmcmd)才能确认。当构建未开启这两个记录开关,或产物被strip删除了该非分配节时,这两项返回 N/A(不计入开启率分母)。若两者同时为 N/A,请先在构建配置中添加-frecord-gcc-switches(GCC)/-frecord-command-line(Clang)。
⚠️ rpath 选项的特殊判定:与其他选项相反,
rpath=YES表示存在不安全的动态库搜索路径(不合规),rpath=NO才是合规状态。页面展示的开启率已做反转处理。
页面展示效果如图:


3. 页面展示说明
3.1 概览页面
构建产物页面以列表形式展示每次扫描的概览数据,每行代表一次扫描记录:
| 展示字段 | 说明 |
|---|---|
| 代码仓 | 被扫描代码仓的链接(格式:gitcode.com/owner/repo),来自插件上报的 gitUrl 字段 |
| 流水线名称+运行编号 | 触发扫描的流水线名称(来自 GitCode Runner 注入的 ATOMGIT_WORKFLOW 环境变量),点击可跳转到流水线详情页 |
| 文件 | 被扫描的构建产物压缩包文件名(如 app-1.0.tar.gz),点击可下载产物,如果产物链接已失效或需要认证,将不可下载 |
| 实际扫描项 | 本次扫描实际检测的选项列表(逗号分隔,如 bindNow,nx,pie),来自插件上报的 scanOptions 字段 |
| 14 项选项开启率 | 每项显示为百分比(如 85.00%),totalFiles 为 0 时显示 NA,可点击跳转至详情页 |
| 检测完成时间 | 扫描完成的时间戳 |
⚠️ 一条流水线可能构建出多个产物包,此时会按照构建产物包名展示多条扫描结果数据
3.2 筛选功能
页面支持以下筛选维度(均为模糊匹配,支持任意组合筛选):
| 筛选项 | 说明 |
|---|---|
| 代码仓 | 输入代码仓链接关键字(如 openlibing/openlibing-cicd),筛选匹配的扫描记录 |
| 流水线 | 输入工作流名称,筛选匹配的扫描记录 |
| 文件 | 输入构建产物压缩包文件名关键字(如 app-1.0),筛选匹配的扫描记录 |
| 检测完成时间 | 按检测完成时间起始/截止筛选 |
筛选数据直接从扫描记录表中查询,无需绑定项目。选中任一筛选项后,其余下拉框自动级联过滤可选项。
3.3 排序功能
概览列表支持以下排序方式:
| 排序字段 | 格式 | 示例 |
|---|---|---|
| 检测完成时间 | detectionCompletedAt |
按时间正序/倒序 |
| 指标排序 | {选项键名}.rate |
bindNow.rate、nx.rate、pie.rate 等 |
| 指标子字段 | {选项键名}.totalFiles / {选项键名}.yesCount |
sp.totalFiles、strip.yesCount |
3.4 下钻详情
点击概览行中的开启率数值,可下钻查看该次扫描中每个 ELF 文件的详细检测结果:
| 字段 | 类型 | 说明 |
|---|---|---|
| fileName | String | 文件名 |
| filePath | String | 文件路径 |
| options | Map<String, String> | 14 项安全编译选项的检测结果(YES / NO / N/A) |
下钻弹窗支持:按文件名模糊搜索、分页浏览。
4. 数据配置说明
页面上的安全编译选项数据由 security-compilation-options-action 插件通过流水线扫描构建产物上报。插件已开源,流水线中可直接通过 openlibing/security-compilation-options-action@v1.0.2 引用。
4.1 工作流配置示例
平台支持三种典型使用场景,按前置插件与 security-compilation-options-action 的协作方式区分,用户根据产物来源和存储需求选择其一即可:
场景一:构建产物上传 OBS 持久存储 + 扫描本地产物
将本地构建产物上传到自管 OBS 桶获取持久下载链接,security-compilation-options-action 扫描同一份本地构建产物并将下载链接上报到openLiBing平台用于溯源。适用于需要长期保留产物或跨流水线共享产物的场景:
name: build
on:
workflow_dispatch:
pull_request_target: ## 使用 pull_request_target 确保 PR 流水线始终读取默认分支(master)的最新配置文件
branches:
- "*"
pull_request_comment: ## PR 评论事件:当用户在 PR 中发表评论时触发
types: [created]
comments: ["/build-and-scan"]
branches:
- "*"
permissions:
repository: read
id-token: write
jobs:
build:
name: build
runs-on: ["codearts-hosted", "ubuntu-latest", "x64", "large"]
steps:
- name: Checkout
uses: checkout
with:
ref: ${{ atomgit.event.pull_request.merge_commit_sha || atomgit.event.pull_request.head.sha }}
# 请替换为实际的构建脚本
- name: build
id: build
run: |
echo "===== CMake 构建打包 ====="
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
# 查找主构建产物
ARTIFACT=$(find . -maxdepth 2 -type f \( -name "*.tar.gz" -o -name "*.zip" -o -perm -111 \) | head -1)
if [ -z "$ARTIFACT" ]; then
echo "ERROR: No build artifact found"
exit 1
fi
echo "artifact-path=${ARTIFACT}" >> "$ATOMGIT_OUTPUT"
echo "Found artifact: ${ARTIFACT}"
- name: upload-artifact-to-obs
id: upload-obs
uses: obs-upload
with:
# 请使用实际的端点和 OBS 桶
endpoint: "obs.cn-southwest-2.myhuaweicloud.com"
bucket: "my-package"
# 请使用自管 OBS桶 AK/SK
access-key: "${{ secrets.OBS_AK }}"
secret-key: "${{ secrets.OBS_SK }}"
# 请使用构建步骤的实际输出参数
artifact-path: "${{ steps.build.outputs.artifact-path }}"
# 请替换为实际需要的产物文件前缀
object-prefix: "${{ atomgit.repository }}/${{ atomgit.ref_name }}/${{ atomgit.run_id }}/"
- name: security-compilation-options-action
# 当前插件已上传至开源仓,可直接调用
uses: openlibing/security-compilation-options-action@v1.0.2
with:
artifact-path: "${{ steps.build.outputs.artifact-path }}"
artifact-download-url: "${{ steps.upload-obs.outputs.primary-url }}"
# 可选:只扫描部分项,不传 scan-options 时默认扫描全部 14 项(stackClash/fstackCheck 依赖编译器记录段,未开启记录或无记录段返回 N/A)
# scan-options: "bindNow,nx,pic,pie,relro,rpath,sp,strip"
场景二:构建产物临时暂存 + 扫描本地产物(免管 OBS)
以 GitCode 官方 upload-artifact 作为前置插件,将本地构建产物临时暂存并获取下载链接,security-compilation-options-action 扫描同一份本地构建产物。retention-days 控制产物保留天数,到期自动清理,无需自管 OBS 桶。适用于无需长期保留产物的场景:
name: build-and-scan-nightly
on:
workflow_dispatch:
schedule:
- cron: "0 0 0 * * ?" # 每天北京时间 00:00 执行
permissions:
repository: read
id-token: write
jobs:
build:
name: nightly-build-and-scan
runs-on: ["codearts-hosted", "ubuntu-latest", "x64", "large"]
steps:
- name: Checkout
uses: checkout
# 请替换为实际的构建脚本
- name: build
id: build
run: |
echo "===== CMake 构建打包 ====="
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
# 查找主构建产物
ARTIFACT=$(find . -maxdepth 2 -type f \( -name "*.tar.gz" -o -name "*.zip" -o -perm -111 \) | head -1)
if [ -z "$ARTIFACT" ]; then
echo "ERROR: No build artifact found"
exit 1
fi
echo "artifact-path=${ARTIFACT}" >> "$ATOMGIT_OUTPUT"
echo "Found artifact: ${ARTIFACT}"
# 使用官方 upload-artifact 暂存产物,无需自管 OBS 桶
# retention-days 控制产物保留天数,到期自动清理
- name: upload-artifact
id: upload-artifact
uses: upload-artifact
with:
name: nightly-build-artifact
path: "${{ steps.build.outputs.artifact-path }}"
retention-days: 3
overwrite: true
if-no-files-found: error
# 扫描本地产物文件,artifact-download-url 使用 upload-artifact 输出的制品页面 URL
- name: security-compilation-options-scan
uses: openlibing/security-compilation-options-action@v1.0.2
with:
artifact-path: "${{ steps.build.outputs.artifact-path }}"
artifact-download-url: "${{ steps.upload-artifact.outputs.artifact-url }}"
场景三:下载已有产物 + 扫描下载产物
将预先存储在 OBS 上的产物包下载到本地,security-compilation-options-action 扫描下载的产物文件。适用于产物已由其他流水线构建并上传到 OBS、本流水线只负责扫描的场景。artifact-path 支持多行指定多个文件,插件逐个扫描并独立上报:
name: security-compilation-options-scan-nightly
on:
workflow_dispatch:
schedule:
- cron: "0 0 0 * * ?"
permissions:
repository: read
id-token: write
jobs:
build:
name: scan-artifacts
runs-on: ["codearts-hosted", "ubuntu-latest", "x64", "large"]
steps:
- name: Checkout
uses: checkout
# ===== 批量下载两个架构的构建产物 =====
- name: download-artifacts
id: download-artifacts
uses: obs-download
with:
endpoint: "obs.cn-north-4.myhuaweicloud.com"
bucket: "my-package"
key: |
cache/master/package_aarch64.tar.gz
cache/master/package_x86_64.tar.gz
path: "./artifacts/"
file-concurrency: "2"
# ===== 扫描指定产物并上传结果 =====
# artifact-path 精确指定只扫描这两个包,避免误扫目录中其他产物
# artifact-download-url 以 / 结尾视为URL前缀,插件自动拼接产物文件名
- name: security-compilation-options-scan
uses: openlibing/security-compilation-options-action@v1.0.2
with:
artifact-path: |
./artifacts/package_aarch64.tar.gz
./artifacts/package_x86_64.tar.gz
4.2 触发方式
| 触发方式 | 配置关键字 | 适用场景 |
|---|---|---|
| 手动触发 | workflow_dispatch |
按需验证、首次接入验证 |
| 代码推送触发 | pull_request_target / pull_request_comment |
提PR/PR评论时自动检测 |
| 定时任务触发 | schedule.cron |
定时检测跟踪趋势(如 Nightly 流水线) |
4.3 Action 输入参数
| 参数 | 必填 | 默认值 | 说明 |
|---|---|---|---|
| artifact-path | 是 | - | 构建产物文件路径,支持多行指定多个文件(每行一个路径)。推荐配合构建步骤的 step output 使用 |
| output | 否 | sec-option-result.json | 结果输出文件路径 |
| package-name | 否 | '' | 构建产物压缩包文件名。不传则自动从 artifact-path 推断 |
| artifact-download-url | 否 | '' | 构建产物下载链接。以 / 结尾视为URL前缀,插件自动拼接前缀+产物文件名作为每个包的下载链接(多产物包只需写一次前缀);不以 / 结尾视为完整URL。如未提供下载链接或下载链接已失效/需要认证信息,openLiBing 平台将不支持产物下载 |
| scan-options | 否 | '' | 实际扫描项列表(逗号分隔),不传或为空表示扫描默认 14 项(stackClash/fstackCheck 依赖编译器记录段,构建未开启 -frecord-gcc-switches/-frecord-command-line 或无记录段时返回 N/A)。仅扫描的项会出现在 overviewData/fileDetails 的 options 字段中,未扫描项 key 缺失 |
| apig-app-key | 否 | '' | 已废弃:APIG 网关 AppKey(AK)。请使用新的 OIDC 认证方式(见下方「上传认证」),无需任何凭证 |
| apig-app-secret | 否 | '' | 已废弃:APIG 网关 AppSecret(SK),与 apig-app-key 配套。请使用新的 OIDC 认证方式,无需任何凭证 |
scan-options 可选 key:bindNow,nx,pic,pie,relro,rpath,sp,strip,fortify,fvisibility,ftrapv,stackClash,fstackCheck,aslr
上传认证
上传功能默认开启,插件使用 OIDC 联邦认证,无需任何凭证,workflow 中声明 permissions: id-token: write 即可(见 4.1 三个场景示例)。未适配的存量工作流保持原样即可:继续在 with 中传入 apig-app-key / apig-app-secret,插件走旧接口和 AK/SK 认证,行为与升级前完全一致。
存量 AK/SK 模式的 secrets(OPENLIBING_APIG_KEY / OPENLIBING_APIG_SECRET)在平台配置方式如下:

概览输出(ATOMGIT_OUTPUT)
扫描完成后,插件会将各产物包在不同扫描项上的开启率通过 ATOMGIT_OUTPUT 输出,便于工作流页面直接查看概览(不含明细)。每个实际扫描项对应一个输出标识(中文名(英文名)),值为 包名 -> 开启率 的映射,开启率为带 % 的百分数或 N/A。输出标识固定为 中文名(英文名),引用时以此为准;内部键(scan-options 输入/排序用)与输出标识的对应关系见下表,仅输出实际扫描到的项。使用方式:${{ steps.<scan-id>.outputs['立即绑定(BIND_NOW)'] }}。
各扫描项的内部键(scan-options 输入/排序用)与输出标识(引用用)对应关系:
| 内部键 | 输出标识(引用用) |
|---|---|
bindNow |
立即绑定(BIND_NOW) |
nx |
堆栈不可执行(NX) |
pic |
地址无关(PIC) |
pie |
随机化(PIE) |
relro |
GOT表保护(RELRO) |
sp |
栈保护(SP) |
rpath |
禁选动态库搜索路径(NO Rpath) |
fortify |
缓冲区溢出保护(FS) |
ftrapv |
整数溢出检查(Ftrapv) |
strip |
删除符号表(Strip) |
aslr |
地址空间布局随机化(ASLR) |
fvisibility |
符号全局不可见(Fvisibility) |
fstackCheck |
栈检查(Fstackcheck) |
stackClash |
栈冲突保护(Fstackclash) |
以单产物包 app.jar 为例:
| 输出标识 | 值 |
|---|---|
立即绑定(BIND_NOW) |
{"app.jar":"100%"} |
堆栈不可执行(NX) |
{"app.jar":"95.83%"} |
栈冲突保护(Fstackclash) |
{"app.jar":"N/A"} |
多个产物包时,同一扫描项标识的值为各包开启率的映射,如 {"app.jar":"100%","lib.so":"80%"}。
平台详情链接输出
上报成功后,插件为每个产物包输出 1 条该产物包在 openLiBing 平台的详情页跳转链接(每包固定 1 行,多产物包按包分别输出);运行日志(core.info)同时打印实际链接便于查看。
输出标识为扫描结果(app.jar)url,示例:
扫描结果(app.jar)url=https://www.openlibing.com/apps/securityOptions/package-detail?recordId=2103041785973575680&projectId=3
未录入 openLiBing 平台的代码仓(repo_info 无匹配)时不输出链接,输出提示行(文案由服务端返回,插件不硬编码):
扫描结果(app.jar)=当前仓未录入openLiBing平台,无法生成跳转链接,请先联系SRE完成代码仓录入。
说明
- 概览输出通过
ATOMGIT_OUTPUT写入。本插件为 Node 自定义 action,写入ATOMGIT_OUTPUT的输出无需在 action.yml 中声明outputs:即可被${{ steps.<scan-id>.outputs.<...> }}引用(中文键使用中括号字符串,如${{ steps.scan.outputs['立即绑定(BIND_NOW)'] }});平台详情链接输出的键同样无需声明即可引用。- 概览值为供工作流页面展示的可读字符串,开启率带
%或为N/A;而「Action 输出」中core.setOutput输出为纯数值(如{option}-rate=95.83),同名项单位同为%。两者不混用:编程取用建议用core.setOutput的纯数值键,概览值仅作展示。- 实际链接由服务端拼接,同一代码仓关联多个项目时,固定输出查询第一个项目对应的 1 条链接(不输出多链接)。
- 上报失败或链接构建异常时不输出链接与提示;写入失败时跳过输出,不影响上报结果。
5. 开启率计算规则
5.1 计算公式
开启率 = yesCount ÷ applicableFiles × 100%
| 字段 | 类型 | 说明 |
|---|---|---|
| totalFiles | Integer | 参与检测的 ELF 文件总数(YES + NO + N/A 均计入,仅作展示参考) |
| applicableFiles | Integer | 适用该选项的文件数(YES + NO,N/A 不计入分母) |
| yesCount | Integer | 合规文件数(rpath 选项为 NO 的文件数,已做反转) |
| rate | Float | 开启率百分比,保留 2 位小数;applicableFiles 为 0 时 rate 为 0.0 |
5.2 三值枚举说明
每个选项的检测结果为三值枚举:
| 值 | 含义 | 是否参与分母(applicableFiles) | 是否计入 yesCount |
|---|---|---|---|
| YES | 已开启 / 合规 | ✅ | ✅(rpath 除外,已反转) |
| NO | 未开启 / 不合规 | ✅ | ❌ |
| N/A | 不适用(如目标文件 .o、纯 Go 文件等) |
❌ | ❌ |
注意:N/A 文件不参与分母计算(不计入 applicableFiles),避免大量
.o目标文件(PIE/RELRO 等返回 N/A)拉低开启率,使开启率能真实反映适用文件的安全水平。totalFiles字段保留全部文件数(含 N/A)仅作展示参考。
5.3 rpath 选项的反转处理
rpath 选项与其他 13 项的判定逻辑相反:
| 选项 | YES 含义 | 计算开启率时的处理 |
|---|---|---|
| 其他 13 项 | 合规(已开启安全选项) | yesCount = YES 文件数 |
| rpath | 不合规(存在不安全搜索路径) | yesCount = NO 文件数(反转) |
页面展示的 rpath 开启率 = 无不安全搜索路径的文件数 ÷ 适用文件数 × 100%。
6. 插件本地输出文件示例
扫描完成后生成的 sec-option-result.json 完整结构:
{
"summary": {
"options": {
"bindNow": {
"totalFiles": 10,
"applicableFiles": 10,
"yesCount": 8,
"rate": 80.0
},
"nx": {
"totalFiles": 10,
"applicableFiles": 10,
"yesCount": 10,
"rate": 100.0
},
"pic": {
"totalFiles": 5,
"applicableFiles": 5,
"yesCount": 3,
"rate": 60.0
},
"pie": {
"totalFiles": 10,
"applicableFiles": 10,
"yesCount": 7,
"rate": 70.0
},
"relro": {
"totalFiles": 10,
"applicableFiles": 10,
"yesCount": 9,
"rate": 90.0
},
"rpath": {
"totalFiles": 10,
"applicableFiles": 10,
"yesCount": 10,
"rate": 100.0
},
"sp": {
"totalFiles": 8,
"applicableFiles": 8,
"yesCount": 6,
"rate": 75.0
},
"strip": {
"totalFiles": 10,
"applicableFiles": 10,
"yesCount": 5,
"rate": 50.0
},
"fortify": {
"totalFiles": 10,
"applicableFiles": 8,
"yesCount": 6,
"rate": 75.0
},
"fvisibility": {
"totalFiles": 5,
"applicableFiles": 5,
"yesCount": 4,
"rate": 80.0
},
"ftrapv": {
"totalFiles": 10,
"applicableFiles": 10,
"yesCount": 2,
"rate": 20.0
},
"stackClash": {
"totalFiles": 10,
"applicableFiles": 10,
"yesCount": 8,
"rate": 80.0
},
"aslr": {
"totalFiles": 10,
"applicableFiles": 10,
"yesCount": 10,
"rate": 100.0
}
}
},
"details": [
{
"filePath": "/path/to/binary",
"fileName": "binary",
"options": {
"bindNow": "YES",
"nx": "NO",
"pic": "N/A",
"pie": "YES",
"relro": "YES",
"rpath": "NO",
"sp": "YES",
"strip": "NO",
"fortify": "YES",
"fvisibility": "N/A",
"ftrapv": "NO",
"stackClash": "YES",
"aslr": "YES"
}
}
],
"detectionInfo": {
"startTime": "2026-06-30T10:00:00",
"endTime": "2026-06-30T10:01:30",
"durationMs": 90000,
"sourceDir": "/abs/path/to/target"
},
"uploadInfo": {
"success": true,
"recordId": 12345
}
}
7. 常见问题(FAQ)
7.1 插件的扫描原理是什么?
插件通过以下流程完成安全编译选项检测:
- 定位构建产物:通过
artifact-path参数显式指定一个或多个构建产物文件路径(支持多行)。每个产物文件会被独立扫描、独立上报。产物文件支持.jar/.war/.ear/.zip/.whl/.tar.gz等容器包格式,也支持裸 ELF 可执行文件或.so共享库 - 自动解压:对每个容器包产物,插件递归解压 ZIP 系列(
.jar/.war/.ear/.zip/.whl)和 tar 系列(.tar.gz/.tgz/.tar.bz2/.tar.xz/.tar),支持嵌套解压(如 Spring Boot fat jar → 嵌套 jar →.so) - ELF 文件识别:通过魔数
\x7fELF快速识别 ELF 格式文件,跳过非 ELF 文件(.class/.py/ 配置文件等);同时跳过 ET_REL(.o目标文件,尚未链接,多数选项无意义) - 逐项检测:对每个 ELF 文件,使用
pyelftools库解析 ELF 头、Program Header、Section Header、Dynamic Segment、Symbol Table 等结构,逐项判断 14 项安全编译选项的开启状态 - 汇总上报:汇总该产物内所有 ELF 文件的检测结果,计算各项开启率,通过 APIG 网关上报到 openLiBing
7.2 pyelftools 的扫描方法是否是业界通用的?
pyelftools 是业界广泛使用的 ELF 解析方案。
- pyelftools 是 Python 生态中解析 ELF 文件的事实标准库,GitHub 3k+ stars,被众多安全扫描工具(如 checksec 的 Python 移植版)采用
- 插件的检测逻辑与 Linux 社区经典工具 checksec(Shell 版)完全一致,均基于 ELF 规范中的标准结构进行判断:
| 选项 | 检测方法 | ELF 结构依据 |
|---|---|---|
| BIND_NOW | 遍历 PT_DYNAMIC 段,检查 DT_BIND_NOW 标签或 DT_FLAGS 中的 DF_BIND_NOW 位 |
ELF Spec - Dynamic Section |
| NX | 检查 PT_GNU_STACK 段的 PF_X(可执行)标志是否未设置 |
GNU Extension - PT_GNU_STACK |
| PIC | ET_DYN 类型文件中检查 DT_TEXTREL 不存在(且非可执行文件即无 DT_DEBUG) |
ELF Spec - Position Independent Code |
| PIE | ET_DYN 类型 + 存在 PT_DYNAMIC + 存在 DT_DEBUG = PIE 可执行文件 |
GNU Extension - Position Independent Executable |
| RELRO | 检查是否存在 PT_GNU_RELRO 段 |
GNU Extension - PT_GNU_RELRO |
| Rpath | 遍历 PT_DYNAMIC 段,检查 DT_RPATH / DT_RUNPATH 标签 |
ELF Spec - Dynamic Section |
| SP | 在 .symtab / .dynsym 符号表中查找 __stack_chk_fail 符号 |
GCC -fstack-protector 编译产物特征 |
| Strip | 检查 .symtab 节是否不存在 |
ELF Spec - Symbol Table Section |
| FORTIFY | 在 .dynsym 中查找 _chk 后缀的加固函数符号(如 __printf_chk) |
GCC -D_FORTIFY_SOURCE 编译产物特征 |
| FVISIBILITY | 检查 .dynsym 中非导出符号的比例,判断可见性控制是否生效 |
GCC -fvisibility 编译产物特征 |
| FTRAPV | 在 .dynsym / .symtab 中查找 __addvsi3 / __subvsi3 等陷阱函数符号 |
GCC -ftrapv 编译产物特征 |
| StackClash | 读取编译器记录段 .GCC.command.line/.llvmcmd,命中 fstack-clash-protection 视为开启;记录段缺失返回 N/A |
编译器记录段(-frecord-gcc-switches / -frecord-command-line) |
| StackCheck | 读取编译器记录段 .GCC.command.line/.llvmcmd,命中 fstack-check 视为开启;记录段缺失返回 N/A |
编译器记录段(-frecord-gcc-switches / -frecord-command-line) |
| ASLR | 检查 PIE + PT_GNU_RELRO + PT_GNU_STACK 组合是否完整,判断 ASLR 是否可生效 |
ASLR 依赖 PIE/RELRO/NX 的组合生效 |
- 唯一的区别是:checksec 调用系统命令(
readelf),而本插件使用 Python 库直接解析二进制,结果等价但跨平台兼容性更好(不依赖系统 readelf)。其中stackClash/fstackCheck走编译器记录段识别,能力超出 checksec(checksec 不检测这两项)。
7.3 安全编译选项开启率意味着什么?
安全编译选项开启率反映的是构建产物中 ELF 二进制文件的安全防护水平:
| 开启率范围 | 含义 | 风险等级 |
|---|---|---|
| 90% ~ 100% | 绝大部分二进制已开启安全编译选项,防护水平良好 | 低 |
| 70% ~ 89% | 多数已开启,但仍有部分二进制缺少防护 | 中 |
| 50% ~ 69% | 近半数二进制缺少安全编译选项,存在可被利用的攻击面 | 高 |
| < 50% | 大量二进制未启用基本防护,攻击面大 | 极高 |
特别说明:
- NX(堆栈不可执行) 是最基础也最重要的防护项,几乎不存在现代系统不支持 NX 的场景,NX 开启率应接近 100%
- PIE + RELRO + BIND_NOW 三者配合构成完整的地址空间布局随机化(ASLR)和 GOT 保护链,缺失任一项都会削弱整体防护
- SP(栈保护) 和 Strip(符号剥离) 分别防御运行时攻击和降低信息泄露风险
- FORTIFY(缓冲区溢出检测) 在编译期和运行期对固定大小缓冲区函数调用进行检查,是 Ascend 社区推荐的编译选项
- FVISIBILITY(符号可见性) 隐藏动态库非导出符号,减小攻击面,降低信息泄露风险
- FTRAPV(整数溢出陷阱) 对有符号整数运算进行运行时检测,捕获溢出攻击,但对性能影响较大
- StackClash(栈冲突保护) 防止攻击者通过大块栈分配跳过守护页实现栈-堆冲突攻击
- ASLR(地址空间布局随机化) 系统级安全配置,需 PIE/RELRO/NX 配合生效,randomize_va_space 应设为 2
7.4 开启率偏低应该怎么处理?
Step 1:定位问题文件
在构建产物详情页,点击开启率数值下钻到文件级详情,找到标记为 NO 的具体 ELF 文件。
Step 2:区分文件类型
| 文件类型 | 典型原因 | 处理方式 |
|---|---|---|
| 自研 C/C++ 编译产物 | 编译器/链接器未开启对应选项 | 修改编译配置(见下方 Step 3) |
第三方 .so / .a |
上游未开启安全编译选项 | 联系上游提供安全编译版本,或自行重新编译 |
.o 目标文件 |
未链接,部分选项无意义(返回 N/A) | 正常现象,无需处理;N/A 不计入分母,不影响开启率 |
| 纯 Go 二进制 | Go 自带栈保护机制,不走 GCC 的 __stack_chk_fail,SP 返回 N/A |
正常现象,Go 有独立的安全机制 |
| cgo 混编产物 | 部分选项取决于 C 编译侧配置 | 检查 CGO 编译 flags |
Step 3:修改编译配置
| 选项 | GCC / G++ 编译 flag | 说明 |
|---|---|---|
| PIE | -fPIE -pie |
生成位置无关可执行文件 |
| PIC | -fPIC |
生成地址无关代码(共享库必需) |
| RELRO(Partial) | -Wl,-z,relro |
启用 GOT 表部分只读 |
| BIND_NOW(Full RELRO) | -Wl,-z,relro,-z,now |
启用 GOT 表完全只读 + 立即绑定 |
| NX | 默认开启(GCC 4.9+) | 若被关闭,检查链接脚本是否移除了 PT_GNU_STACK |
| SP | -fstack-protector-strong |
启用栈保护(推荐 -strong,比 -all 性能开销小) |
| Strip | -s 或后处理 strip --strip-all |
剥离符号表 |
| Rpath | 避免使用 -Wl,-rpath |
改用 LD_LIBRARY_PATH 或 ldconfig 管理库路径 |
| FORTIFY | -D_FORTIFY_SOURCE=2 -O2 |
缓冲区溢出检测,需配合 -O2 及以上优化级别 |
| FVISIBILITY | -fvisibility=hidden |
隐藏非导出符号,减小动态库攻击面 |
| FTRAPV | -ftrapv |
有符号整数溢出陷阱,对性能影响较大,建议根据测试结果决定 |
| StackClash | -fstack-clash-protection |
栈冲突保护,逐页分配栈空间并立即探测 |
| ASLR | 系统配置 echo 2 >/proc/sys/kernel/randomize_va_space |
地址空间布局随机化,需 PIE/RELRO/NX 配合生效,randomize_va_space=2 |
推荐 baseline 配置:
-fPIE -fPIC -fstack-protector-strong -Wl,-z,relro,-z,now -D_FORTIFY_SOURCE=2 -O2 -fvisibility=hidden -fstack-clash-protection -frecord-gcc-switches(-frecord-gcc-switches让 GCC 把实际编译选项写入.GCC.command.line记录段,供扫描器识别-fstack-clash-protection等栈选项;Clang 对应-frecord-command-line,无记录段时该两项会返回 N/A)
Step 4:Maven / CMake / Makefile 配置示例
Maven(pom.xml):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-fPIE</arg>
<arg>-fstack-protector-strong</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<!-- native 代码通过 Maven Nar Plugin 或 exec-maven-plugin 编译时加 -Wl,-z,relro,-z,now -->
</manifestEntries>
</archive>
</configuration>
</plugin>
CMake(CMakeLists.txt):
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIE -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 -fvisibility=hidden -fstack-clash-protection")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIE -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 -fvisibility=hidden -fstack-clash-protection")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pie -Wl,-z,relro,-z,now")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,relro,-z,now")
Makefile:
CFLAGS += -fPIE -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 -fvisibility=hidden -fstack-clash-protection
CXXFLAGS += -fPIE -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 -fvisibility=hidden -fstack-clash-protection
LDFLAGS += -pie -Wl,-z,relro,-z,now
7.5 为什么开启率计算排除了 N/A 文件?
N/A 表示该选项对当前文件"不适用"(如 .o 目标文件对 PIE/RELRO/BIND_NOW 返回 N/A)。N/A 文件不计入开启率分母(applicableFiles),只参与 totalFiles 统计展示,避免大量无意义文件拉低开启率,使开启率能真实反映"适用文件"的安全水平。
常见返回 N/A 的场景:
| 场景 | 说明 | 是否需要处理 |
|---|---|---|
构建产物中包含 .o 中间文件 |
.o 文件尚未链接,PIE/RELRO/BIND_NOW 无意义返回 N/A |
不影响开启率;建议构建流程只发布最终链接产物,避免误将中间产物打入包中 |
| 纯 Go 二进制 | Go 不使用 GCC 栈保护机制,SP 返回 N/A | 正常现象,Go 有 buildmode=pie 等自身安全机制 |
静态库 .a |
内部 .o 成员的 PIE/RELRO 返回 N/A |
静态库在最终链接时才生效,关注最终产物即可 |
| 构建未开启编译器记录段 | 未加 -frecord-gcc-switches/-frecord-command-line,或无记录段被 strip,stackClash/fstackCheck 返回 N/A |
若需要这两项开启率,在构建中添加对应的编译器记录开关 |
7.6 只扫描 ELF 文件,非 Linux 产物怎么办?
本插件专门检测 ELF 格式二进制文件(Linux/Android 平台的 .so / 可执行文件)。以下情况不会产生有效检测结果:
| 产物类型 | 检测结果 | 说明 |
|---|---|---|
Java .class 文件 |
不检测 | Java 运行在 JVM 上,不存在 ELF 层面的编译选项问题 |
Windows .exe / .dll |
不检测 | PE 格式,非 ELF;Windows 有独立的安全编译机制(如 /GS, /DYNAMICBASE) |
macOS .dylib / Mach-O |
不检测 | Mach-O 格式,非 ELF |
| 纯前端产物(JS/CSS/HTML) | 不检测 | 非二进制文件 |
如果项目是纯 Java(无 JNI/native 依赖),扫描结果可能为空或无 ELF 文件——这是正常现象,说明项目不涉及 ELF 层面的安全编译选项风险。
7.7 多个构建产物包如何扫描?
通过 artifact-path 参数多行指定多个产物文件,插件会:
- 逐行解析
artifact-path,对每个文件路径做存在性校验 - 分别扫描每个产物包中的 ELF 文件
- 分别上报扫描结果,
package-name默认取各自产物文件名(也可通过package-name参数统一指定)
例如 artifact-path 指定了两个产物包:
with:
artifact-path: |
./artifacts/app-1.0.tar.gz
./artifacts/lib-utils-2.0.tar.gz
若 app-1.0.tar.gz(含 3 个 .so)和 lib-utils-2.0.tar.gz(含 2 个 .so),则会产生两条独立的上报记录,package-name 分别为 app-1.0.tar.gz 和 lib-utils-2.0.tar.gz,对应平台概览页面的两行记录。
如果只指定单个产物文件(单行),则直接扫描该文件,package-name 默认取文件名。
7.8 扫描对构建流程有影响吗?
没有影响。 插件以只读方式分析构建产物,不会修改任何文件。具体保障:
- 解压操作在临时目录中进行,扫描完成后自动清理
- 不修改构建产物本身
- 不依赖或安装系统级工具(如
readelf/checksec),全部通过 Python 库解析
7.9 如何验证编译选项是否真的生效?
除了使用本插件扫描外,可以在 Linux 环境下手动验证:
# 使用 checksec 工具(系统级)
checksec --file=/path/to/binary
# 使用 readelf 手动检查(示例:检查 PIE)
readelf -h /path/to/binary | grep Type
# Type: DYN = PIE, Type: EXEC = 非PIE
# 检查 RELRO
readelf -l /path/to/binary | grep GNU_RELRO
# 检查 BIND_NOW(Full RELRO)
readelf -d /path/to/binary | grep BIND_NOW
手动验证结果应与插件扫描结果一致。