본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 27. · 2 Views
침해사고 대응 실무 완벽 가이드
보안 침해사고가 발생했을 때 초기 대응부터 디지털 포렌식, 침해 지표 추출까지 실무에서 바로 활용할 수 있는 파이썬 기반 대응 기법을 다룹니다. 초급 개발자도 이해할 수 있도록 실제 시나리오와 함께 설명합니다.
목차
1. 침해사고 초기 대응
월요일 아침, 김보안 씨가 출근하자마자 모니터링 시스템에서 경고음이 울렸습니다. "비정상적인 네트워크 트래픽 감지"라는 알림이 빨갛게 깜빡이고 있었습니다.
심장이 쿵쾅거리기 시작했습니다. 이제 무엇부터 해야 할까요?
침해사고 초기 대응은 보안 사고 발생 시 가장 먼저 수행하는 일련의 절차입니다. 마치 화재가 발생했을 때 119에 신고하고 대피하는 것처럼, 사이버 공격에도 정해진 초기 대응 절차가 있습니다.
이 단계에서 올바른 판단을 내려야 피해를 최소화하고 증거를 보존할 수 있습니다.
다음 코드를 살펴봅시다.
import datetime
import socket
import subprocess
import json
class IncidentResponse:
def __init__(self, incident_id):
self.incident_id = incident_id
self.timestamp = datetime.datetime.now()
self.hostname = socket.gethostname()
self.findings = []
def initial_triage(self):
# 시스템 상태 빠르게 파악
triage_data = {
"incident_id": self.incident_id,
"detection_time": self.timestamp.isoformat(),
"hostname": self.hostname,
"network_connections": self._get_network_connections(),
"running_processes": self._get_process_count(),
"logged_users": self._get_logged_users()
}
return triage_data
def _get_network_connections(self):
# 현재 네트워크 연결 상태 확인
result = subprocess.run(['netstat', '-an'], capture_output=True, text=True)
return len(result.stdout.split('\n'))
def _get_process_count(self):
result = subprocess.run(['ps', 'aux'], capture_output=True, text=True)
return len(result.stdout.split('\n')) - 1
def _get_logged_users(self):
result = subprocess.run(['who'], capture_output=True, text=True)
return result.stdout.strip().split('\n') if result.stdout.strip() else []
# 사용 예시
ir = IncidentResponse("INC-2024-001")
triage = ir.initial_triage()
print(json.dumps(triage, indent=2, ensure_ascii=False))
김보안 씨는 입사 6개월 차 보안 분석가입니다. 평소 공부한 대로 침해사고 대응을 해야 하는데, 막상 실제 상황이 닥치니 머릿속이 하얘졌습니다.
다행히 옆자리의 선배 이포렌 씨가 침착하게 다가왔습니다. "김 대리, 당황하지 마세요.
초기 대응에서 가장 중요한 건 세 가지입니다. 격리, 보존, 기록이에요." 이포렌 씨의 말을 듣고 김보안 씨는 고개를 끄덕였습니다.
마치 범죄 현장에서 경찰이 하는 일과 비슷했습니다. 현장을 보존하고, 증거를 오염시키지 않으면서, 모든 것을 기록하는 것입니다.
격리란 감염된 시스템을 네트워크에서 분리하는 것을 말합니다. 하지만 여기서 중요한 점이 있습니다.
무조건 전원을 끄면 안 됩니다. 메모리에 있는 중요한 증거가 사라질 수 있기 때문입니다.
네트워크 케이블만 뽑거나 방화벽 규칙으로 해당 시스템을 격리하는 것이 좋습니다. 보존은 현재 시스템 상태를 그대로 유지하는 것입니다.
공격자가 남긴 흔적, 실행 중인 프로세스, 네트워크 연결 정보 등 모든 것이 증거가 됩니다. 이 증거들은 나중에 공격 경로를 분석하고 재발을 방지하는 데 결정적인 역할을 합니다.
기록은 발생한 모든 상황과 대응 조치를 문서화하는 것입니다. 언제 경고를 받았는지, 누가 어떤 조치를 취했는지, 시스템 상태가 어땠는지를 시간 순서대로 기록해야 합니다.
위의 코드를 살펴보겠습니다. IncidentResponse 클래스는 침해사고 초기 대응을 자동화합니다.
initial_triage 메서드는 시스템의 현재 상태를 빠르게 스냅샷으로 저장합니다. 네트워크 연결 수, 실행 중인 프로세스 수, 로그인한 사용자 목록을 한눈에 파악할 수 있습니다.
실제 현업에서는 이런 스크립트를 미리 준비해 두어야 합니다. 사고가 발생하면 시간이 금입니다.
미리 만들어 둔 도구를 실행하면 몇 초 만에 중요한 정보를 수집할 수 있습니다. 주의할 점이 있습니다.
초기 대응 과정에서 시스템을 직접 수정하거나 파일을 삭제하면 안 됩니다. 공격자가 남긴 악성코드를 발견했더라도, 바로 삭제하지 말고 먼저 분석을 위해 복사본을 만들어야 합니다.
김보안 씨는 이포렌 씨의 안내에 따라 침착하게 초기 대응을 수행했습니다. 모니터링 시스템의 경고 시각, 자신이 취한 모든 조치, 시스템에서 수집한 정보를 빠짐없이 기록했습니다.
이 기록들은 나중에 사고 보고서를 작성할 때 큰 도움이 될 것입니다.
실전 팁
💡 - 초기 대응 스크립트는 반드시 사전에 테스트하고 준비해 두세요
- 모든 조치는 시간과 함께 기록하고, 가능하면 화면 캡처도 남기세요
- 섣불리 시스템을 재부팅하거나 파일을 삭제하지 마세요
2. 증거 수집 스크립트 작성
초기 대응을 마친 김보안 씨에게 이포렌 씨가 말했습니다. "자, 이제 본격적으로 증거를 수집해야 해요.
그런데 수작업으로 하면 시간도 오래 걸리고 실수할 수도 있어요. 스크립트로 자동화해 봅시다."
증거 수집 스크립트는 침해된 시스템에서 포렌식 분석에 필요한 데이터를 체계적으로 수집하는 자동화 도구입니다. 마치 형사가 범죄 현장에서 증거물을 봉투에 담아 라벨을 붙이는 것처럼, 디지털 증거도 정해진 절차에 따라 수집하고 무결성을 보장해야 합니다.
다음 코드를 살펴봅시다.
import os
import hashlib
import subprocess
import datetime
import json
class EvidenceCollector:
def __init__(self, case_id, output_dir="/tmp/evidence"):
self.case_id = case_id
self.output_dir = f"{output_dir}/{case_id}"
self.collection_log = []
os.makedirs(self.output_dir, exist_ok=True)
def collect_volatile_data(self):
# 휘발성 데이터 우선 수집 (메모리, 네트워크, 프로세스)
volatile_evidence = {}
# 현재 네트워크 연결
netstat = subprocess.run(['netstat', '-anp'], capture_output=True, text=True)
volatile_evidence['network_connections'] = netstat.stdout
# 실행 중인 프로세스
ps = subprocess.run(['ps', 'auxww'], capture_output=True, text=True)
volatile_evidence['processes'] = ps.stdout
# 열린 파일 목록
lsof = subprocess.run(['lsof', '-n'], capture_output=True, text=True)
volatile_evidence['open_files'] = lsof.stdout
# 라우팅 테이블
route = subprocess.run(['route', '-n'], capture_output=True, text=True)
volatile_evidence['routing_table'] = route.stdout
self._save_evidence("volatile_data.json", json.dumps(volatile_evidence))
return volatile_evidence
def collect_system_logs(self):
# 시스템 로그 수집
log_files = [
"/var/log/auth.log",
"/var/log/syslog",
"/var/log/secure",
"/var/log/messages"
]
for log_file in log_files:
if os.path.exists(log_file):
self._copy_with_hash(log_file)
def _copy_with_hash(self, source_path):
# 파일 복사 및 해시값 계산으로 무결성 보장
filename = os.path.basename(source_path)
dest_path = f"{self.output_dir}/{filename}"
with open(source_path, 'rb') as f:
content = f.read()
file_hash = hashlib.sha256(content).hexdigest()
with open(dest_path, 'wb') as f:
f.write(content)
self.collection_log.append({
"source": source_path,
"destination": dest_path,
"sha256": file_hash,
"collected_at": datetime.datetime.now().isoformat()
})
def _save_evidence(self, filename, content):
filepath = f"{self.output_dir}/{filename}"
with open(filepath, 'w') as f:
f.write(content)
file_hash = hashlib.sha256(content.encode()).hexdigest()
self.collection_log.append({
"file": filename,
"sha256": file_hash,
"collected_at": datetime.datetime.now().isoformat()
})
# 사용 예시
collector = EvidenceCollector("CASE-2024-001")
collector.collect_volatile_data()
collector.collect_system_logs()
print(json.dumps(collector.collection_log, indent=2))
이포렌 씨가 화이트보드에 그림을 그리며 설명을 시작했습니다. "증거 수집에는 순서가 있어요.
바로 휘발성 순서입니다." 휘발성이란 무엇일까요? 쉽게 말해 '사라지기 쉬운 정도'입니다.
컴퓨터를 끄면 메모리에 있는 데이터는 즉시 사라집니다. 반면 하드디스크에 저장된 파일은 그대로 남아 있습니다.
따라서 메모리 데이터를 먼저 수집하고, 그다음 디스크 데이터를 수집해야 합니다. 이포렌 씨가 화이트보드에 피라미드를 그렸습니다.
맨 위부터 아래로: 레지스터와 캐시, 메모리, 네트워크 연결, 실행 중인 프로세스, 디스크 데이터, 로그 파일 순서입니다. 위로 갈수록 휘발성이 높습니다.
"하지만 현실적으로 레지스터나 캐시까지 수집하기는 어려워요. 우리가 집중할 건 네트워크 연결, 프로세스, 그리고 로그 파일입니다." 위의 코드에서 collect_volatile_data 메서드를 보면, netstat으로 현재 네트워크 연결을, ps로 실행 중인 프로세스를, lsof로 열린 파일 목록을 수집합니다.
이 정보들은 공격자가 어떤 연결을 맺고 있었는지, 어떤 악성 프로세스를 실행했는지 알려줍니다. 여기서 핵심적인 부분이 있습니다.
바로 해시값 계산입니다. _copy_with_hash 메서드를 보면 파일을 복사할 때 SHA-256 해시값을 함께 계산합니다.
이 해시값은 일종의 '디지털 지문'입니다. 나중에 누군가 "이 증거가 조작된 것 아니냐"고 물으면, 해시값을 비교해서 원본과 동일하다는 것을 증명할 수 있습니다.
법정에서 디지털 증거가 인정받으려면 증거의 연속성이 보장되어야 합니다. 수집 시점의 해시값과 분석 시점의 해시값이 일치해야 증거로서 가치를 인정받습니다.
실제 현업에서는 이 스크립트를 USB에 담아 다니거나, 보안 관제 시스템에 미리 배포해 둡니다. 사고가 발생하면 해당 시스템에서 바로 실행할 수 있어야 합니다.
김보안 씨가 질문했습니다. "스크립트를 감염된 시스템에서 실행하면, 그 자체가 시스템을 변경하는 것 아닌가요?" 좋은 질문입니다.
이것을 로카르드 교환 법칙이라고 합니다. 범죄 현장에 들어가면 흔적을 남기고, 현장의 흔적을 가져오게 된다는 것입니다.
디지털 포렌식에서도 마찬가지입니다. 스크립트를 실행하면 어느 정도 시스템에 영향을 줍니다.
그래서 수집 도구는 가능한 한 가볍게 만들고, 수집 과정 자체도 기록해야 합니다.
실전 팁
💡 - 휘발성이 높은 데이터(네트워크, 프로세스)부터 먼저 수집하세요
- 모든 수집 파일에 해시값을 계산하여 무결성을 보장하세요
- 수집 스크립트 자체의 해시값도 문서화해 두세요
3. 타임라인 분석 도구 개발
증거 수집을 마친 김보안 씨 앞에는 수백 개의 로그 파일이 쌓여 있었습니다. "이걸 어떻게 다 분석하죠?" 이포렌 씨가 웃으며 말했습니다.
"시간 순서대로 정리하면 공격의 흐름이 보여요. 타임라인을 만들어 봅시다."
타임라인 분석은 여러 출처의 로그와 이벤트를 시간 순서로 정렬하여 사건의 흐름을 재구성하는 기법입니다. 마치 형사가 CCTV 영상 여러 개를 시간대별로 이어 붙여 용의자의 동선을 파악하는 것과 같습니다.
흩어진 퍼즐 조각들이 하나의 그림으로 완성됩니다.
다음 코드를 살펴봅시다.
import re
import os
from datetime import datetime
from collections import defaultdict
import json
class TimelineAnalyzer:
def __init__(self):
self.events = []
self.log_patterns = {
'auth_log': r'(\w{3}\s+\d+\s+\d+:\d+:\d+)\s+(\S+)\s+(\S+):\s+(.*)',
'syslog': r'(\w{3}\s+\d+\s+\d+:\d+:\d+)\s+(\S+)\s+(\S+)\[?\d*\]?:\s+(.*)',
'apache_access': r'(\d+\.\d+\.\d+\.\d+).*\[(.+?)\]\s+"(\w+)\s+(.+?)"',
}
def parse_auth_log(self, filepath):
# 인증 로그 파싱 (SSH 로그인, sudo 등)
if not os.path.exists(filepath):
return
with open(filepath, 'r') as f:
for line in f:
match = re.match(self.log_patterns['auth_log'], line)
if match:
timestamp_str, hostname, service, message = match.groups()
# 의심스러운 이벤트 식별
event_type = self._classify_auth_event(message)
if event_type:
self.events.append({
'timestamp': self._parse_timestamp(timestamp_str),
'source': 'auth.log',
'hostname': hostname,
'service': service,
'event_type': event_type,
'message': message,
'severity': self._get_severity(event_type)
})
def _classify_auth_event(self, message):
# 이벤트 분류
classifications = {
'Failed password': 'LOGIN_FAILED',
'Accepted password': 'LOGIN_SUCCESS',
'Accepted publickey': 'LOGIN_SUCCESS_KEY',
'Invalid user': 'INVALID_USER',
'session opened': 'SESSION_START',
'session closed': 'SESSION_END',
'sudo:': 'PRIVILEGE_ESCALATION'
}
for pattern, event_type in classifications.items():
if pattern in message:
return event_type
return None
def _parse_timestamp(self, timestamp_str):
# 타임스탬프 파싱 (현재 연도 추가)
current_year = datetime.now().year
try:
dt = datetime.strptime(f"{current_year} {timestamp_str}", "%Y %b %d %H:%M:%S")
return dt.isoformat()
except:
return timestamp_str
def _get_severity(self, event_type):
severity_map = {
'LOGIN_FAILED': 'MEDIUM',
'INVALID_USER': 'HIGH',
'PRIVILEGE_ESCALATION': 'HIGH',
'LOGIN_SUCCESS': 'LOW',
'LOGIN_SUCCESS_KEY': 'LOW'
}
return severity_map.get(event_type, 'INFO')
def generate_timeline(self):
# 시간순 정렬된 타임라인 생성
sorted_events = sorted(self.events, key=lambda x: x['timestamp'])
return sorted_events
def find_attack_patterns(self):
# 공격 패턴 탐지 (예: 브루트포스)
failed_logins = defaultdict(list)
for event in self.events:
if event['event_type'] == 'LOGIN_FAILED':
# IP 주소 추출 시도
ip_match = re.search(r'from\s+(\d+\.\d+\.\d+\.\d+)', event['message'])
if ip_match:
ip = ip_match.group(1)
failed_logins[ip].append(event['timestamp'])
# 5회 이상 실패한 IP 식별
suspicious_ips = {ip: times for ip, times in failed_logins.items() if len(times) >= 5}
return suspicious_ips
# 사용 예시
analyzer = TimelineAnalyzer()
analyzer.parse_auth_log("/var/log/auth.log")
timeline = analyzer.generate_timeline()
suspicious = analyzer.find_attack_patterns()
print(f"총 {len(timeline)}개의 이벤트 분석 완료")
print(f"의심스러운 IP: {list(suspicious.keys())}")
이포렌 씨가 화면에 엑셀 같은 표를 띄웠습니다. 왼쪽에는 시간, 오른쪽에는 발생한 이벤트가 적혀 있었습니다.
"이게 바로 타임라인이에요. 사건을 시간순으로 보면 이야기가 보입니다." 타임라인 분석은 왜 중요할까요?
침해사고는 보통 여러 단계로 진행됩니다. 먼저 정찰, 그다음 침투, 권한 상승, 내부 이동, 데이터 탈취 순서입니다.
이 각 단계는 서로 다른 로그에 흔적을 남깁니다. SSH 로그, 웹서버 로그, 시스템 로그를 따로 보면 조각난 퍼즐처럼 보이지만, 시간순으로 합치면 전체 그림이 드러납니다.
위 코드의 TimelineAnalyzer 클래스를 살펴봅시다. parse_auth_log 메서드는 리눅스의 인증 로그를 파싱합니다.
정규표현식으로 로그의 각 부분(시간, 호스트명, 서비스, 메시지)을 추출합니다. _classify_auth_event 메서드가 핵심입니다.
로그 메시지를 보고 어떤 종류의 이벤트인지 분류합니다. "Failed password"가 있으면 로그인 실패, "Accepted password"가 있으면 로그인 성공으로 분류합니다.
이렇게 분류해 두면 나중에 "로그인 실패만 필터링"하는 것이 쉬워집니다. find_attack_patterns 메서드는 흥미로운 기능을 합니다.
같은 IP에서 5회 이상 로그인 실패가 발생하면 브루트포스 공격으로 의심합니다. 실제로 공격자는 자동화 도구로 수천 번의 로그인 시도를 합니다.
이런 패턴을 자동으로 탐지할 수 있습니다. 실제 사례를 들어보겠습니다.
어느 회사에서 침해사고가 발생했을 때, 타임라인을 만들어보니 이런 순서가 드러났습니다. 새벽 2시에 외부 IP에서 SSH 브루트포스 시작, 2시 47분에 로그인 성공, 2시 48분에 sudo 명령 실행, 2시 50분에 크론탭 수정, 3시에 외부 서버로 데이터 전송.
이렇게 타임라인을 보면 공격자가 무엇을 했는지 한눈에 파악할 수 있습니다. 김보안 씨가 물었습니다.
"로그마다 시간 형식이 다르면 어떻게 하죠?" 좋은 질문입니다. _parse_timestamp 메서드에서 볼 수 있듯이, 각 로그 형식에 맞는 파서를 만들어야 합니다.
표준 ISO 8601 형식으로 통일하면 서로 다른 출처의 로그도 쉽게 비교할 수 있습니다. 주의할 점이 있습니다.
시스템마다 시간대가 다를 수 있습니다. 서버는 UTC, 워크스테이션은 KST일 수 있습니다.
타임라인을 만들기 전에 반드시 시간대를 확인하고 통일해야 합니다.
실전 팁
💡 - 모든 로그의 시간대를 UTC로 통일하면 비교가 쉬워집니다
- 의심스러운 이벤트에는 심각도를 부여하여 우선순위를 정하세요
- 타임라인은 시각화하면 패턴을 더 쉽게 발견할 수 있습니다
4. 디지털 포렌식 분석
타임라인에서 의심스러운 시점을 발견한 김보안 씨. 그때 실행된 프로세스가 무엇이었는지 더 깊이 파고들어야 했습니다.
이포렌 씨가 말했습니다. "이제 본격적인 포렌식 분석을 시작해 봅시다.
디스크와 메모리 속에 숨겨진 진실을 찾아야 해요."
디지털 포렌식 분석은 디지털 장치에서 법적 증거 가치가 있는 정보를 과학적 방법으로 수집, 보존, 분석하는 과정입니다. 마치 고고학자가 유적지를 조심스럽게 발굴하듯이, 디지털 증거도 원본을 훼손하지 않으면서 숨겨진 정보를 찾아내야 합니다.
삭제된 파일도 복구할 수 있고, 숨겨진 악성코드도 찾아낼 수 있습니다.
다음 코드를 살펴봅시다.
import os
import stat
import pwd
import grp
import hashlib
from datetime import datetime
import subprocess
import json
class ForensicAnalyzer:
def __init__(self, target_path):
self.target_path = target_path
self.findings = []
def analyze_file_permissions(self, filepath):
# 파일 권한 분석 - SUID/SGID 비트 확인
try:
file_stat = os.stat(filepath)
mode = file_stat.st_mode
suspicious = False
issues = []
# SUID 비트 확인 (권한 상승에 악용 가능)
if mode & stat.S_ISUID:
issues.append("SUID bit set - potential privilege escalation")
suspicious = True
# SGID 비트 확인
if mode & stat.S_ISGID:
issues.append("SGID bit set")
suspicious = True
# 월드 writable 확인
if mode & stat.S_IWOTH:
issues.append("World writable - security risk")
suspicious = True
return {
'path': filepath,
'mode': oct(mode),
'owner': pwd.getpwuid(file_stat.st_uid).pw_name,
'group': grp.getgrgid(file_stat.st_gid).gr_name,
'suspicious': suspicious,
'issues': issues
}
except Exception as e:
return {'path': filepath, 'error': str(e)}
def find_hidden_files(self, directory):
# 숨겨진 파일 및 의심스러운 파일 탐지
hidden_files = []
for root, dirs, files in os.walk(directory):
for name in files + dirs:
filepath = os.path.join(root, name)
# 점으로 시작하는 숨김 파일
if name.startswith('.'):
hidden_files.append({
'path': filepath,
'type': 'hidden',
'reason': 'Starts with dot'
})
# 이중 확장자 (예: document.pdf.exe)
if name.count('.') > 1:
hidden_files.append({
'path': filepath,
'type': 'double_extension',
'reason': 'Multiple extensions detected'
})
# 공백이나 특수문자로 숨긴 파일
if name.strip() != name or '\x00' in name:
hidden_files.append({
'path': filepath,
'type': 'obfuscated',
'reason': 'Whitespace or null character in name'
})
return hidden_files
def analyze_startup_persistence(self):
# 시작 프로그램 분석 - 악성코드 지속성 확인
persistence_locations = [
"/etc/rc.local",
"/etc/crontab",
"/etc/init.d/",
"/etc/systemd/system/",
"/home/*/.bashrc",
"/home/*/.profile"
]
findings = []
# crontab 분석
try:
result = subprocess.run(['crontab', '-l'], capture_output=True, text=True)
if result.stdout:
for line in result.stdout.split('\n'):
if line and not line.startswith('#'):
findings.append({
'location': 'crontab',
'content': line,
'analysis': self._analyze_cron_entry(line)
})
except:
pass
# systemd 서비스 분석
try:
result = subprocess.run(
['systemctl', 'list-unit-files', '--type=service', '--state=enabled'],
capture_output=True, text=True
)
for line in result.stdout.split('\n')[1:]:
if line.strip():
findings.append({
'location': 'systemd',
'content': line.split()[0] if line.split() else '',
'type': 'enabled_service'
})
except:
pass
return findings
def _analyze_cron_entry(self, entry):
# cron 항목 위험도 분석
suspicious_patterns = [
('wget', 'Downloads from internet'),
('curl', 'Downloads from internet'),
('base64', 'Encoded payload'),
('/tmp/', 'Execution from temp directory'),
('chmod', 'Permission modification'),
('nc ', 'Netcat - potential reverse shell'),
('bash -i', 'Interactive shell')
]
risks = []
for pattern, description in suspicious_patterns:
if pattern in entry:
risks.append(description)
return risks if risks else ['No obvious risks detected']
def calculate_file_entropy(self, filepath):
# 파일 엔트로피 계산 - 암호화/압축 탐지
try:
with open(filepath, 'rb') as f:
data = f.read()
if not data:
return 0
entropy = 0
for x in range(256):
p_x = data.count(bytes([x])) / len(data)
if p_x > 0:
entropy -= p_x * (p_x.bit_length() - 1)
# 높은 엔트로피는 암호화 또는 압축을 의미
return {
'filepath': filepath,
'entropy': abs(entropy),
'likely_encrypted': abs(entropy) > 7.5,
'likely_compressed': 7.0 < abs(entropy) <= 7.5
}
except Exception as e:
return {'filepath': filepath, 'error': str(e)}
# 사용 예시
analyzer = ForensicAnalyzer("/home")
hidden = analyzer.find_hidden_files("/tmp")
persistence = analyzer.analyze_startup_persistence()
print(f"숨겨진 파일 발견: {len(hidden)}개")
print(f"지속성 메커니즘 발견: {len(persistence)}개")
이포렌 씨가 터미널 창을 열며 말했습니다. "포렌식 분석의 핵심은 '비정상'을 찾는 거예요.
정상적인 시스템이 어떤 모습인지 알아야 비정상을 발견할 수 있습니다." 포렌식 분석에서 가장 먼저 살펴보는 것 중 하나가 파일 권한입니다. 리눅스에서 SUID 비트가 설정된 파일은 실행 시 파일 소유자의 권한으로 동작합니다.
만약 root 소유의 SUID 파일에 취약점이 있다면, 일반 사용자도 root 권한을 얻을 수 있습니다. 공격자는 종종 SUID 비트를 악용하거나, 직접 SUID 파일을 만들어 백도어로 사용합니다.
위 코드의 analyze_file_permissions 메서드는 이런 위험한 권한 설정을 탐지합니다. SUID, SGID 비트뿐 아니라 '누구나 쓸 수 있는' 파일도 찾아냅니다.
숨겨진 파일 탐지도 중요합니다. 공격자는 자신의 도구를 숨기기 위해 다양한 기법을 사용합니다.
점으로 시작하는 파일명, 이중 확장자(악성코드.txt.exe), 공백 문자를 이용한 숨김 등입니다. find_hidden_files 메서드는 이런 의심스러운 파일들을 찾아냅니다.
지속성 메커니즘 분석은 특히 중요합니다. 공격자는 시스템을 재부팅해도 접근을 유지하기 위해 다양한 방법을 사용합니다.
크론탭에 악성 스크립트 등록, systemd 서비스 생성, 쉘 설정 파일(.bashrc) 수정 등이 대표적입니다. analyze_startup_persistence 메서드를 보면, 크론탭과 systemd 서비스를 분석합니다.
_analyze_cron_entry 메서드는 크론 항목에서 의심스러운 패턴을 찾습니다. wget이나 curl로 뭔가를 다운로드하거나, /tmp 디렉토리에서 실행하거나, base64로 인코딩된 명령을 실행하면 의심해봐야 합니다.
엔트로피 분석도 흥미로운 기법입니다. 엔트로피는 데이터의 무작위성을 측정합니다.
일반 텍스트 파일은 엔트로피가 낮고, 암호화되거나 압축된 파일은 엔트로피가 높습니다. 공격자가 데이터를 암호화해서 숨겼다면, 높은 엔트로피로 탐지할 수 있습니다.
김보안 씨가 실습하면서 물었습니다. "정상적인 SUID 파일도 있잖아요.
sudo 같은 거요. 어떻게 구분하죠?" 이포렌 씨가 답했습니다.
"좋은 질문이에요. 그래서 기준선이 필요합니다.
정상 상태에서 SUID 파일 목록을 미리 만들어 두고, 사고 발생 시 비교하면 새로 추가된 것을 찾을 수 있어요. 이걸 베이스라인 비교라고 합니다." 실제 침해사고에서 포렌식 분석을 통해 발견한 사례가 있습니다.
공격자가 /usr/bin/ncat에 SUID 비트를 설정해 두었습니다. 겉보기에는 일반 유틸리티 같았지만, 실제로는 언제든 root 쉘을 얻을 수 있는 백도어였습니다.
실전 팁
💡 - 정상 상태의 SUID 파일 목록을 미리 만들어 두세요
- 크론탭과 systemd 서비스를 정기적으로 검토하세요
- 의심스러운 파일은 삭제하지 말고 먼저 분석하세요
5. 파일 메타데이터 분석
포렌식 분석 중 김보안 씨가 이상한 파일을 발견했습니다. 겉보기에는 평범한 이미지 파일이었지만, 뭔가 수상했습니다.
이포렌 씨가 다가와 말했습니다. "파일의 겉모습만 보면 안 돼요.
메타데이터를 확인해 봅시다."
파일 메타데이터는 파일의 내용이 아닌 파일에 관한 정보입니다. 생성 시간, 수정 시간, 소유자, 권한, 파일 형식 등이 여기에 해당합니다.
마치 편지 봉투에 적힌 발신인과 수신인, 소인 날짜처럼, 메타데이터는 파일의 '이력'을 알려줍니다. 공격자가 파일명이나 확장자를 위장해도 메타데이터는 진실을 말해줍니다.
다음 코드를 살펴봅시다.
import os
import stat
import magic
import hashlib
from datetime import datetime
import struct
class MetadataAnalyzer:
def __init__(self):
self.mime = magic.Magic(mime=True)
self.magic_full = magic.Magic()
def get_file_metadata(self, filepath):
# 파일의 모든 메타데이터 수집
try:
file_stat = os.stat(filepath)
metadata = {
'path': filepath,
'size': file_stat.st_size,
'size_human': self._human_readable_size(file_stat.st_size),
# MAC 타임스탬프 (Modify, Access, Change)
'modified_time': datetime.fromtimestamp(file_stat.st_mtime).isoformat(),
'accessed_time': datetime.fromtimestamp(file_stat.st_atime).isoformat(),
'changed_time': datetime.fromtimestamp(file_stat.st_ctime).isoformat(),
# 파일 타입
'mime_type': self.mime.from_file(filepath),
'file_type': self.magic_full.from_file(filepath),
'extension': os.path.splitext(filepath)[1],
# 해시값
'md5': self._calculate_hash(filepath, 'md5'),
'sha256': self._calculate_hash(filepath, 'sha256'),
# 권한 정보
'permissions': oct(file_stat.st_mode)[-3:],
'uid': file_stat.st_uid,
'gid': file_stat.st_gid
}
# 확장자와 실제 타입 불일치 검사
metadata['type_mismatch'] = self._check_type_mismatch(
metadata['extension'],
metadata['mime_type']
)
return metadata
except Exception as e:
return {'path': filepath, 'error': str(e)}
def _human_readable_size(self, size):
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} PB"
def _calculate_hash(self, filepath, algorithm):
hash_func = hashlib.new(algorithm)
with open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
hash_func.update(chunk)
return hash_func.hexdigest()
def _check_type_mismatch(self, extension, mime_type):
# 확장자와 MIME 타입 불일치 검사
extension_mime_map = {
'.jpg': ['image/jpeg'],
'.jpeg': ['image/jpeg'],
'.png': ['image/png'],
'.gif': ['image/gif'],
'.pdf': ['application/pdf'],
'.exe': ['application/x-executable', 'application/x-dosexec'],
'.py': ['text/x-python', 'text/plain'],
'.sh': ['text/x-shellscript', 'text/plain'],
'.txt': ['text/plain']
}
extension = extension.lower()
if extension in extension_mime_map:
if mime_type not in extension_mime_map[extension]:
return {
'mismatch': True,
'expected': extension_mime_map[extension],
'actual': mime_type,
'warning': 'File extension does not match content type!'
}
return {'mismatch': False}
def detect_file_signature(self, filepath):
# 파일 시그니처(매직 바이트) 분석
signatures = {
b'\x7fELF': 'ELF Executable (Linux)',
b'MZ': 'DOS/Windows Executable',
b'\x89PNG': 'PNG Image',
b'\xff\xd8\xff': 'JPEG Image',
b'%PDF': 'PDF Document',
b'PK\x03\x04': 'ZIP Archive',
b'\x1f\x8b': 'GZIP Compressed',
b'#!/': 'Shell Script',
b'import ': 'Python Script (likely)',
}
with open(filepath, 'rb') as f:
header = f.read(16)
for sig, file_type in signatures.items():
if header.startswith(sig):
return {'signature': sig.hex(), 'type': file_type}
return {'signature': header[:8].hex(), 'type': 'Unknown'}
def analyze_timestamps(self, filepath):
# 타임스탬프 이상 탐지
file_stat = os.stat(filepath)
mtime = datetime.fromtimestamp(file_stat.st_mtime)
atime = datetime.fromtimestamp(file_stat.st_atime)
ctime = datetime.fromtimestamp(file_stat.st_ctime)
anomalies = []
# 미래 시간 체크
now = datetime.now()
if mtime > now:
anomalies.append("Modified time is in the future - possible tampering")
if atime > now:
anomalies.append("Access time is in the future - possible tampering")
# 수정 시간이 변경 시간보다 이전 (timestomping 의심)
if mtime < ctime:
anomalies.append("Modified time before change time - possible timestomping")
return {
'mtime': mtime.isoformat(),
'atime': atime.isoformat(),
'ctime': ctime.isoformat(),
'anomalies': anomalies
}
# 사용 예시
analyzer = MetadataAnalyzer()
metadata = analyzer.get_file_metadata("/usr/bin/python3")
signature = analyzer.detect_file_signature("/usr/bin/python3")
timestamps = analyzer.analyze_timestamps("/usr/bin/python3")
print(f"파일 타입: {metadata['file_type']}")
print(f"시그니처: {signature['type']}")
print(f"타임스탬프 이상: {timestamps['anomalies']}")
이포렌 씨가 터미널에서 file 명령을 실행했습니다. "이 파일, 확장자는 .jpg인데 실제로는 실행 파일이에요.
공격자가 위장한 거죠." 파일 메타데이터 분석의 핵심은 겉과 속이 다른 파일을 찾는 것입니다. 공격자는 악성 코드를 숨기기 위해 다양한 위장술을 사용합니다.
가장 흔한 방법은 확장자를 바꾸는 것입니다. malware.exe를 document.pdf로 이름만 바꾸면 사용자가 속아서 클릭할 수 있습니다.
하지만 파일의 실제 타입은 숨길 수 없습니다. 모든 파일은 시작 부분에 매직 바이트(파일 시그니처)가 있습니다.
JPEG 파일은 FF D8 FF로 시작하고, PNG 파일은 89 50 4E 47로 시작합니다. ELF 실행 파일은 7F 45 4C 46으로 시작합니다.
detect_file_signature 메서드는 이 매직 바이트를 읽어서 파일의 실제 타입을 판별합니다. _check_type_mismatch 메서드는 확장자와 MIME 타입을 비교합니다.
.jpg 파일인데 MIME 타입이 application/x-executable이면 뭔가 수상한 것입니다. 타임스탬프 분석도 매우 중요합니다.
리눅스에서 파일에는 세 가지 시간이 기록됩니다. mtime(내용 수정 시간), atime(접근 시간), ctime(메타데이터 변경 시간)입니다.
이것을 MAC 타임이라고 부릅니다. 공격자는 자신의 흔적을 숨기기 위해 이 타임스탬프를 조작하기도 합니다.
이것을 타임스탬핑(timestomping)이라고 합니다. 예를 들어 오늘 만든 악성 파일의 mtime을 1년 전으로 바꿔서 오래된 시스템 파일처럼 위장할 수 있습니다.
analyze_timestamps 메서드는 이런 조작을 탐지합니다. 수정 시간이 미래로 되어 있거나, 수정 시간이 메타데이터 변경 시간보다 이전이면 조작을 의심합니다.
ctime은 chmod 같은 명령으로도 변경되기 때문에, mtime보다 ctime이 나중인 게 정상입니다. 반대로 되어 있다면 누군가 의도적으로 mtime을 과거로 돌린 것입니다.
해시값은 파일의 고유 지문입니다. 같은 내용의 파일은 항상 같은 해시값을 가집니다.
악성코드 데이터베이스에는 알려진 악성코드의 해시값이 저장되어 있습니다. 수상한 파일의 해시값을 VirusTotal 같은 서비스에 조회하면 이미 알려진 악성코드인지 확인할 수 있습니다.
김보안 씨가 발견한 파일을 분석해보니, 확장자는 .png였지만 매직 바이트는 ELF 실행 파일이었습니다. 그리고 mtime은 2년 전으로 되어 있었지만 ctime은 어제였습니다.
명백한 공격자의 은폐 시도였습니다. "타임스탬프 조작까지 했네요.
꽤 신경 쓴 공격자예요." 이포렌 씨가 고개를 끄덕였습니다.
실전 팁
💡 - 확장자가 아닌 매직 바이트로 파일 타입을 확인하세요
- MAC 타임스탬프의 논리적 일관성을 검사하세요
- 의심스러운 파일의 해시값을 VirusTotal에서 조회해 보세요
6. 침해 지표 추출
분석을 마무리하던 김보안 씨에게 이포렌 씨가 말했습니다. "이제 우리가 발견한 것들을 정리해야 해요.
다른 시스템도 같은 공격을 받았는지 확인하고, 미래의 공격을 방어하려면 침해 지표를 추출해야 합니다."
**침해 지표(IoC, Indicator of Compromise)**는 시스템이 침해되었음을 나타내는 증거입니다. 악성 IP 주소, 도메인, 파일 해시, 레지스트리 키 등이 여기에 해당합니다.
마치 범인의 지문, 혈흔, DNA처럼, IoC는 공격자의 흔적입니다. 이 지표를 다른 시스템에 적용하면 동일한 공격을 받았는지 빠르게 확인할 수 있습니다.
다음 코드를 살펴봅시다.
import re
import json
import hashlib
from datetime import datetime
from collections import defaultdict
class IoC_Extractor:
def __init__(self, case_id):
self.case_id = case_id
self.iocs = {
'ip_addresses': set(),
'domains': set(),
'urls': set(),
'file_hashes': [],
'email_addresses': set(),
'file_paths': set(),
'registry_keys': set(),
'mutex_names': set(),
'user_agents': set()
}
self.extraction_time = datetime.now()
def extract_from_text(self, text):
# 텍스트에서 IoC 추출
# IP 주소 추출
ip_pattern = r'\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b'
ips = re.findall(ip_pattern, text)
for ip in ips:
if not self._is_private_ip(ip):
self.iocs['ip_addresses'].add(ip)
# 도메인 추출
domain_pattern = r'\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}\b'
domains = re.findall(domain_pattern, text)
for domain in domains:
if not self._is_common_domain(domain):
self.iocs['domains'].add(domain.lower())
# URL 추출
url_pattern = r'https?://[^\s<>"{}|\\^`\[\]]+'
urls = re.findall(url_pattern, text)
self.iocs['urls'].update(urls)
# 이메일 추출
email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
emails = re.findall(email_pattern, text)
self.iocs['email_addresses'].update(emails)
def _is_private_ip(self, ip):
# 사설 IP 필터링
octets = list(map(int, ip.split('.')))
if octets[0] == 10: # 10.0.0.0/8
return True
if octets[0] == 172 and 16 <= octets[1] <= 31: # 172.16.0.0/12
return True
if octets[0] == 192 and octets[1] == 168: # 192.168.0.0/16
return True
if octets[0] == 127: # Loopback
return True
return False
def _is_common_domain(self, domain):
# 일반적인 도메인 필터링
common_domains = [
'google.com', 'microsoft.com', 'apple.com',
'amazon.com', 'cloudflare.com', 'github.com',
'googleapis.com', 'gstatic.com'
]
return any(domain.endswith(d) for d in common_domains)
def add_file_ioc(self, filepath, sha256, md5, description=""):
# 파일 기반 IoC 추가
self.iocs['file_hashes'].append({
'path': filepath,
'sha256': sha256,
'md5': md5,
'description': description,
'added_at': datetime.now().isoformat()
})
self.iocs['file_paths'].add(filepath)
def add_network_ioc(self, ioc_type, value, context=""):
# 네트워크 기반 IoC 추가
if ioc_type == 'ip':
self.iocs['ip_addresses'].add(value)
elif ioc_type == 'domain':
self.iocs['domains'].add(value)
elif ioc_type == 'url':
self.iocs['urls'].add(value)
def export_stix(self):
# STIX 2.1 형식으로 내보내기 (간소화 버전)
stix_bundle = {
"type": "bundle",
"id": f"bundle--{self.case_id}",
"objects": []
}
for ip in self.iocs['ip_addresses']:
stix_bundle['objects'].append({
"type": "indicator",
"pattern": f"[ipv4-addr:value = '{ip}']",
"pattern_type": "stix",
"valid_from": self.extraction_time.isoformat()
})
for domain in self.iocs['domains']:
stix_bundle['objects'].append({
"type": "indicator",
"pattern": f"[domain-name:value = '{domain}']",
"pattern_type": "stix",
"valid_from": self.extraction_time.isoformat()
})
for file_hash in self.iocs['file_hashes']:
stix_bundle['objects'].append({
"type": "indicator",
"pattern": f"[file:hashes.'SHA-256' = '{file_hash['sha256']}']",
"pattern_type": "stix",
"valid_from": self.extraction_time.isoformat()
})
return stix_bundle
def generate_report(self):
# IoC 보고서 생성
report = {
"case_id": self.case_id,
"extraction_time": self.extraction_time.isoformat(),
"summary": {
"total_ips": len(self.iocs['ip_addresses']),
"total_domains": len(self.iocs['domains']),
"total_urls": len(self.iocs['urls']),
"total_file_hashes": len(self.iocs['file_hashes']),
"total_emails": len(self.iocs['email_addresses'])
},
"indicators": {
"ip_addresses": list(self.iocs['ip_addresses']),
"domains": list(self.iocs['domains']),
"urls": list(self.iocs['urls']),
"file_hashes": self.iocs['file_hashes'],
"email_addresses": list(self.iocs['email_addresses']),
"file_paths": list(self.iocs['file_paths'])
}
}
return report
# 사용 예시
extractor = IoC_Extractor("CASE-2024-001")
# 로그에서 IoC 추출
sample_log = """
Connection from 185.220.101.45 to internal server
Malware downloaded from http://evil-domain.xyz/malware.exe
C2 communication with badactor.net on port 443
"""
extractor.extract_from_text(sample_log)
# 파일 IoC 추가
extractor.add_file_ioc(
"/tmp/.hidden/backdoor",
"a1b2c3d4e5f6...",
"abc123...",
"Backdoor shell script"
)
# 보고서 생성
report = extractor.generate_report()
print(json.dumps(report, indent=2))
이포렌 씨가 화이트보드에 'IoC'라고 크게 썼습니다. "IoC는 Indicator of Compromise, 우리말로 침해 지표입니다.
공격자가 남긴 흔적을 체계적으로 정리한 거예요." IoC는 왜 중요할까요? 첫째, 같은 공격 캠페인에 속한 다른 침해를 탐지할 수 있습니다.
공격자는 보통 같은 도구, 같은 서버를 재사용합니다. 우리가 발견한 악성 IP 주소가 다른 시스템에서도 발견된다면, 그 시스템도 침해되었을 가능성이 높습니다.
둘째, 미래의 공격을 방어할 수 있습니다. 발견한 IoC를 방화벽이나 IDS에 등록하면, 같은 공격자가 다시 시도할 때 차단할 수 있습니다.
IoC에는 여러 종류가 있습니다. 네트워크 IoC는 악성 IP 주소, 도메인, URL 등입니다.
호스트 IoC는 파일 해시, 파일 경로, 레지스트리 키 등입니다. 행위 IoC는 특정 프로세스 이름, 뮤텍스 이름, 사용자 에이전트 문자열 등입니다.
위 코드의 IoC_Extractor 클래스를 살펴봅시다. extract_from_text 메서드는 텍스트에서 자동으로 IoC를 추출합니다.
정규표현식으로 IP 주소, 도메인, URL, 이메일 주소 패턴을 찾습니다. 여기서 중요한 점이 있습니다.
추출한 모든 것이 악성은 아닙니다. _is_private_ip 메서드는 사설 IP를 필터링하고, _is_common_domain 메서드는 Google, Microsoft 같은 일반적인 도메인을 필터링합니다.
이렇게 화이트리스트를 적용해야 의미 있는 IoC만 남습니다. export_stix 메서드는 STIX 형식으로 IoC를 내보냅니다.
STIX(Structured Threat Information eXpression)는 위협 정보를 공유하기 위한 국제 표준입니다. 이 형식으로 내보내면 다른 보안 도구나 조직과 쉽게 공유할 수 있습니다.
실제 현업에서는 위협 인텔리전스 플랫폼을 사용합니다. MISP, OpenCTI 같은 도구에 IoC를 등록하면, 조직 내 모든 보안 장비가 이 정보를 활용할 수 있습니다.
김보안 씨가 추출한 IoC를 정리했습니다. 악성 IP 3개, 의심스러운 도메인 2개, 악성 파일 해시 5개.
이것을 방화벽과 EDR에 등록하니 다른 서버에서도 같은 IP로의 연결 시도가 있었다는 것을 발견했습니다. 공격 범위가 생각보다 넓었던 것입니다.
"IoC 덕분에 감염된 시스템을 더 찾을 수 있었네요." 김보안 씨가 말했습니다. 이포렌 씨가 고개를 끄덕였습니다.
"네, 그리고 이 IoC를 CERT에 공유하면 같은 공격을 받는 다른 조직도 도움받을 수 있어요. 위협 정보 공유는 보안 커뮤니티 전체의 방어력을 높이는 일입니다." 침해사고 대응의 마지막 단계는 교훈 도출입니다.
어떻게 침해가 발생했는지, 어떤 취약점이 악용되었는지, 앞으로 어떻게 방지할 수 있는지를 정리해야 합니다. 김보안 씨는 이번 경험을 통해 많은 것을 배웠습니다.
다음에는 더 빠르고 체계적으로 대응할 수 있을 것입니다.
실전 팁
💡 - 추출한 IoC에서 정상 트래픽을 필터링하세요
- STIX/TAXII 형식으로 내보내면 다른 보안 도구와 연동하기 쉽습니다
- IoC는 시간이 지나면 가치가 떨어지므로 유효기간을 관리하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
클라우드 보안 실전 완벽 가이드
AWS, Docker, Kubernetes 환경에서 보안을 자동화하고 취약점을 사전에 탐지하는 방법을 알아봅니다. 초급 개발자도 바로 적용할 수 있는 실전 보안 점검 스크립트와 CI/CD 파이프라인 보안 구축 방법을 다룹니다.
Memory Systems 에이전트 메모리 아키텍처 완벽 가이드
AI 에이전트가 정보를 기억하고 활용하는 메모리 시스템의 핵심 아키텍처를 다룹니다. 벡터 스토어의 한계부터 Knowledge Graph, Temporal Knowledge Graph까지 단계별로 이해할 수 있습니다.
Phase 5 취약점 발굴과 분석 완벽 가이드
보안 전문가가 되기 위한 취약점 발굴의 핵심 기법을 다룹니다. 코드 리뷰부터 퍼징, 바이너리 분석까지 실무에서 바로 활용할 수 있는 기술을 초급자 눈높이에 맞춰 설명합니다.
Multi-Agent Patterns 멀티 에이전트 아키텍처 완벽 가이드
여러 AI 에이전트가 협력하여 복잡한 작업을 수행하는 멀티 에이전트 시스템의 핵심 패턴을 다룹니다. 컨텍스트 격리부터 Supervisor, Swarm, Hierarchical 패턴까지 실무에서 바로 적용할 수 있는 아키텍처 설계 원칙을 배웁니다.
Context Compression 컨텍스트 압축 전략 완벽 가이드
LLM 애플리케이션에서 컨텍스트 윈도우를 효율적으로 관리하는 압축 전략을 다룹니다. Anchored Summarization부터 프로브 기반 평가까지, 토큰 비용을 최적화하면서 정보 품질을 유지하는 핵심 기법들을 실무 관점에서 설명합니다.