๋ณด์ ์ ๋ฌด๋ฅผ ํ๋ค ๋ณด๋ฉด ์์ฌ์ค๋ฌ์ด IP๋ ๋๋ฉ์ธ์ ๋ง์ฃผ์น๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค. "์ด IP๊ฐ ์ ์ฑ์ธ์ง ์๋์ง" ํ์ธํ๊ธฐ ์ํด ๋งค๋ฒ VirusTotal ์ฌ์ดํธ์ ์ง์ ๋ค์ด๊ฐ์ ๋ณต๋ถํ๋ ์์ ์ด ๋ฐ๋ณต๋๋ค.
ํ๋ฃจ์ ์์ญ ๊ฐ๋ฅผ ํ์ธํด์ผ ํ๋ ์ํฉ์ด๋ผ๋ฉด? ๊ทธ๋ฅ ์์ผ๋ก ํ๋ ๊ฑด ํ๊ณ๊ฐ ์๋ค.
์ด ๊ธ์์๋ Python์ผ๋ก VirusTotal API๋ฅผ ํธ์ถํด์ IPยท๋๋ฉ์ธยทํ์ผ ํด์์ ์ ์ฑ ์ฌ๋ถ๋ฅผ ์๋์ผ๋ก ์กฐํํ๊ณ , ๊ฒฐ๊ณผ๋ฅผ Slack์ผ๋ก ๋ฐ์กํ๋ ์คํฌ๋ฆฝํธ๋ฅผ ์ฒ์๋ถํฐ ๋ง๋ค์ด๋ณธ๋ค. ๋ณด์ ์๋ํ์ ์ ๋ฌธํ๋ ค๋ ์ฌ๋์ด๋ผ๋ฉด ์ด ๊ธ ํ๋๋ก ์ค์ ์์ ๋ฐ๋ก ์ธ ์ ์๋ ์ฝ๋๋ฅผ ๊ฐ์ ธ๊ฐ ์ ์๋ค.
์ Python์ธ๊ฐ?
๋ณด์ ์๋ํ ๋๊ตฌ๋ฅผ ๋ง๋ค ๋ Python์ด ์ฌ์ค์ ํ์ค์ฒ๋ผ ์ฐ์ด๋ ๋ฐ๋ ์ด์ ๊ฐ ์๋ค.
requests, json, re ๊ฐ์ ๊ธฐ๋ณธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ง์ผ๋ก๋ HTTP API ํธ์ถ, ๋ฐ์ดํฐ ํ์ฑ, ์ ๊ท์ ์ฒ๋ฆฌ๊ฐ ์ ๋ถ ๊ฐ๋ฅํ๋ค. ๋ณ๋์ ๋ณต์กํ ๋น๋ ํ๊ฒฝ ์์ด ์คํฌ๋ฆฝํธ ํ๋๋ก ๋๋๋ค.
๋ณด์ ๋ถ์ผ์์ Python์ด ๊ฐ์ธ์ธ ๋ ๋ค๋ฅธ ์ด์ ๋ ์ํ๊ณ๋ค. MISP, Cortex, TheHive, ๊ทธ๋ฆฌ๊ณ ๊ฐ์ข SOAR ํ๋ซํผ์ ์ปค๋ฅํฐ๊ฐ Python์ผ๋ก ์์ฑ๋์ด ์๋ค. VirusTotal, Shodan, AbuseIPDB, AlienVault OTX ๊ฐ์ ์ํ ์ธํ ๋ฆฌ์ ์ค ํ๋ซํผ๋ Python SDK๋ฅผ ๊ณต์ ์ง์ํ๋ค.1
์ฆ, Python์ ๋ฐฐ์ฐ๋ฉด ๋ณด์ ์๋ํ์ ์ง์ ์ฅ๋ฒฝ์ด ํจ์ฌ ๋ฎ์์ง๋ค.
VirusTotal์ด๋?
VirusTotal์ Google์ด ์ธ์ํ ์ํ ์ธํ ๋ฆฌ์ ์ค ํ๋ซํผ์ด๋ค. 70๊ฐ ์ด์์ ๋ณด์ ๋ฒค๋(Kaspersky, McAfee, CrowdStrike ๋ฑ)์ ์์ง์ด ๋ชจ์ฌ ์์ด์, ํ์ผ ํ๋๋ฅผ ์ฌ๋ฆฌ๋ฉด ๋ชจ๋ ์์ง์ผ๋ก ๋์์ ๊ฒ์ฌํ๋ค.2
์ฃผ์ ์กฐํ ๋์์ ์ธ ๊ฐ์ง๋ค:
| ๋์ | ์์ | ํ์ฉ ์ฌ๋ก |
|---|---|---|
| IP ์ฃผ์ | 8.8.8.8 |
์ ์ฑ C2 ์๋ฒ ์ฌ๋ถ ํ์ธ |
| ๋๋ฉ์ธ | malware-site.com |
ํผ์ฑ ๋๋ฉ์ธ ํ์ธ |
| ํ์ผ ํด์ | MD5, SHA-256 | ์ ์ฑ ํ์ผ ์ฌ๋ถ ํ์ธ |
๋ฌด๋ฃ ๊ณ์ ์ผ๋ก ํ๋ฃจ 500ํ, ๋ถ๋น 4ํ ์กฐํ๊ฐ ๊ฐ๋ฅํ๋ค. ์๊ท๋ชจ ์๋ํ์๋ ์ถฉ๋ถํ๋ค.3
์ค๋น
API ํค ๋ฐ๊ธ
- VirusTotal ํ์๊ฐ์
- ์ฐ์ธก ์๋จ ํ๋กํ โ API key ํด๋ฆญ
- API ํค ๋ณต์ฌ
ํจํค์ง ์ค์น
pip install requests python-dotenvํ๋ก์ ํธ ๊ตฌ์กฐ
vt-checker/
โโโ .env โ API ํค ์ ์ฅ (์ ๋ ์ปค๋ฐ ๊ธ์ง)
โโโ checker.py โ ๋ฉ์ธ ์คํฌ๋ฆฝํธ
โโโ .gitignore.env ํ์ผ:
VT_API_KEY=your_virustotal_api_key_here
SLACK_WEBHOOK_URL=your_slack_webhook_url_hereVirusTotal API ๊ธฐ๋ณธ ๊ตฌ์กฐ
VirusTotal API v3๋ REST ๋ฐฉ์์ด๋ค. ๊ณตํต ๊ตฌ์กฐ๋ ์ด๋ ๋ค:
GET https://www.virustotal.com/api/v3/{๋ฆฌ์์ค ํ์
}/{์๋ณ์}์์:
| ์กฐํ ๋์ | ์๋ํฌ์ธํธ |
|---|---|
| IP ์ฃผ์ | GET /api/v3/ip_addresses/{ip} |
| ๋๋ฉ์ธ | GET /api/v3/domains/{domain} |
| ํ์ผ ํด์ | GET /api/v3/files/{hash} |
์ธ์ฆ์ ํค๋์ API ํค๋ฅผ ๋ฃ๋ ๋ฐฉ์์ด๋ค:
headers = {
"x-apikey": "YOUR_API_KEY"
}์๋ต ๊ตฌ์กฐ๋ JSON์ด๊ณ , ํต์ฌ์ last_analysis_stats ํ๋๋ค:
{
"data": {
"attributes": {
"last_analysis_stats": {
"malicious": 5,
"suspicious": 1,
"undetected": 60,
"harmless": 8,
"timeout": 0
}
}
}
}malicious ๊ฐ์ด ๋์์๋ก ์
์ฑ ๊ฐ๋ฅ์ฑ์ด ๋๋ค. ์ผ๋ฐ์ ์ผ๋ก 3 ์ด์์ด๋ฉด ์
์ฑ์ผ๋ก ํ๋จํ๋ ๊ธฐ์ค์ ๋ง์ด ์ด๋ค.
์ฝ๋ ์์ฑ
1๋จ๊ณ: ๊ธฐ๋ณธ IP ์กฐํ
import requests
import os
from dotenv import load_dotenv
load_dotenv()
VT_API_KEY = os.getenv("VT_API_KEY")
BASE_URL = "https://www.virustotal.com/api/v3"
HEADERS = {"x-apikey": VT_API_KEY}
def check_ip(ip: str) -> dict:
"""IP ์ฃผ์ ์
์ฑ ์ฌ๋ถ ์กฐํ"""
url = f"{BASE_URL}/ip_addresses/{ip}"
response = requests.get(url, headers=HEADERS)
if response.status_code == 200:
data = response.json()
stats = data["data"]["attributes"]["last_analysis_stats"]
return {
"type": "ip",
"indicator": ip,
"malicious": stats.get("malicious", 0),
"suspicious": stats.get("suspicious", 0),
"harmless": stats.get("harmless", 0),
"undetected": stats.get("undetected", 0),
}
elif response.status_code == 404:
return {"type": "ip", "indicator": ip, "error": "Not found"}
else:
return {"type": "ip", "indicator": ip, "error": f"HTTP {response.status_code}"}2๋จ๊ณ: ๋๋ฉ์ธ๊ณผ ํด์ ์ถ๊ฐ
๊ตฌ์กฐ๊ฐ ๊ฑฐ์ ๊ฐ์์ ํจ์๋ฅผ ๋ฌถ์ด์ ์ฒ๋ฆฌํ ์ ์๋ค:
def check_domain(domain: str) -> dict:
"""๋๋ฉ์ธ ์
์ฑ ์ฌ๋ถ ์กฐํ"""
url = f"{BASE_URL}/domains/{domain}"
return _parse_response(url, "domain", domain)
def check_hash(file_hash: str) -> dict:
"""ํ์ผ ํด์ ์
์ฑ ์ฌ๋ถ ์กฐํ"""
url = f"{BASE_URL}/files/{file_hash}"
return _parse_response(url, "hash", file_hash)
def _parse_response(url: str, indicator_type: str, indicator: str) -> dict:
"""๊ณตํต ์๋ต ํ์ฑ"""
response = requests.get(url, headers=HEADERS)
if response.status_code == 200:
stats = (
response.json()["data"]["attributes"]["last_analysis_stats"]
)
return {
"type": indicator_type,
"indicator": indicator,
"malicious": stats.get("malicious", 0),
"suspicious": stats.get("suspicious", 0),
"harmless": stats.get("harmless", 0),
"undetected": stats.get("undetected", 0),
}
elif response.status_code == 404:
return {"type": indicator_type, "indicator": indicator, "error": "Not found"}
else:
return {
"type": indicator_type,
"indicator": indicator,
"error": f"HTTP {response.status_code}",
}3๋จ๊ณ: ์ ์ฑ ํ๋จ ๋ก์ง
๋จ์ํ ์ซ์๋ง ๋ณด๋ ๊ฒ ์๋๋ผ, ํ๋จ ๊ธฐ์ค์ ๋ช ํํ ์ ์ํด๋๋ ๊ฒ ์ข๋ค:
def judge(result: dict) -> str:
"""
malicious >= 5 โ MALICIOUS (์
์ฑ)
malicious >= 1 โ SUSPICIOUS (์์ฌ)
malicious == 0 โ CLEAN (์ ์)
"""
if "error" in result:
return "UNKNOWN"
malicious = result.get("malicious", 0)
suspicious = result.get("suspicious", 0)
if malicious >= 5:
return "MALICIOUS"
if malicious >= 1 or suspicious >= 3:
return "SUSPICIOUS"
return "CLEAN"์๊ณ๊ฐ(5, 1, 3)์ ํ๊ฒฝ์ ๋ฐ๋ผ ์กฐ์ ํ๋ฉด ๋๋ค. ๋ด๋ถ๋ง IP๋ฅผ ์กฐํํ ๋๋ false positive๊ฐ ๋ง์ผ๋ ์๊ณ๊ฐ์ ๋์ด๋ ์์ผ๋ก.
4๋จ๊ณ: Slack ์๋ฆผ
import json
SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL")
VERDICT_EMOJI = {
"MALICIOUS": ":red_circle:",
"SUSPICIOUS": ":large_yellow_circle:",
"CLEAN": ":white_check_mark:",
"UNKNOWN": ":white_circle:",
}
VERDICT_COLOR = {
"MALICIOUS": "#e74c3c",
"SUSPICIOUS": "#e67e22",
"CLEAN": "#27ae60",
"UNKNOWN": "#7f8c8d",
}
def send_slack(result: dict, verdict: str) -> None:
"""Slack ์นํ
์ผ๋ก ์กฐํ ๊ฒฐ๊ณผ ๋ฐ์ก"""
if not SLACK_WEBHOOK_URL:
print("[!] SLACK_WEBHOOK_URL ๋ฏธ์ค์ โ Slack ์๋ฆผ ๊ฑด๋๋๋๋ค.")
return
emoji = VERDICT_EMOJI.get(verdict, ":white_circle:")
color = VERDICT_COLOR.get(verdict, "#7f8c8d")
indicator = result["indicator"]
if "error" in result:
text = f"{emoji} *{indicator}* โ ์กฐํ ์คํจ: {result['error']}"
fields = []
else:
text = f"{emoji} *{indicator}* โ {verdict}"
fields = [
{"title": "Malicious", "value": str(result["malicious"]), "short": True},
{"title": "Suspicious", "value": str(result["suspicious"]), "short": True},
{"title": "Harmless", "value": str(result["harmless"]), "short": True},
{"title": "Undetected", "value": str(result["undetected"]), "short": True},
]
payload = {
"attachments": [
{
"color": color,
"text": text,
"fields": fields,
"footer": "VirusTotal API",
}
]
}
requests.post(
SLACK_WEBHOOK_URL,
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
)5๋จ๊ณ: ์ ์ฒด ํตํฉ
import time
def check_and_notify(indicator: str) -> None:
"""ํ์
์๋ ๊ฐ์ง ํ ์กฐํ ๋ฐ ์๋ฆผ"""
import re
# ํ์
ํ๋จ
ip_pattern = re.compile(
r"^(\d{1,3}\.){3}\d{1,3}$"
)
hash_pattern = re.compile(
r"^[a-fA-F0-9]{32}$|^[a-fA-F0-9]{40}$|^[a-fA-F0-9]{64}$"
)
indicator = indicator.strip()
if ip_pattern.match(indicator):
result = check_ip(indicator)
elif hash_pattern.match(indicator):
result = check_hash(indicator)
else:
result = check_domain(indicator)
verdict = judge(result)
print(f"[{verdict}] {indicator}")
send_slack(result, verdict)
def batch_check(indicators: list, delay: float = 15.0) -> None:
"""
์ฌ๋ฌ ์งํ ์ผ๊ด ์กฐํ
delay: ์์ฒญ ๊ฐ๊ฒฉ (๋ฌด๋ฃ ๊ณ์ ์ ๋ถ๋น 4ํ = ์ต์ 15์ด)
"""
for i, indicator in enumerate(indicators):
print(f"[{i+1}/{len(indicators)}] {indicator} ์กฐํ ์ค...")
check_and_notify(indicator)
if i < len(indicators) - 1:
time.sleep(delay)
# โโ ์คํ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if __name__ == "__main__":
targets = [
"8.8.8.8",
"185.220.101.45", # ์๋ ค์ง Tor exit node
"malware-test.com",
"44d88612fea8a8f36de82e1278abb02f", # EICAR ํ
์คํธ ํด์ (MD5)
]
batch_check(targets)์ ์ฒด ๋์ ํ๋ฆ
์คํ ๊ฒฐ๊ณผ ์์
[1/4] 8.8.8.8 ์กฐํ ์ค...
[CLEAN] 8.8.8.8
[2/4] 185.220.101.45 ์กฐํ ์ค...
[MALICIOUS] 185.220.101.45
[3/4] malware-test.com ์กฐํ ์ค...
[SUSPICIOUS] malware-test.com
[4/4] 44d88612fea8a8f36de82e1278abb02f ์กฐํ ์ค...
[MALICIOUS] 44d88612fea8a8f36de82e1278abb02fSlack์๋ ์ด๋ฐ ์์ผ๋ก ๋ฐ์ก๋๋ค:
- ๐ด
185.220.101.45โ MALICIOUS | Malicious: 18 | Suspicious: 3 - ๐ก
malware-test.comโ SUSPICIOUS | Malicious: 2 | Suspicious: 1 - โ
8.8.8.8โ CLEAN | Malicious: 0 | Harmless: 76
Rate Limit ์ฒ๋ฆฌ
๋ฌด๋ฃ ๊ณ์ ์ ๋ถ๋น 4ํ, ํ๋ฃจ 500ํ ์ ํ์ด ์๋ค.3 ์ด๋ฅผ ์ด๊ธฐ๋ฉด 429 Too Many Requests๊ฐ ๋์์จ๋ค.
์ ์ฝ๋์์ delay=15.0์ผ๋ก ์ค์ ํ ๊ฒ ๊ทธ ์ด์ ๋ค. 15์ด ๊ฐ๊ฒฉ์ด๋ฉด ๋ถ๋น 4ํ ์ด๋ด๋ค.
๋ ๊ฒฌ๊ณ ํ๊ฒ ๋ง๋ค๋ ค๋ฉด ์ฌ์๋ ๋ก์ง์ ์ถ๊ฐํ ์ ์๋ค:
def safe_request(url: str, headers: dict, retries: int = 3) -> requests.Response:
"""429 ๋ฐ์ ์ ์๋ ์ฌ์๋"""
for attempt in range(retries):
response = requests.get(url, headers=headers)
if response.status_code == 429:
wait = 60 * (attempt + 1)
print(f"[!] Rate limit ๋๋ฌ. {wait}์ด ๋๊ธฐ ํ ์ฌ์๋...")
time.sleep(wait)
else:
return response
return responseํ์ฅ ์์ด๋์ด
์ด ์คํฌ๋ฆฝํธ๋ ์์์ ์ด๋ค. ์ฌ๊ธฐ์ ์ฌ๋ฌ ๋ฐฉํฅ์ผ๋ก ํ์ฅํ ์ ์๋ค.
1. CSV/ํ ์คํธ ํ์ผ ์ ๋ ฅ
๋ถ์ํ ์งํ๊ฐ ๋ง์ ๋:
with open("indicators.txt", "r") as f:
targets = [line.strip() for line in f if line.strip()]
batch_check(targets)2. ๊ฒฐ๊ณผ๋ฅผ CSV๋ก ์ ์ฅ
import csv
import datetime
def save_to_csv(results: list, filename: str = None) -> None:
if not filename:
today = datetime.date.today().isoformat()
filename = f"vt_results_{today}.csv"
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(
f,
fieldnames=["indicator", "type", "verdict",
"malicious", "suspicious", "harmless", "undetected"]
)
writer.writeheader()
writer.writerows(results)
print(f"[*] ๊ฒฐ๊ณผ ์ ์ฅ: {filename}")3. SOAR ํ๋ ์ด๋ถ๊ณผ ์ฐ๋
SOAR ํ๋ซํผ(FortiSOAR, Palo Alto XSOAR ๋ฑ)์์ Python ์คํฌ๋ฆฝํธ๋ฅผ ํ๋ ์ด๋ถ ์คํ
์ผ๋ก ์ฝ์
ํ ์ ์๋ค. ์ ์ฝ๋์์ check_and_notify() ํจ์๋ฅผ ๊ทธ๋๋ก ์ปค๋ฅํฐ ์ก์
์ผ๋ก ๋ํํ๋ฉด ๋๋ค.
์ธ์๋ํธ์์ ์ถ์ถํ IOC๋ฅผ ํ๋ ์ด๋ถ์ด ์๋์ผ๋ก ์ด ํจ์์ ๋๊ธฐ๊ณ , ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์ฐจ๋จ/์์ค์ปฌ๋ ์ด์ ์ ๊ฒฐ์ ํ๋ ์์ด๋ค. ํ๋ ์ด๋ถ ํ๋๋ก IOC ์์ง๋ถํฐ ํํ ์กฐํ, ๋์๊น์ง ์๋ํ๋๋ ๊ตฌ์กฐ๋ค.
์ฃผ์์ฌํญ
API ํค๋ ์ ๋ ์ฝ๋์ ์ง์ ๋ฃ์ง ์๋๋ค. .env ํ์ผ์ ์ฌ์ฉํ๊ณ , .gitignore์ ๋ฐ๋์ ์ถ๊ฐํด์ผ ํ๋ค:
# .gitignore
.env
*.envGitHub์ API ํค๊ฐ ์ฌ๋ผ๊ฐ๋ฉด ์๋์ผ๋ก ๊ฐ์ง๋์ด ํค๊ฐ ์ฆ์ ๋ฌดํจํ๋๊ณ ์ด๋ฉ์ผ ์๋ฆผ์ด ์จ๋ค. ํ์ง๋ง ํค๊ฐ ๋ ธ์ถ๋๋ ์๊ฐ ๋์ ๋ฌด๋จ ์ฌ์ฉ ๊ฐ๋ฅ์ฑ์ด ์์ผ๋ ์ฒ์๋ถํฐ ์กฐ์ฌํ๋ ๊ฒ ๋ง๋ค.
๋ง์น๋ฉฐ
์ด ์คํฌ๋ฆฝํธ ํ๋๋ก ํ ์ ์๋ ๊ฒ๋ค์ ์ ๋ฆฌํ๋ฉด ์ด๋ ๋ค:
| ์์ | ์๋ํ ์ | ์๋ํ ํ |
|---|---|---|
| IP 10๊ฐ ์กฐํ | ์๋ ๋ณต๋ถ 10ํ (์ฝ 5๋ถ) | ์คํฌ๋ฆฝํธ ์คํ 1ํ (์ฝ 2.5๋ถ ๋๊ธฐ) |
| ๊ฒฐ๊ณผ ๊ธฐ๋ก | ์ง์ ์์ ์์ฑ | CSV ์๋ ์ ์ฅ |
| ํ ๊ณต์ | ์ด๋ฉ์ผ/๋ฉ์์ง ์ง์ ๋ฐ์ก | Slack ์๋ ๋ฐ์ก |
๋ณด์ ์๋ํ์ ํต์ฌ์ ์ฌ๋์ด ํ๋จํด์ผ ํ๋ ๊ฒ๊ณผ ๊ธฐ๊ณ๊ฐ ์ฒ๋ฆฌํด์ผ ํ๋ ๊ฒ์ ๊ตฌ๋ถํ๋ ๋ฐ ์๋ค. IOC ํํ ์กฐํ์ฒ๋ผ ๋ฐ๋ณต์ ์ด๊ณ ๊ท์น ๊ธฐ๋ฐ์ธ ์์ ์ ์๋ํํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ์ผ๋ก ํ ์ค์ ํ๋จ๊ณผ ๋์์ ์ฌ๋์ ์๊ฐ์ ์ฐ๋ ๊ฒ์ด ๋ง๋ค.
๋ค์ ๊ธ์์๋ ์ด๋ฒ์ ๋ง๋ ์คํฌ๋ฆฝํธ๋ฅผ ํ์ฅํด์ AbuseIPDB, AlienVault OTX ๊ฐ์ ์ถ๊ฐ ์ํ ์ธํ ๋ฆฌ์ ์ค ์์ค์ ๋ฉํฐ์์ค ์กฐํ๋ฅผ ํตํฉํ๋ ๋ฐฉ๋ฒ์ ๋ค๋ค๋ณผ ์์ ์ด๋ค.
- VirusTotal. "VirusTotal API v3 Overview." VirusTotal Developer Documentation. https://developers.virustotal.com/reference/overviewโฉ
- VirusTotal. "How it works." VirusTotal. https://www.virustotal.com/gui/how-it-worksโฉ
- VirusTotal. "API Rate Limiting." VirusTotal Developer Documentation. https://developers.virustotal.com/reference/public-vs-premium-apiโฉ
