文件最后提交记录最后更新时间
2 个月前
8 天前
README

构建产物安全编译选项指标统计解决方案

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 插件的扫描原理是什么?

插件通过以下流程完成安全编译选项检测:

  1. 定位构建产物:通过 artifact-path 参数显式指定一个或多个构建产物文件路径(支持多行)。每个产物文件会被独立扫描、独立上报。产物文件支持 .jar / .war / .ear / .zip / .whl / .tar.gz 等容器包格式,也支持裸 ELF 可执行文件或 .so 共享库
  2. 自动解压:对每个容器包产物,插件递归解压 ZIP 系列(.jar / .war / .ear / .zip / .whl)和 tar 系列(.tar.gz / .tgz / .tar.bz2 / .tar.xz / .tar),支持嵌套解压(如 Spring Boot fat jar → 嵌套 jar → .so)
  3. ELF 文件识别:通过魔数 \x7fELF 快速识别 ELF 格式文件,跳过非 ELF 文件(.class / .py / 配置文件等);同时跳过 ET_REL(.o 目标文件,尚未链接,多数选项无意义)
  4. 逐项检测:对每个 ELF 文件,使用 pyelftools 库解析 ELF 头、Program Header、Section Header、Dynamic Segment、Symbol Table 等结构,逐项判断 14 项安全编译选项的开启状态
  5. 汇总上报:汇总该产物内所有 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 参数多行指定多个产物文件,插件会:

  1. 逐行解析 artifact-path,对每个文件路径做存在性校验
  2. 分别扫描每个产物包中的 ELF 文件
  3. 分别上报扫描结果,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

手动验证结果应与插件扫描结果一致。