DNSTwist로 타이포스쿼팅 도메인 분석은(는) 무엇을 하나요?
개요
DNSTwist는 유사한 도메인 이름을 생성하여 타이포스쿼팅, 호모그래프 피싱 공격 및 브랜드 사칭을 탐지하는 도메인 이름 순열 엔진입니다. 문자 치환, 전치, 삽입, 생략, 호모글리프 대체 등 다양한 기법을 사용하여 수천 개의 도메인 변형을 생성한 후, DNS 레코드(A, AAAA, NS, MX)를 확인하고 퍼지 해싱(ssdeep) 및 지각 해싱(pHash)을 통해 웹 페이지 유사도를 계산하며 잠재적으로 악성인 등록 도메인을 식별합니다.
사용 시기
- dnstwist를 사용한 타이포스쿼팅 도메인 분석이 필요한 보안 사고를 조사할 때
- 이 도메인에 대한 탐지 규칙 또는 위협 헌팅 쿼리를 구축할 때
- SOC 분석가가 이 분석 유형에 대한 체계적인 절차를 필요로 할 때
- 관련 공격 기법에 대한 보안 모니터링 적용 범위를 검증할 때
사전 요구 사항
- Python 3.9+ 및
dnstwist설치 (pip install dnstwist[full]) - 선택 사항: IP 지리적 위치를 위한 GeoIP 데이터베이스
- 선택 사항: 보강을 위한 Shodan API 키
- DNS 쿼리를 수행하기 위한 네트워크 액세스
- DNS 레코드 유형 및 도메인 등록에 대한 이해
주요 개념
도메인 순열 기법
DNSTwist는 다음 기법을 사용하여 순열을 생성합니다: 추가(문자 덧붙이기), 비트스쿼팅(비트 플립 오류), 호모글리프(시각적으로 유사한 유니코드 문자, 예: rn vs m), 하이픈 넣기(하이픈 추가), 삽입(문자 삽입), 생략(문자 제거), 반복(문자 반복), 대체(인접한 키보드 키로 대체), 하위 도메인(점 삽입), 전치(인접 문자 교환), 모음 교체(모음 교환), 사전 기반(일반 단어 덧붙이기).
퍼지 해싱 및 시각적 유사성
DNSTwist는 ssdeep(위치 기반 해시)을 사용하여 HTML 콘텐츠를 비교하고 pHash(지각 해시)를 사용하여 웹 페이지 스크린샷을 비교합니다. 이는 합법적인 사이트를 시각적으로 모방하는 클론 피싱 사이트를 식별하는 데 도움이 됩니다. 높은 유사성 점수는 피싱 페이지일 가능성이 높음을 나타냅니다.
탐지 워크플로우
일반적인 워크플로우는 다음과 같습니다: 도메인 순열 생성 -> DNS 레코드 확인 -> 등록 도메인 확인 -> 웹 페이지 유사성 비교 -> 의심스러운 도메인 플래그 지정 -> 보안 팀에 경고 -> 제거 요청. 일반적인 기업 도메인의 경우 dnstwist는 5,000~10,000개의 순열을 생성합니다.
워크플로우
1단계: 기본 도메인 순열 스캔
import subprocess
import json
import csv
from datetime import datetime
def run_dnstwist_scan(domain, output_file=None):
"""대상 도메인에 대해 dnstwist 스캔을 실행합니다."""
cmd = [
"dnstwist",
"--registered", # 등록된 도메인만 표시
"--format", "json", # JSON 형식으로 출력
"--nameservers", "8.8.8.8,1.1.1.1",
"--threads", "50",
"--mxcheck", # MX 레코드 확인
"--ssdeep", # 퍼지 해시 비교
"--geoip", # GeoIP 조회
domain,
]
print(f"[*] {domain}에 대한 순열 스캔 중...")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
if result.returncode == 0:
results = json.loads(result.stdout)
registered = [r for r in results if r.get("dns_a") or r.get("dns_aaaa")]
print(f"[+] 등록된 유사 도메인 {len(registered)}개 발견")
if output_file:
with open(output_file, "w") as f:
json.dump(registered, f, indent=2)
print(f"[+] 결과가 {output_file}에 저장되었습니다")
return registered
else:
print(f"[-] dnstwist 오류: {result.stderr}")
return []
results = run_dnstwist_scan("example.com", "typosquat_results.json")
2단계: 결과 분석 및 우선순위 지정
def analyze_results(results, legitimate_ips=None):
"""dnstwist 결과를 분석하고 위협 우선순위를 지정합니다."""
legitimate_ips = legitimate_ips or set()
high_risk = []
medium_risk = []
low_risk = []
for entry in results:
domain = entry.get("domain", "")
fuzzer = entry.get("fuzzer", "")
dns_a = entry.get("dns_a", [])
dns_mx = entry.get("dns_mx", [])
ssdeep_score = entry.get("ssdeep_score", 0)
risk_score = 0
risk_factors = []
# 합법적인 사이트와의 높은 유사성
if ssdeep_score and ssdeep_score > 50:
risk_score += 40
risk_factors.append(f"높은 웹 유사성 ({ssdeep_score}%)")
# MX 레코드가 있음 (이메일 수신 / 피싱 가능)
if dns_mx:
risk_score += 20
risk_factors.append("MX 레코드 있음 (이메일 가능)")
# 최근에 등록됨 (whois 데이터가 있는 경우)
whois_created = entry.get("whois_created", "")
if whois_created:
try:
created = datetime.fromisoformat(whois_created.replace("Z", "+00:00"))
age_days = (datetime.now(created.tzinfo) - created).days
if age_days < 30:
risk_score += 30
risk_factors.append(f"최근 등록 ({age_days}일 전)")
elif age_days < 90:
risk_score += 15
risk_factors.append(f"{age_days}일 전 등록")
except (ValueError, TypeError):
pass
# 호모글리프 공격은 가장 높은 위험
if fuzzer == "homoglyph":
risk_score += 25
risk_factors.append("호모글리프 (시각적으로 동일)")
elif fuzzer in ("addition", "replacement", "transposition"):
risk_score += 10
risk_factors.append(f"순열 유형: {fuzzer}")
# 합법적인 인프라를 가리키지 않음
if dns_a and not set(dns_a).intersection(legitimate_ips):
risk_score += 10
risk_factors.append("합법적인 IP와 다른 IP")
entry["risk_score"] = risk_score
entry["risk_factors"] = risk_factors
if risk_score >= 50:
high_risk.append(entry)
elif risk_score >= 25:
medium_risk.append(entry)
else:
low_risk.append(entry)
high_risk.sort(key=lambda x: x["risk_score"], reverse=True)
medium_risk.sort(key=lambda x: x["risk_score"], reverse=True)
print(f"\n=== 타이포스쿼팅 분석 ===")
print(f"높은 위험: {len(high_risk)}")
print(f"중간 위험: {len(medium_risk)}")
print(f"낮은 위험: {len(low_risk)}")
if high_risk:
print(f"\n--- 높은 위험 도메인 ---")
for entry in high_risk[:10]:
print(f" {entry['domain']} (점수: {entry['risk_score']})")
for factor in entry['risk_factors']:
print(f" - {factor}")
return {"high": high_risk, "medium": medium_risk, "low": low_risk}
analysis = analyze_results(results, legitimate_ips={"93.184.216.34"})
3단계: 지속적 모니터링 파이프라인
import time
import hashlib
class TyposquatMonitor:
def __init__(self, domains, known_domains_file="known_typosquats.json"):
self.domains = domains
self.known_file = known_domains_file
self.known_domains = self._load_known()
def _load_known(self):
try:
with open(self.known_file, "r") as f:
return json.load(f)
except FileNotFoundError:
return {}
def _save_known(self):
with open(self.known_file, "w") as f:
json.dump(self.known_domains, f, indent=2)
def scan_all_domains(self):
"""모니터링 중인 모든 도메인에서 새로운 타이포스쿼트를 스캔합니다."""
new_findings = []
for domain in self.domains:
results = run_dnstwist_scan(domain)
for entry in results:
domain_key = entry.get("domain", "")
if domain_key not in self.known_domains:
entry["first_seen"] = datetime.now().isoformat()
entry["monitored_domain"] = domain
self.known_domains[domain_key] = entry
new_findings.append(entry)
print(f" [NEW] {domain_key} ({entry.get('fuzzer', '')})")
self._save_known()
print(f"\n[+] 새로운 타이포스쿼팅 도메인 발견: {len(new_findings)}개")
return new_findings
def generate_alert(self, findings):
"""새로운 고위험 타이포스쿼팅 도메인에 대한 경고를 생성합니다."""
analysis = analyze_results(findings)
alerts = []
for entry in analysis["high"]:
alerts.append({
"severity": "HIGH",
"domain": entry["domain"],
"target": entry.get("monitored_domain", ""),
"risk_score": entry["risk_score"],
"risk_factors": entry["risk_factors"],
"dns_a": entry.get("dns_a", []),
"dns_mx": entry.get("dns_mx", []),
"timestamp": datetime.now().isoformat(),
})
return alerts
monitor = TyposquatMonitor(["mycompany.com", "mycompany.org"])
new_findings = monitor.scan_all_domains()
alerts = monitor.generate_alert(new_findings)
4단계: 차단 목록 및 제거 요청 내보내기
def export_blocklist(analysis, output_file="blocklist.txt"):
"""고위험 도메인을 방화벽/프록시용 차단 목록으로 내보냅니다."""
domains = []
for entry in analysis["high"] + analysis["medium"]:
domain = entry.get("domain", "")
if domain:
domains.append(domain)
with open(output_file, "w") as f:
f.write(f"# {datetime.now().isoformat()}에 생성된 타이포스쿼팅 차단 목록\n")
for d in sorted(set(domains)):
f.write(f"{d}\n")
print(f"[+] 차단 목록 저장됨: {len(domains)}개 도메인 -> {output_file}")
return domains
def generate_takedown_report(high_risk_domains):
"""제거 요청 보고서를 생성합니다."""
report = f"""# 도메인 제거 요청
생성일: {datetime.now().isoformat()}
## 요약
{len(high_risk_domains)}개의 도메인이 잠재적 타이포스쿼팅/피싱으로 식별되었습니다.
## 제거가 필요한 도메인
"""
for entry in high_risk_domains:
report += f"""
### {entry['domain']}
- **순열 유형**: {entry.get('fuzzer', 'unknown')}
- **IP 주소**: {', '.join(entry.get('dns_a', ['N/A']))}
- **MX 레코드**: {', '.join(entry.get('dns_mx', ['N/A']))}
- **위험 점수**: {entry.get('risk_score', 0)}
- **위험 요소**: {'; '.join(entry.get('risk_factors', []))}
- **웹 유사성**: {entry.get('ssdeep_score', 'N/A')}%
"""
with open("takedown_report.md", "w") as f:
f.write(report)
print("[+] 제거 요청 보고서 생성됨: takedown_report.md")
export_blocklist(analysis)
generate_takedown_report(analysis["high"])
검증 기준
- DNSTwist가 대상 도메인에 대한 도메인 순열을 생성함
- DNS 해석이 등록된 유사 도메인을 식별함
- 웹 유사성 점수가 클론 피싱 페이지를 탐지함
- 위험 점수 계산이 위협 수준별로 도메인 우선순위를 지정함
- 지속적 모니터링이 새로 등록된 타이포스쿼트를 탐지함
- 차단 목록 및 제거 요청 보고서가 올바르게 생성됨