(2/4) 방어 관점 — 생성형 AI로 탐지·대응 자동화
🛡️ 생성형 AI × 보안 4부작 (전체 4편)
- 공격 — 인가된 모의침투에 LLM 붙이기
- 방어 — 탐지·대응 자동화 ← 지금 글
- 포렌식과 데이터 복원
- LLM 그 자체의 보안 — 프롬프트 인젝션과 가드레일
Summary
1편이 공격(레드팀) 관점이었다면, 이번엔 방어(블루팀) 입니다. 방어자의 가장 큰 적은 해커가 아니라 알람 피로(alert fatigue) 예요. SOC에 하루 수만 건씩 쏟아지는 경보 중에서 진짜를 골라내는 일, 여기에 LLM이 제대로 들어맞습니다.
이 글에서도 1편의 ask_llm 헬퍼를 그대로 재료로 씁니다. 다만 방어 쪽은 민감 로그를 다루기 때문에, 외부 API에 넣기 전 마스킹과 로컬 모델 선택이 1편보다 훨씬 중요해져요.
💡 이 글에서 다루는 것
- 로그 트리아지 — 의심 로그를 구조화 JSON으로
- IOC(침해지표) 추출과 ATT&CK 매핑
- 탐지 룰 생성 — Sigma / YARA 초안
- 위협 인텔(CTI) 리포트 요약
- 인시던트 대응(IR) 타임라인·보고서 초안
- 로그를 LLM에 넣기 전: 마스킹과 온프렘 모델
1. 방어자의 진짜 문제: 알람 피로
탐지 도구는 이미 많아요. EDR, IDS, WAF, SIEM… 문제는 이것들이 너무 많이 운다는 거예요. 하루 5만 건 경보 중 99.9% 가 오탐인데, 나머지 0.1% 를 놓치면 사고가 납니다. 분석가는 사람인지라, 반복되는 비슷한 경보를 보다 보면 둔감해져요.
LLM이 들어갈 자리가 여기예요.
- 1차 트리아지 — 경보를 “조사 필요 / 무시 가능 / 즉시 대응” 으로 분류
- 맥락 결합 — 흩어진 로그를 사건 하나의 내러티브로 묶기
- 구조화 — 자유 텍스트 로그를 쿼리 가능한 JSON으로 변환
- 초안 생성 — 탐지 룰·IR 보고서의 1차 버전
핵심 원칙 하나만 먼저. LLM의 판단은 “사람의 검토를 줄이는 필터” 지, “사람을 대체하는 결정” 이 아니에요. “무시 가능” 으로 분류된 것도 샘플링해서 사람이 다시 봅니다. 그래야 모델이 놓친 0.1% 를 잡아요.
2. 로그 트리아지 — 자유 텍스트를 구조화 JSON으로
로그는 사람이 읽으라고 만든 자유 텍스트예요. 자동화하려면 구조가 필요하죠. LLM에게 스키마를 주고 그 틀에 맞춰 채우게 하면, 너저분한 로그가 쿼리 가능한 데이터가 됩니다.
아래는 의심스러운 웹 접근 로그 한 줄을 구조화하는 예시예요. (출발지 IP는 문서용 대역 198.51.100.x 로 마스킹)
import json
log_line = (
'198.51.100.77 - - [09/Jun/2026:11:02:13 +0900] '
'"GET /admin/../../etc/passwd HTTP/1.1" 200 1832 '
'"-" "sqlmap/1.7#stable"'
)
system = (
"너는 SOC 분석가다. 주어진 로그 한 줄을 분석해 아래 JSON 스키마로만 답하라. "
"추가 설명 금지.\n"
'{"severity":"low|medium|high|critical",'
'"attack_type":"문자열","indicators":["..."],'
'"recommended_action":"문자열"}'
)
raw = ask_llm(system=system, user=log_line)
result = json.loads(raw)
print(json.dumps(result, ensure_ascii=False, indent=2))
{
"severity": "high",
"attack_type": "Path Traversal + 자동화 스캐너",
"indicators": [
"../../etc/passwd 경로 조작 시도",
"User-Agent: sqlmap (자동 공격 도구)",
"HTTP 200 — 민감 파일 노출 가능성"
],
"recommended_action": "해당 출발지 IP 차단, 200 응답 본문 확인, /admin 접근통제 점검"
}
여기서 결정적으로 좋은 점은 응답이 JSON 이라는 거예요. 텍스트가 아니라 데이터니까, 그대로 SIEM에 적재하거나 임계치로 자동 분기할 수 있어요.
def triage(log_line: str) -> dict:
"""로그 한 줄 → 트리아지 결과 dict. severity 가 high 이상이면 즉시 알림 대상."""
raw = ask_llm(system=system, user=log_line)
return json.loads(raw)
r = triage(log_line)
if r["severity"] in ("high", "critical"):
print(f"🚨 즉시 대응: {r['attack_type']} → {r['recommended_action']}")
🚨 즉시 대응: Path Traversal + 자동화 스캐너 → 해당 출발지 IP 차단, 200 응답 본문 확인, /admin 접근통제 점검
⚠️ 모델이 가끔 JSON 앞뒤에 군말을 붙여
json.loads가 깨질 때가 있어요. 실무에서는try/except로 감싸고, 더 단단히 하려면 모델의 구조화 출력(structured output)·tool use 기능으로 스키마를 강제하세요. (강제 방법은 4편에서 가드레일과 함께 다룹니다.)
3. IOC 추출과 ATT&CK 매핑
인시던트 보고서나 로그 덤프에서 IOC(Indicator of Compromise) — 악성 IP·도메인·해시·파일 경로 — 를 뽑아내는 건 손이 많이 가는 일이에요. LLM은 비정형 텍스트에서 이걸 잘 긁어냅니다. 게다가 한 발 더 나가서 MITRE ATT&CK 기법 ID까지 매핑해줘요.
incident_note = """\
사용자 PC에서 powershell.exe 가 인코딩된 명령으로 실행됨.
외부 198.51.100.203 으로 443 아웃바운드 발생. 이후 C:\\Users\\Public\\svc.exe
생성(해시 d41d8cd98f00b204e9800998ecf8427e). 작업 스케줄러에 'SystemUpdate'
항목이 새로 등록됨.
"""
system = (
"너는 위협 인텔 분석가다. 아래 노트에서 IOC를 종류별로 추출하고, "
"관찰된 행위를 MITRE ATT&CK 기법(ID 포함)에 매핑하라. "
"JSON 으로만 답하라: {\"iocs\":{...}, \"attack\":[{\"id\":..,\"name\":..}]}"
)
print(ask_llm(system=system, user=incident_note))
{
"iocs": {
"ipv4": ["198.51.100.203"],
"md5": ["d41d8cd98f00b204e9800998ecf8427e"],
"files": ["C:\\Users\\Public\\svc.exe"],
"scheduled_task": ["SystemUpdate"]
},
"attack": [
{"id": "T1059.001", "name": "PowerShell (인코딩 명령 실행)"},
{"id": "T1071.001", "name": "Application Layer Protocol: Web (443 C2)"},
{"id": "T1053.005", "name": "Scheduled Task/Job (지속성)"}
]
}
이게 왜 강력하냐면, ATT&CK 매핑은 곧 “다음에 뭘 찾아야 하는지” 의 지도가 되거든요. 지속성(T1053)이 잡혔으니 다른 지속성 메커니즘(레지스트리 Run 키, 서비스 등록)도 같이 뒤져봐야 한다는 단서를 줍니다. 분석가가 머릿속으로 하던 연결을 LLM이 1차로 깔아주는 거예요.
추출한 IOC는 그대로 차단 목록·위협 헌팅 쿼리로 흘려보낼 수 있어요.
import re
def extract_iocs(text: str) -> dict:
"""LLM 추출 + 정규식 교차검증. LLM이 놓친 패턴을 정규식이 보완."""
llm = json.loads(ask_llm(system=system, user=text))
# 정규식으로 교차검증 (LLM 단독 신뢰 금지)
ipv4 = re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", text)
llm.setdefault("iocs", {})["ipv4_regex_check"] = sorted(set(ipv4))
return llm
print(extract_iocs(incident_note)["iocs"]["ipv4_regex_check"])
['198.51.100.203']
💡 LLM과 정규식을 둘 다 돌리세요. 정규식은 형식이 명확한 IOC(IP·해시)를 빠짐없이 잡고, LLM은 “작업 스케줄러에 등록된 이름” 같은 맥락 IOC를 잡아요. 서로의 빈틈을 메웁니다.
4. 탐지 룰 생성 — Sigma / YARA 초안
분석으로 끝나면 같은 공격에 또 당해요. 탐지를 룰로 굳혀야 다음에 자동으로 잡힙니다. Sigma(로그 탐지)·YARA(파일 탐지) 룰은 문법이 정형화돼 있어서 LLM이 초안을 잘 만들어요.
앞에서 본 PowerShell 인코딩 실행을 Sigma 룰로 만들어볼게요.
system = (
"너는 탐지 엔지니어다. 아래 행위를 탐지하는 Sigma 룰 YAML 을 작성하라. "
"title, logsource, detection, condition, level 을 포함하고, "
"오탐을 줄이기 위한 falsepositives 섹션도 넣어라."
)
behavior = "powershell.exe 가 -EncodedCommand 인자로 base64 인코딩 명령을 실행"
print(ask_llm(system=system, user=behavior))
title: Suspicious PowerShell Encoded Command Execution
logsource:
category: process_creation
product: windows
detection:
selection:
Image|endswith: '\powershell.exe'
CommandLine|contains:
- '-EncodedCommand'
- '-enc '
condition: selection
falsepositives:
- 일부 정상 관리 스크립트(SCCM, 설치 프로그램)도 -enc 를 사용
level: medium
여기서 LLM이 falsepositives 섹션까지 채운 게 포인트예요. 초보 탐지 룰이 흔히 빠지는 함정이 “정상 트래픽까지 다 잡아서 또 알람 피로를 만드는 것” 인데, 모델에게 미리 오탐 후보를 적게 하면 룰 품질이 올라가요.
YARA도 같은 패턴이에요. 멀웨어 표본의 특징 문자열을 주고 룰을 받습니다.
system = (
"너는 멀웨어 분석가다. 아래 특징을 가진 파일을 탐지하는 YARA 룰을 작성하라. "
"meta 에 작성자/설명/날짜 자리표시자를 넣고, strings 와 condition 을 채워라."
)
traits = (
"PE 실행파일. 문자열 'svc_update' 와 'C:\\\\Users\\\\Public\\\\svc.exe' 포함. "
"exec(base64.b64decode 패턴의 드로퍼 스크립트도 함께 떨어뜨림."
)
print(ask_llm(system=system, user=traits))
rule Dropper_SvcUpdate {
meta:
author = "<analyst>"
description = "svc_update 드로퍼 탐지"
date = "<YYYY-MM-DD>"
strings:
$s1 = "svc_update" ascii
$s2 = "C:\\Users\\Public\\svc.exe" ascii
$s3 = "base64.b64decode" ascii
condition:
uint16(0) == 0x5A4D and 2 of ($s*)
}
🚨 LLM이 만든 룰은 반드시 테스트셋으로 검증하세요. 정상 파일/로그에 돌려 오탐률을 보고, 실제 표본에 돌려 탐지율을 봅니다. 검증 없이 운영 SIEM에 넣으면 오탐 폭주나 탐지 누락으로 오히려 위험해져요.
5. 위협 인텔(CTI) 요약과 IR 보고서 초안
방어자는 매일 쏟아지는 위협 인텔 리포트·어드바이저리를 따라가야 해요. 영문 수십 페이지 리포트를 LLM이 “우리 환경에 해당되는 부분만” 요약해주면 따라가기가 한결 수월합니다.
cti_report = """\
(가상 CTI 리포트 발췌 — APT 그룹이 Jenkins 구버전의 인증 우회를 악용해
초기 침투 후 PowerShell 기반 C2 를 구축, 작업 스케줄러로 지속성 확보 ...)
"""
system = (
"너는 우리 조직 보안팀 분석가다. 아래 위협 인텔을 읽고 "
"(1) 핵심 위협 한 문단 (2) 우리가 점검할 항목 체크리스트 "
"(3) 관련 ATT&CK 기법 으로 요약하라."
)
print(ask_llm(system=system, user=cti_report))
[핵심 위협] 구버전 Jenkins 인증 우회를 입구로 PowerShell C2와 작업 스케줄러
지속성을 결합하는 침투 체인. 우리 CI 인프라가 외부 노출돼 있으면 직접적 위협.
[점검 체크리스트]
- [ ] 외부 노출된 Jenkins 인스턴스 버전 점검·차단
- [ ] PowerShell -EncodedCommand 로그 탐지 룰 활성화 여부
- [ ] 작업 스케줄러 신규 항목 모니터링
- [ ] CI 서버 아웃바운드 443 트래픽 베이스라인 확인
[ATT&CK] T1190(노출 앱 악용) → T1059.001 → T1053.005
사고가 실제로 터지면, IR 보고서 초안도 LLM이 빠르게 잡아줘요. 흩어진 타임라인 조각을 시간순 내러티브로 묶고, 경영진용 요약과 기술 상세를 분리해줍니다. (타임라인 재구성은 3편 포렌식에서 더 깊게 다뤄요.)
timeline = """\
11:02 web 로그에 path traversal + sqlmap UA
11:05 동일 IP 가 /admin 로그인 50회 시도
11:09 admin 세션으로 svc.exe 업로드
11:10 작업 스케줄러 SystemUpdate 등록
"""
system = (
"너는 IR 리드다. 아래 타임라인으로 인시던트 보고서 초안을 작성하라. "
"'경영진 요약 3줄' 과 '기술 타임라인' 을 분리하라."
)
print(ask_llm(system=system, user=timeline))
[경영진 요약]
- 외부 공격자가 자동화 스캐너로 웹 취약점을 탐색 후 관리자 계정을 침해.
- 악성 실행파일을 업로드하고 작업 스케줄러로 지속성을 확보.
- 현재 해당 IP 차단·계정 잠금 완료, 영향 범위 조사 진행 중.
[기술 타임라인]
11:02 초기 정찰: Path Traversal + sqlmap (T1190)
11:05 자격증명 공격: /admin 무차별 대입 (T1110)
11:09 실행: svc.exe 업로드 (T1105)
11:10 지속성: 작업 스케줄러 등록 (T1053.005)
6. 로그를 LLM에 넣기 전 — 마스킹과 로컬 모델
방어 워크플로우가 공격보다 조심스러운 이유가 이거예요. 우리 로그에는 우리 조직의 진짜 정보 — 사번, 내부 IP, 사용자 계정, 때로는 고객 데이터 — 가 들어있습니다. 이걸 그대로 외부 API에 보내면 그 자체가 정보 유출이에요.
두 갈래로 대응합니다.
① 입력 전 마스킹. 외부 API를 써야 한다면, 보내기 전에 식별정보를 가립니다.
import re
def mask(text: str) -> str:
"""LLM 입력 전 식별정보 마스킹. 의미는 살리고 값만 가린다."""
text = re.sub(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", "<IP>", text) # IP
text = re.sub(r"[\w.+-]+@[\w-]+\.[\w.-]+", "<EMAIL>", text) # 이메일
text = re.sub(r"\b\d{6}\b", "<EMP_ID>", text) # 6자리 사번
return text
sample = "user kim@corp.example 사번 482915 가 10.0.3.21 에서 로그인 실패"
print(mask(sample))
user <EMAIL> 사번 <EMP_ID> 가 <IP> 에서 로그인 실패
마스킹은 분석 품질을 거의 안 떨어뜨려요. “어떤 IP” 가 아니라 “외부 IP가 내부로 붙었다” 는 구조가 분석엔 더 중요하거든요.
② 아예 로컬 모델. 규제 산업(금융·의료·공공)에서는 로그를 외부로 내보내는 것 자체가 금지인 경우가 많아요. 그럴 땐 온프렘에 오픈웨이트 모델을 띄우고 ask_llm 의 엔드포인트만 바꿉니다.
# 로컬 추론 서버(OpenAI 호환 API)로 갈아끼우는 예
from openai import OpenAI
local = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
def ask_local(system: str, user: str, model: str = "local-model") -> str:
resp = local.chat.completions.create(
model=model,
messages=[{"role": "system", "content": system},
{"role": "user", "content": user}],
)
return resp.choices[0].message.content
# 같은 트리아지/룰 생성 파이프라인을 그대로 재사용, 데이터는 망 밖으로 안 나감
🚨 “편하니까” 로 운영 로그를 외부 LLM에 통째로 붙여넣는 습관이 가장 흔한 사고 경로예요. 마스킹 또는 로컬 모델 둘 중 하나는 무조건 깔고 가세요. 규정·계약을 먼저 확인하는 게 순서입니다.
마무리
방어에서 LLM의 진짜 가치는 “사람을 대체” 가 아니라 “알람 피로를 줄여 사람이 진짜 사건에 집중하게” 하는 것이에요. 트리아지로 노이즈를 걷고, IOC와 ATT&CK로 사건을 구조화하고, 탐지 룰로 교훈을 굳히고, 보고서 초안으로 손을 덜고 — 그렇게 아낀 시간을 0.1% 의 진짜 위협에 씁니다.
다음 편은 포렌식과 데이터 복원이에요. 사고가 이미 났을 때, 아티팩트를 해석하고 타임라인을 재구성하고, 손상·삭제된 데이터를 복원하는 과정에 LLM을 붙이는 법을 — 증거능력을 해치지 않는 선에서 — 정리할게요.
일단 오늘은 여기까지…..
다음 글에서는 사고 이후의 흔적을 읽는 포렌식·복원 이야기를 풀어볼게요.
← 이전 글: (1/4) 공격 — 인가된 모의침투에 LLM 붙이기 | 다음 글 →: (3/4) 포렌식과 데이터 복원