flawopen.com/Incidents/solarwinds-sunburst-build-compromise-december-2020
SUNBURST: 빌드 파이프라인이 곧 취약점이었을 때
SUNSPOT은 빌드 중 Orion 소스 코드를 바꿔 서명된 업데이트에 SUNBURST를 넣었습니다. 유효한 서명은 출처만 증명할 뿐 코드 무결성은 증명하지 못합니다.
수천 개 건물에 봉인하고 도장을 찍은 정비 키트를 보내는 믿을 만한 열쇠 업체를 떠올려 보세요. 누군가 키트를 조립하는 공장에 몰래 들어가, 봉인과 도장을 찍기 전에 키트마다 작은 부품 하나를 더 넣습니다. 모든 건물은 정말 그 업체에서 온 키트를 받아 설치합니다. 봉인은 한 번도 뜯기지 않았습니다. 변조가 봉인 전에 일어났기 때문입니다.
핵심 개념 및 용어
SUNSPOT- CrowdStrike가 분석한 빌드 서버 임플란트입니다. MSBuild.exe가 Orion을 빌드하는지 감시하다가, 빌드하는 동안만 소스 파일을 백도어가 든 버전으로 바꿔 넣었습니다.
SUNBURST- SolarWinds.Orion.Core.BusinessLayer.dll에 컴파일된 백도어입니다. 최대 2주 동안 잠복한 뒤 HTTP로 공격자 인프라에 연결했습니다.
코드 서명- 바이너리가 키 보유자에게서 나왔음을 보여 주는 암호학적 서명입니다. 그 바이너리를 만든 빌드가 변조되었는지는 전혀 말해 주지 않습니다.
재현 가능한 빌드- 같은 소스에서 비트 단위로 동일한 결과를 내는 빌드입니다. 독립된 빌더들이 서로의 결과를 검증할 수 있습니다.
CWE-506- 내장된 악성 코드입니다. 이번에는 소스 저장소가 아니라 벤더 자신의 빌드 과정에서 삽입되었습니다.
프레임워크 매핑
- 이 가이드에서 설명하는 약점의 CWE 참조: CWE-506 CWE-829
- D3-OTF 아웃바운드 트래픽 필터링 — 이 가이드의 아웃바운드 네트워크 제한은 서비스 또는 호스트가 접근할 수 있는 대상을 줄입니다.
사고 타임라인
공격자들이 SolarWinds 환경에 접근하고, 9월 12일 방법이 통하는지 확인하려고 Orion 빌드에 테스트 코드를 주입합니다.
SUNSPOT을 통해 SUNBURST가 Orion 빌드에 컴파일됩니다.
SolarWinds가 모르는 사이 SUNBURST가 든 Orion 핫픽스를 배포하기 시작합니다. 영향받은 버전에는 2019.4 HF5와 2020.2가 포함됩니다.
공격자들이 빌드 환경에서 SUNSPOT을 제거합니다.
FireEye가 SUNBURST 분석을 공개하고, CISA가 연방 기관에 Orion 연결 해제를 지시하는 Emergency Directive 21-01을 발령합니다.
단계별 공격 실행 흐름
빌드 서버가 침해된다
공격자들은 SolarWinds의 빌드 환경에 접근해 Orion을 컴파일하는 서버에 SUNSPOT을 설치했습니다.
빌드 도중 소스 파일이 바뀐다
SUNSPOT은 MSBuild.exe가 Orion 솔루션을 빌드하는 것을 보면 InventoryManager.cs를 백도어가 든 버전으로 바꾸고, 컴파일이 끝나면 원본을 되돌려 놓았습니다.
진짜 키가 결과물에 서명한다
컴파일된 SolarWinds.Orion.Core.BusinessLayer.dll은 정상적인 서명 단계를 거쳤고, 그래서 백도어가 든 DLL에 유효한 SolarWinds 서명이 붙었습니다.
고객이 신뢰받는 업데이트를 설치한다
영향받은 업데이트를 설치한 고객은 18,000곳 미만이었습니다. 이후 공격자들은 훨씬 적은 수의 피해자를 골라 추가 침투를 이어 갔습니다.
소스 코드 비교: 취약한 구현 vs 보안 패치
설명용 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
}
}엔지니어링 및 시스템 보안 강화 체크리스트
- ✓CI 러너를 프로덕션처럼 보호합니다. 소스, 시크릿, 서명 키를 보유하고 고객이 실행할 것을 만들어 내므로 프로덕션 데이터베이스 수준의 격리, 접근 통제, 감사 로그가 필요합니다.
- ✓릴리스는 새로 준비하고 따로 관리하는 두 빌더에서 결정적 컴파일로 두 번 빌드하고, 산출물 다이제스트가 다르면 릴리스를 실패 처리합니다.
- ✓유효한 서명을 안전의 증거로 여기지 않습니다. 서명은 출처를 증명할 뿐이며, 그 출처는 키를 보관한 환경만큼만 믿을 수 있습니다.
- ✓특권 에이전트의 외부 통신을 제한합니다. SUNBURST는 명령·제어 서버에 닿아야 했으므로, 임의의 인터넷 호스트와 통신할 수 있는 모니터링 도구는 명시적으로 검토한 결정이어야 합니다.
- ✓각 벤더 에이전트의 권한은 배포 편의가 아니라, 완전히 침해됐을 때 공격자가 얻을 수 있는 것을 기준으로 정합니다.
- ✓공격자가 수정할 수 없는 텔레메트리를 남깁니다. 호스트 밖에 두는 추가 전용 로그가 있어야 몇 달에 걸친 침투를 나중에 재구성할 수 있습니다.