FlawOpen

flawopen.com/Incidents/solarwinds-sunburst-build-compromise-december-2020

● SolarWinds · CWE-506 / CWE-829 · September 2019 – December 2020 · 严重
安全研究 · FlawOpen

SUNBURST:当构建流水线本身就是漏洞

SUNSPOT 在构建时篡改 Orion 源码,将 SUNBURST 植入已签名更新。有效签名只能证明来源,不能证明代码完整性。

💡 通俗易懂的原理解析 (ELI5)

想象一家值得信赖的锁匠公司,定期给成千上万栋楼寄去封好口、盖了章的维护套件。有人闯进组装套件的工厂,在封口和盖章之前往每套里多放了一个小零件。每栋楼收到的套件确实来自这家锁匠,于是照常安装。封条从未被破坏,因为篡改发生在封装之前。

核心概念与专有名词

SUNSPOT
CrowdStrike 分析过的构建服务器植入程序。它监视 MSBuild.exe 编译 Orion 的时刻,并在构建期间把一个源文件替换成带后门的版本。
SUNBURST
被编译进 SolarWinds.Orion.Core.BusinessLayer.dll 的后门。它会潜伏最长两周,然后通过 HTTP 联系攻击者的基础设施。
代码签名
证明二进制文件来自密钥持有者的加密签名。它无法说明产出该文件的构建过程是否被篡改。
可复现构建
用同一份源码构建出逐位相同结果的构建方式,便于相互独立的构建机互相校验结果。
CWE-506
嵌入式恶意代码。本案中,恶意代码是在厂商自己的构建过程中被插入的,而不是在源码仓库里。

框架映射

事件演进时间线

2019-09-04

攻击者进入 SolarWinds 的环境;9 月 12 日,他们向一次 Orion 构建注入测试代码,以验证手法可行。

2020-02-20

SUNBURST 借助 SUNSPOT 被编译进 Orion 构建。

2020-03-26

SolarWinds 开始发布在不知情中含有 SUNBURST 的 Orion 热修复;受影响版本包括 2019.4 HF5 和 2020.2。

2020-06-04

攻击者把 SUNSPOT 从构建环境中移除。

2020-12-13

FireEye 发布对 SUNBURST 的分析,CISA 发布第 21-01 号紧急指令,要求联邦机构断开 Orion。

攻击执行流程分解

步骤 1

构建服务器被攻陷

攻击者进入 SolarWinds 的构建环境,在编译 Orion 的服务器上安装了 SUNSPOT。

步骤 2

构建途中替换源文件

SUNSPOT 一旦发现 MSBuild.exe 在构建 Orion 解决方案,就把 InventoryManager.cs 换成带后门的版本,编译结束后再换回原文件。

步骤 3

真正的密钥为结果签名

编译出的 SolarWinds.Orion.Core.BusinessLayer.dll 照常经过签名步骤,因此带后门的 DLL 带有有效的 SolarWinds 签名。

步骤 4

客户安装受信任的更新

安装受影响更新的客户不到 18,000 家。随后,攻击者从中挑选了规模小得多的一批受害者继续入侵。

源代码对比:漏洞与安全实现

示意性的 C# 代码,并非 SolarWinds 的流水线。有漏洞的任务信任单一、长期运行的构建服务器,并为其产出的任何东西签名,这正是 SUNSPOT 所利用的。加固后的关卡要求在全新部署、分别管理的两台构建机上构建同一个已批准的提交,以恒定时间比较两者的 SHA-256 摘要,只要有任何差异就拒绝签名。这样,一台构建机上的植入程序只会造成结果不一致,而不是一个已签名的发布;只有攻击者同时控制两台构建机时才会失效,因此它们绝不能共享凭据或镜像。

存在漏洞的实现
using System;
using System.Diagnostics;

public static class ReleasePipeline
{
    // 构建在一台服务器上使用共享且可写的工作副本,并对
    // 任何构建结果签名。构建期间对源码树的修改也会被签名。
    public static void BuildAndSign(string project, string outputDll)
    {
        using var build = Process.Start("msbuild.exe", $"\"{project}\" /p:Configuration=Release");
        build!.WaitForExit();
        if (build.ExitCode != 0)
            throw new InvalidOperationException("build failed");

        Signer.Sign(outputDll);   // HSM-backed signing service client
    }
}
加固后的安全修复
using System;
using System.IO;
using System.Security.Cryptography;

public static class ReleaseGate
{
    // 两个隔离的构建器从全新、只读的检出中编译同一个固定 commit,
    // 并使用 /p:Deterministic=true。门禁只对完全相同的输出签名。
    public static void VerifyAndSign(string builderAOutput, string builderBOutput,
                                     string builtCommit, string approvedCommit)
    {
        if (!string.Equals(builtCommit, approvedCommit, StringComparison.Ordinal))
            throw new InvalidOperationException("build is not from the approved commit");

        byte[] a = SHA256.HashData(File.ReadAllBytes(builderAOutput));
        byte[] b = SHA256.HashData(File.ReadAllBytes(builderBOutput));
        if (!CryptographicOperations.FixedTimeEquals(a, b))
            throw new InvalidOperationException("independent builds differ; refusing to sign");

        Signer.Sign(builderAOutput, Convert.ToHexString(a));   // digest is logged with the signature
    }
}

工程与系统安全加固清单

参考来源