IOC Defanging 및 공유 파이프라인 구축은(는) 무엇을 하나요?
개요
IOC 디펜징은 잠재적으로 악성인 지표(URL, IP 주소, 도메인, 이메일 주소)를 변환하여 실수로 클릭하거나 실행되는 것을 방지하면서도 분석 및 공유를 위한 가독성을 유지합니다. 이 스킬은 여러 소스에서 원시 IOC를 수집하고, 정규화 및 중복 제거한 후 안전한 인간 소비를 위해 디펜징하고, 기계 소비를 위해 STIX 2.1 형식으로 변환한 다음 TAXII 서버, MISP 인스턴스 및 이메일 보고서를 통해 배포하는 자동화된 파이프라인을 구축하는 방법을 다룹니다.
사용 시기
- 사용자 환경에서 IOC 디펜징 및 공유 파이프라인을 구축하거나 구성할 때
- 규정 준수 요구 사항에 맞춰 보안 제어를 수립할 때
- 이 도메인의 보안 아키텍처를 구축하거나 개선할 때
- 이 구현이 필요한 보안 평가를 수행할 때
사전 요구 사항
- Python 3.9+ 및
defang,ioc-fanger,stix2,requests,validators라이브러리 - 자동 공유를 위한 MISP 인스턴스 또는 TAXII 서버
- IOC 유형 이해: IPv4/IPv6, 도메인, URL, 이메일 주소, 파일 해시
- STIX 2.1 Indicator 패턴 및 TLP 마킹 정의에 대한 친숙함
- IOC 수집을 위한 위협 인텔리전스 피드 접근 권한
핵심 개념
IOC 디펜징 표준
디펜징은 실행을 방지하기 위해 활성 프로토콜 및 도메인 구성 요소를 대체합니다: http:// → hxxp://, https:// → hxxps://, 도메인/IP의 점은 [.], 이메일의 @는 [@]가 됩니다. 이는 보고서, 이메일, Slack 채널 및 Paste 사이트에서 자동 링크가 악성 인프라에 대한 네트워크 연결을 트리거할 수 있는 경우에 공유할 때 중요합니다.
IOC 정규화
다른 소스의 원시 IOC는 일관되지 않은 형식으로 제공됩니다. 정규화는 소문자 변환, 후행 슬래시 및 공백 제거, URL에서 도메인 추출, URL 인코딩 해결, 형식 정확성 검증 및 소스 간 중복 제거를 포함합니다.
STIX 2.1 Indicator 패턴
STIX 패턴은 IOC를 표준화된 형식으로 표현합니다: [ipv4-addr:value = '203.0.113.1'], [domain-name:value = 'malicious.example.com'], [url:value = 'http://evil.com/payload'], [file:hashes.'SHA-256' = 'abc123...']. 각 지표는 valid_from, indicator_types, confidence 및 선택적 TLP 마킹을 포함합니다.
워크플로우
단계 1: IOC 추출 및 정규화 구축
import re
import hashlib
from urllib.parse import urlparse, unquote
from datetime import datetime
class IOCExtractor:
"""텍스트에서 IOC를 추출하고 정규화합니다."""
PATTERNS = {
"ipv4": r'\b(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\b',
"domain": r'\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}\b',
"url": r'https?://[^\s<>"{}|\\^`\[\]]+',
"email": r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
"md5": r'\b[a-fA-F0-9]{32}\b',
"sha1": r'\b[a-fA-F0-9]{40}\b',
"sha256": r'\b[a-fA-F0-9]{64}\b',
}
WHITELIST_DOMAINS = {
"google.com", "microsoft.com", "amazon.com", "github.com",
"cloudflare.com", "akamai.com", "example.com",
}
def extract_from_text(self, text):
"""자유 텍스트에서 모든 IOC 유형을 추출합니다."""
# 먼저 이미 디펜징된 지표를 리펜징합니다
text = self._refang(text)
iocs = {"ipv4": set(), "domain": set(), "url": set(),
"email": set(), "md5": set(), "sha1": set(), "sha256": set()}
for ioc_type, pattern in self.PATTERNS.items():
matches = re.findall(pattern, text)
for match in matches:
normalized = self._normalize(match, ioc_type)
if normalized and not self._is_whitelisted(normalized, ioc_type):
iocs[ioc_type].add(normalized)
# URL의 일부인 도메인 제거
url_domains = set()
for url in iocs["url"]:
parsed = urlparse(url)
url_domains.add(parsed.netloc)
iocs["domain"] -= url_domains
total = sum(len(v) for v in iocs.values())
print(f"[+] 텍스트에서 {total}개의 고유 IOC 추출됨")
return {k: sorted(v) for k, v in iocs.items()}
def _refang(self, text):
"""디펜징된 지표를 활성 형태로 변환합니다."""
text = text.replace("hxxp://", "http://").replace("hxxps://", "https://")
text = text.replace("[.]", ".").replace("[@]", "@")
text = text.replace("[://]", "://").replace("(.)", ".")
return text
def _normalize(self, value, ioc_type):
"""IOC 값을 정규화합니다."""
value = value.strip().lower()
if ioc_type == "url":
value = unquote(value).rstrip("/")
elif ioc_type == "domain":
value = value.rstrip(".")
return value
def _is_whitelisted(self, value, ioc_type):
"""IOC가 화이트리스트에 있는지 확인합니다."""
if ioc_type == "domain":
return value in self.WHITELIST_DOMAINS
if ioc_type == "url":
parsed = urlparse(value)
return parsed.netloc in self.WHITELIST_DOMAINS
return False
extractor = IOCExtractor()
sample_text = """
악성코드 C2: hxxps://evil-domain[.]com/beacon
192.168.1.100에서 페이로드 드롭 및 10[.]0[.]0[.]1 접촉
SHA256: 275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f
attacker[@]phishing-domain[.]com에서 피싱 이메일
"""
iocs = extractor.extract_from_text(sample_text)
단계 2: 디펜징 엔진
class IOCDefanger:
"""보고서 및 커뮤니케이션에서 안전하게 공유하기 위해 IOC를 디펜징합니다."""
def defang_url(self, url):
return url.replace("http://", "hxxp://").replace("https://", "hxxps://").replace(".", "[.]")
def defang_domain(self, domain):
return domain.replace(".", "[.]")
def defang_ip(self, ip):
return ip.replace(".", "[.]")
def defang_email(self, email):
return email.replace("@", "[@]").replace(".", "[.]")
def defang_all(self, iocs):
"""딕셔너리의 모든 IOC를 디펜징합니다."""
defanged = {}
for ioc_type, values in iocs.items():
if ioc_type == "url":
defanged[ioc_type] = [self.defang_url(v) for v in values]
elif ioc_type == "domain":
defanged[ioc_type] = [self.defang_domain(v) for v in values]
elif ioc_type == "ipv4":
defanged[ioc_type] = [self.defang_ip(v) for v in values]
elif ioc_type == "email":
defanged[ioc_type] = [self.defang_email(v) for v in values]
else:
defanged[ioc_type] = values # 해시는 디펜징 불필요
return defanged
def generate_sharing_report(self, iocs, defanged, report_name="IOC 보고서"):
"""사람이 읽을 수 있는 디펜징된 IOC 보고서를 생성합니다."""
report = f"# {report_name}\n"
report += f"생성일: {datetime.now().isoformat()}\n\n"
for ioc_type in ["url", "domain", "ipv4", "email", "sha256", "sha1", "md5"]:
values = defanged.get(ioc_type, [])
if values:
report += f"## {ioc_type.upper()} ({len(values)}개)\n"
for v in values:
report += f"- `{v}`\n"
report += "\n"
return report
defanger = IOCDefanger()
defanged = defanger.defang_all(iocs)
report = defanger.generate_sharing_report(iocs, defanged, "악성코드 캠페인 IOC")
print(report)
단계 3: STIX 2.1 형식으로 변환
from stix2 import Indicator, Bundle, TLP_WHITE, TLP_GREEN, TLP_AMBER
from datetime import datetime
class STIXConverter:
"""원시 IOC를 STIX 2.1 Indicator 객체로 변환합니다."""
TLP_MAP = {"white": TLP_WHITE, "green": TLP_GREEN, "amber": TLP_AMBER}
def iocs_to_stix(self, iocs, tlp="green", confidence=75):
"""IOC 딕셔너리를 STIX 2.1 번들로 변환합니다."""
stix_objects = []
marking = self.TLP_MAP.get(tlp, TLP_GREEN)
for ip in iocs.get("ipv4", []):
stix_objects.append(Indicator(
name=f"악성 IP: {ip}",
pattern=f"[ipv4-addr:value = '{ip}']",
pattern_type="stix",
valid_from=datetime.now(),
indicator_types=["malicious-activity"],
confidence=confidence,
object_marking_refs=[marking],
))
for domain in iocs.get("domain", []):
stix_objects.append(Indicator(
name=f"악성 도메인: {domain}",
pattern=f"[domain-name:value = '{domain}']",
pattern_type="stix",
valid_from=datetime.now(),
indicator_types=["malicious-activity"],
confidence=confidence,
object_marking_refs=[marking],
))
for url in iocs.get("url", []):
escaped = url.replace("'", "\\'")
stix_objects.append(Indicator(
name=f"악성 URL: {url[:60]}",
pattern=f"[url:value = '{escaped}']",
pattern_type="stix",
valid_from=datetime.now(),
indicator_types=["malicious-activity"],
confidence=confidence,
object_marking_refs=[marking],
))
for sha256 in iocs.get("sha256", []):
stix_objects.append(Indicator(
name=f"악성 파일 해시: {sha256[:16]}...",
pattern=f"[file:hashes.'SHA-256' = '{sha256}']",
pattern_type="stix",
valid_from=datetime.now(),
indicator_types=["malicious-activity"],
confidence=confidence,
object_marking_refs=[marking],
))
bundle = Bundle(objects=stix_objects)
print(f"[+] {len(stix_objects)}개의 지표로 STIX 번들 생성됨")
return bundle
converter = STIXConverter()
stix_bundle = converter.iocs_to_stix(iocs, tlp="amber", confidence=80)
with open("iocs_stix_bundle.json", "w") as f:
f.write(stix_bundle.serialize(pretty=True))
단계 4: MISP 및 TAXII를 통해 배포
import requests
import json
class IOCDistributor:
"""다양한 채널을 통해 IOC를 배포합니다."""
def push_to_misp(self, iocs, misp_url, misp_key, event_info):
"""IOC를 MISP에 새 이벤트로 푸시합니다."""
headers = {
"Authorization": misp_key,
"Content-Type": "application/json",
"Accept": "application/json",
}
event = {
"Event": {
"info": event_info,
"distribution": "1", # 이 커뮤니티만
"threat_level_id": "2", # 중간
"analysis": "2", # 완료됨
"Attribute": [],
}
}
type_mapping = {
"ipv4": "ip-dst",
"domain": "domain",
"url": "url",
"email": "email-src",
"md5": "md5",
"sha1": "sha1",
"sha256": "sha256",
}
for ioc_type, values in iocs.items():
misp_type = type_mapping.get(ioc_type)
if misp_type:
for value in values:
event["Event"]["Attribute"].append({
"type": misp_type,
"value": value,
"category": "Network activity" if ioc_type in ("ipv4", "domain", "url") else "Payload delivery",
"to_ids": True,
})
resp = requests.post(
f"{misp_url}/events",
headers=headers,
json=event,
verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true", # 실습 환경에서 자체 서명 인증서 사용 시 SKIP_TLS_VERIFY=true 설정
)
if resp.status_code == 200:
event_id = resp.json().get("Event", {}).get("id", "")
print(f"[+] MISP 이벤트 생성됨: {event_id}")
return event_id
else:
print(f"[-] MISP 오류: {resp.status_code} - {resp.text[:200]}")
return None
def push_to_taxii(self, stix_bundle, taxii_url, collection_id, username, password):
"""STIX 번들을 TAXII 2.1 컬렉션으로 푸시합니다."""
from taxii2client.v21 import Collection
collection = Collection(
f"{taxii_url}/collections/{collection_id}/",
user=username, password=password,
)
response = collection.add_objects(stix_bundle.serialize())
print(f"[+] TAXII: 번들 게시됨, 상태: {response.status}")
return response
distributor = IOCDistributor()
distributor.push_to_misp(
iocs,
misp_url="https://misp.organization.com",
misp_key="YOUR_MISP_API_KEY",
event_info="악성코드 캠페인 IOC - 2025",
)
검증 기준
- IOC가 리펜징 지원과 함께 자유 텍스트에서 올바르게 추출됨
- 디펜징이 안전하고 클릭할 수 없는 지표를 생성함
- STIX 2.1 번들에 유효한 Indicator 패턴이 포함됨
- IOC가 MISP 및 TAXII로 성공적으로 배포됨
- 중복 제거로 중복 지표 방지
- 화이트리스트가 알려진 양호한 도메인에 대한 오탐 방지