DNS 로그 분석을 통한 데이터 유출 탐지
사용 시기
이 스킬은 다음과 같은 경우에 사용합니다:
- SOC 팀이 방화벽/프록시 차단을 우회하는 DNS 터널링을 통한 데이터 유출을 의심할 때
- 위협 인텔리전스가 공격자가 DNS 기반 C2 채널(예: Cobalt Strike DNS 비콘)을 사용하고 있다고 나타낼 때
- UEBA가 특정 호스트에서 비정상적인 DNS 쿼리 볼륨을 감지할 때
- 악성코드 분석 결과 DNS-over-HTTPS(DoH) 또는 DNS 터널링 기능이 발견될 때
표준 DNS 문제 해결 또는 가용성 모니터링에는 사용하지 마십시오 — 이 스킬은 보안 관련 DNS 남용 탐지에 중점을 둡니다.
사전 요구 사항
- DNS 쿼리 로깅 활성화(Windows DNS Server, Bind, Infoblox, Cisco Umbrella)
- SIEM에 수집된 DNS 로그(Splunk의
Stream:DNS,dnssourcetype, 또는 Zeek DNS 로그) - 과거 도메인 해석 분석을 위한 패시브 DNS 데이터
- 정상 DNS 동작 기준(쿼리 볼륨, 도메인 분포, TXT 레코드 빈도)
- 엔트로피 계산을 위한 Python
math및collections라이브러리
작업 흐름
1단계: 서브도메인 길이 분석을 통한 DNS 터널링 탐지
DNS 터널링은 서브도메인 레이블에 데이터를 인코딩하여 비정상적으로 긴 쿼리를 생성합니다:
index=dns sourcetype="stream:dns" query_type IN ("A", "AAAA", "TXT", "CNAME", "MX")
| eval domain_parts = split(query, ".")
| eval subdomain = mvindex(domain_parts, 0, mvcount(domain_parts)-3)
| eval subdomain_str = mvjoin(subdomain, ".")
| eval subdomain_len = len(subdomain_str)
| eval tld = mvindex(domain_parts, -1)
| eval registered_domain = mvindex(domain_parts, -2).".".tld
| where subdomain_len > 50
| stats count AS queries, dc(query) AS unique_queries,
avg(subdomain_len) AS avg_subdomain_len,
max(subdomain_len) AS max_subdomain_len,
values(src_ip) AS sources
by registered_domain
| where queries > 20
| sort - avg_subdomain_len
| table registered_domain, queries, unique_queries, avg_subdomain_len, max_subdomain_len, sources
2단계: 고엔트로피 도메인 쿼리 탐지(DGA 탐지)
도메인 생성 알고리즘은 무작위로 보이는 도메인을 생성합니다:
index=dns sourcetype="stream:dns"
| eval domain_parts = split(query, ".")
| eval sld = mvindex(domain_parts, -2)
| eval sld_len = len(sld)
| eval char_count = sld_len
| eval vowels = len(replace(sld, "[^aeiou]", ""))
| eval consonants = len(replace(sld, "[^bcdfghjklmnpqrstvwxyz]", ""))
| eval digits = len(replace(sld, "[^0-9]", ""))
| eval vowel_ratio = if(char_count > 0, vowels / char_count, 0)
| eval digit_ratio = if(char_count > 0, digits / char_count, 0)
| where sld_len > 12 AND (vowel_ratio < 0.2 OR digit_ratio > 0.3)
| stats count AS queries, dc(query) AS unique_domains, values(src_ip) AS sources
by query
| where unique_domains > 10
| sort - queries
DNS 쿼리용 Python 기반 섀넌 엔트로피 계산:
import math
from collections import Counter
def shannon_entropy(text):
"""문자열의 섀넌 엔트로피 계산"""
if not text:
return 0
counter = Counter(text.lower())
length = len(text)
entropy = -sum(
(count / length) * math.log2(count / length)
for count in counter.values()
)
return round(entropy, 4)
# 예제로 테스트
normal_domain = "google" # 낮은 엔트로피
dga_domain = "x8kj2m9p4qw7n" # 높은 엔트로피
tunnel_subdomain = "aGVsbG8gd29ybGQ.evil.com" # Base64 인코딩 데이터
print(f"정상: {shannon_entropy(normal_domain)}") # ~2.25
print(f"DGA: {shannon_entropy(dga_domain)}") # ~3.70
print(f"터널: {shannon_entropy(tunnel_subdomain)}") # ~3.50
# 임계값: 서브도메인 엔트로피 > 3.5 = 터널링/DGA 가능성
엔트로피 점수에 대한 Splunk 구현:
index=dns sourcetype="stream:dns"
| eval domain_parts = split(query, ".")
| eval check_string = mvindex(domain_parts, 0)
| eval check_len = len(check_string)
| where check_len > 8
| eval chars = split(check_string, "")
| stats count AS total_chars, dc(chars) AS unique_chars by query, src_ip, check_string, check_len
| eval entropy_estimate = log(unique_chars, 2) * (unique_chars / check_len)
| where entropy_estimate > 3.5
| stats count AS high_entropy_queries, dc(query) AS unique_queries by src_ip
| where high_entropy_queries > 50
| sort - high_entropy_queries
3단계: 비정상적인 DNS 쿼리 볼륨 탐지
비정상적인 DNS 트래픽을 생성하는 호스트를 식별합니다:
index=dns sourcetype="stream:dns" earliest=-24h
| bin _time span=1h
| stats count AS queries, dc(query) AS unique_domains by src_ip, _time
| eventstats avg(queries) AS avg_queries, stdev(queries) AS stdev_queries by src_ip
| eval z_score = (queries - avg_queries) / stdev_queries
| where z_score > 3 OR queries > 5000
| sort - z_score
| table _time, src_ip, queries, unique_domains, avg_queries, z_score
TXT 레코드 남용 탐지(일반적인 터널링 방법):
index=dns sourcetype="stream:dns" query_type="TXT"
| stats count AS txt_queries, dc(query) AS unique_txt_domains,
values(query) AS domains by src_ip
| where txt_queries > 100
| eval suspicion = case(
txt_queries > 1000, "CRITICAL — DNS 터널링 가능성 높음",
txt_queries > 500, "HIGH — DNS 터널링 가능성 있음",
txt_queries > 100, "MEDIUM — 비정상적인 TXT 볼륨"
)
| sort - txt_queries
| table src_ip, txt_queries, unique_txt_domains, suspicion
4단계: 알려진 DNS 터널링 도구 탐지
일반적인 DNS 터널링 도구의 시그니처를 검색합니다:
index=dns sourcetype="stream:dns"
| eval query_lower = lower(query)
| where (
match(query_lower, "\.dnscat\.") OR
match(query_lower, "\.dns2tcp\.") OR
match(query_lower, "\.iodine\.") OR
match(query_lower, "\.dnscapy\.") OR
match(query_lower, "\.cobalt.*\.beacon") OR
query_type="NULL" OR
(query_type="TXT" AND len(query) > 100)
)
| stats count by src_ip, query, query_type
| sort - count
로컬 DNS를 우회하는 DNS over HTTPS(DoH) 탐지:
index=proxy OR index=firewall
dest IN ("1.1.1.1", "1.0.0.1", "8.8.8.8", "8.8.4.4",
"9.9.9.9", "149.112.112.112", "208.67.222.222")
dest_port=443
| stats sum(bytes_out) AS total_bytes, count AS connections by src_ip, dest
| where connections > 100 OR total_bytes > 10485760
| eval alert = "DoH 우회 가능성 — 공용 리졸버로 HTTPS를 통해 DNS 쿼리 전송"
| sort - total_bytes
5단계: DNS 발견 사항과 엔드포인트 데이터 상관 분석
의심스러운 DNS를 프로세스 데이터와 교차 참조:
index=dns src_ip="192.168.1.105" query="*.evil-tunnel.com" earliest=-24h
| stats count AS dns_queries, earliest(_time) AS first_query, latest(_time) AS last_query
by src_ip, query
| join src_ip [
search index=sysmon EventCode=3 DestinationPort=53 Computer="WORKSTATION-042"
| stats count AS connections, values(Image) AS processes by SourceIp
| rename SourceIp AS src_ip
]
| table src_ip, query, dns_queries, first_query, last_query, processes
6단계: 데이터 유출량 추정 계산
DNS 쿼리에 인코딩된 데이터 볼륨을 추정합니다:
index=dns src_ip="192.168.1.105" query="*.evil-tunnel.com" earliest=-24h
| eval domain_parts = split(query, ".")
| eval encoded_data = mvindex(domain_parts, 0)
| eval encoded_bytes = len(encoded_data)
| eval decoded_bytes = encoded_bytes * 0.75 -- Base64 디코딩 계수
| stats sum(decoded_bytes) AS total_bytes_estimated, count AS total_queries,
earliest(_time) AS first_seen, latest(_time) AS last_seen
| eval estimated_kb = round(total_bytes_estimated / 1024, 1)
| eval estimated_mb = round(total_bytes_estimated / 1048576, 2)
| eval duration_hours = round((last_seen - first_seen) / 3600, 1)
| eval rate_kbps = round(estimated_kb / (duration_hours * 3600) * 8, 2)
| table total_queries, estimated_mb, duration_hours, rate_kbps, first_seen, last_seen
주요 개념
| 용어 | 정의 |
|---|---|
| DNS 터널링 | DNS 쿼리/응답 내에 데이터를 인코딩하여 데이터를 유출하거나 DNS를 통해 C2 채널을 설정하는 기술 |
| DGA | 도메인 생성 알고리즘 — C2 복원력을 위해 의사 난수 도메인 이름을 생성하는 악성코드 기술 |
| 섀넌 엔트로피 | 문자열의 무작위성을 측정하는 수학적 척도 — 도메인 이름에서 높은 엔트로피(>3.5)는 DGA 또는 터널링을 나타냄 |
| TXT 레코드 남용 | 텍스트 데이터용으로 설계된 DNS TXT 레코드를 데이터 터널링의 고대역폭 채널로 사용 |
| DNS over HTTPS(DoH) | HTTPS(포트 443)를 통해 암호화된 DNS 쿼리로, 기존 DNS 모니터링을 우회 |
| 패시브 DNS | 시간 경과에 따라 도메인이 어떤 IP로 해석되었는지 보여주는 과거 DNS 해석 기록 |
도구 및 시스템
- Splunk Stream: SIEM 분석을 위해 파싱된 DNS 쿼리 데이터를 제공하는 네트워크 트래픽 캡처 애드온
- Zeek (Bro): 분석을 위한 상세한 DNS 트랜잭션 로그를 생성하는 네트워크 보안 모니터
- Cisco Umbrella (OpenDNS): 악성 도메인을 차단하고 쿼리 데이터를 기록하는 클라우드 DNS 보안 플랫폼
- Infoblox DNS Firewall: RPZ 기반 차단 및 상세 쿼리 로깅을 제공하는 DNS 계층 보안
- Farsight DNSDB: 과거 도메인 해석 조회 및 인프라 매핑을 위한 패시브 DNS 데이터베이스
일반적인 시나리오
- Cobalt Strike DNS 비콘: C2 도메인에 인코딩된 페이로드가 포함된 주기적인 TXT 쿼리 탐지
- 데이터 유출: Base64/16진수로 인코딩된 도난 데이터를 포함한 대량의 고유 서브도메인 쿼리
- DGA 악성코드: 알고리즘으로 생성된 도메인에 대한 DNS 쿼리 탐지(높은 엔트로피, 웹 콘텐츠 없음)
- DNS-over-HTTPS 우회: 직원이 회사 DNS 필터링 및 모니터링을 우회하기 위해 DoH 사용
- 느린 드립 유출: 임계값 알림 아래에 머무는 낮은 볼륨의 DNS 터널링(기준 비교 필요)
출력 형식
DNS 유출 분석 — WORKSTATION-042
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
기간: 2024-03-14 ~ 2024-03-15
출처: 192.168.1.105 (WORKSTATION-042, 재무 부서)
발견 사항:
[CRITICAL] evil-tunnel[.]com으로 DNS 터널링 탐지
쿼리 볼륨: 18시간 동안 12,847건의 쿼리
평균 서브도메인 길이: 63자(정상: <20)
평균 엔트로피: 3.82(임계값: 3.5)
쿼리 유형: TXT (89%), A (11%)
추정 데이터량: DNS를 통해 약 4.7MB 유출
전송 속도: 0.58kbps(느린 드립 패턴)
[HIGH] DGA 유사 도메인 해석
고유 DGA 도메인: 247개 도메인 해석
패턴: 15자 랜덤 알파벳숫자.xyz TLD
엔트로피 범위: 3.6 ~ 4.1
프로세스 귀속:
프로세스: svchost_update.exe(가장 — 정식 svchost 아님)
PID: 4892
상위 프로세스: explorer.exe
해시: SHA256: a1b2c3d4... (VT: 34/72 악성 — Cobalt Strike 비콘)
격리 조치:
[완료] EDR을 통해 호스트 격리
[완료] evil-tunnel[.]com 도메인 DNS 싱크홀에 추가
[완료] 사고 IR-2024-0448 생성