이메일 포렌식을 위한 Outlook PST 분석
개요
Microsoft Outlook PST(Personal Storage Table) 및 OST(Offline Storage Table) 파일은 디지털 포렌식 조사에서 중요한 증거 소스입니다. PST 파일은 MAPI(Messaging Application Programming Interface) 속성 시스템을 기반으로 한 독점 바이너리 형식으로 이메일, 일정 이벤트, 연락처, 작업 및 메모를 저장합니다. 이러한 파일의 포렌식 분석을 통해 삭제된 이메일(복구 가능한 항목 폴더에서) 복구, 메시지 라우팅 추적을 위한 이메일 헤더 추출, 맬웨어 또는 유출 데이터 분석을 위한 첨부 파일 분석, 통신 패턴 재구성이 가능합니다. 최신 PST 파일은 4KB 페이지의 유니코드 형식을 사용하며 최대 50GB까지 커질 수 있는 반면, 레거시 ANSI 형식은 2GB로 제한됩니다.
사용 시기
- 이메일 포렌식을 위해 Outlook PST 분석이 필요한 보안 인시던트 조사 시
- 이 도메인에 대한 탐지 규칙 또는 위협 헌팅 쿼리 구축 시
- SOC 분석가가 이 분석 유형에 대한 체계적인 절차가 필요할 때
- 관련 공격 기술에 대한 보안 모니터링 범위를 검증할 때
필수 조건
- libpff/pffexport (오픈 소스 PST 파서)
- pypff 또는 libratom 라이브러리가 포함된 Python 3.8+
- MailXaminer, Forensic Email Collector 또는 SysTools PST Forensics (상용)
- Microsoft Outlook (선택 사항, 네이티브 PST 액세스용)
- 추출된 콘텐츠를 위한 충분한 디스크 공간
PST 파일 위치
| 출처 | 경로 |
|---|---|
| Outlook 2016+ 기본값 | %USERPROFILE%\Documents\Outlook Files*.pst |
| Outlook 레거시 | %LOCALAPPDATA%\Microsoft\Outlook*.pst |
| OST 캐시 | %LOCALAPPDATA%\Microsoft\Outlook*.ost |
| 보관 파일 | %USERPROFILE%\Documents\Outlook Files\archive.pst |
오픈 소스 도구를 사용한 분석
libpff / pffexport
# PST 파일에서 모든 항목 내보내기
pffexport -m all evidence.pst -t exported_pst
# 이메일 메시지만 내보내기
pffexport -m items evidence.pst -t exported_emails
# 복구/삭제된 항목 내보내기
pffexport -m recovered evidence.pst -t recovered_items
# PST 파일 정보 얻기
pffinfo evidence.pst
Python PST 분석
import pypff
import os
import json
import hashlib
import email
import sys
from datetime import datetime
from collections import defaultdict
class PSTForensicAnalyzer:
"""Outlook PST/OST 파일의 포렌식 분석."""
def __init__(self, pst_path: str, output_dir: str):
self.pst_path = pst_path
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
self.pst = pypff.file()
self.pst.open(pst_path)
self.messages = []
self.attachments = []
self.stats = defaultdict(int)
def process_folder(self, folder, folder_path: str = ""):
"""PST 폴더를 재귀적으로 처리하고 메시지를 추출합니다."""
folder_name = folder.name or "Root"
current_path = f"{folder_path}/{folder_name}" if folder_path else folder_name
for i in range(folder.number_of_sub_messages):
try:
message = folder.get_sub_message(i)
msg_data = self.extract_message(message, current_path)
if msg_data:
self.messages.append(msg_data)
self.stats["total_messages"] += 1
except Exception as e:
self.stats["parse_errors"] += 1
for i in range(folder.number_of_sub_folders):
try:
subfolder = folder.get_sub_folder(i)
self.process_folder(subfolder, current_path)
except Exception:
continue
def extract_message(self, message, folder_path: str) -> dict:
"""단일 이메일 메시지에서 포렌식 메타데이터를 추출합니다."""
msg_data = {
"folder": folder_path,
"subject": message.subject or "",
"sender": message.sender_name or "",
"sender_email": "",
"creation_time": str(message.creation_time) if message.creation_time else None,
"delivery_time": str(message.delivery_time) if message.delivery_time else None,
"modification_time": str(message.modification_time) if message.modification_time else None,
"has_attachments": message.number_of_attachments > 0,
"attachment_count": message.number_of_attachments,
"body_size": len(message.plain_text_body or b""),
"html_size": len(message.html_body or b""),
}
# 라우팅 분석을 위한 전송 헤더 추출
headers = message.transport_headers
if headers:
msg_data["headers_present"] = True
msg_data["headers_size"] = len(headers)
# 주요 헤더 파싱
parsed = email.message_from_string(headers)
msg_data["from_header"] = parsed.get("From", "")
msg_data["to_header"] = parsed.get("To", "")
msg_data["date_header"] = parsed.get("Date", "")
msg_data["message_id"] = parsed.get("Message-ID", "")
msg_data["x_originating_ip"] = parsed.get("X-Originating-IP", "")
msg_data["received_headers"] = parsed.get_all("Received", [])
# 첨부 파일 처리
for j in range(message.number_of_attachments):
try:
attachment = message.get_attachment(j)
att_data = {
"message_subject": msg_data["subject"],
"name": attachment.name or f"attachment_{j}",
"size": attachment.size,
"content_type": "",
}
self.attachments.append(att_data)
self.stats["total_attachments"] += 1
except Exception:
continue
return msg_data
def save_attachments(self, max_size_mb: int = 100):
"""분석을 위해 첨부 파일을 디스크로 내보냅니다."""
att_dir = os.path.join(self.output_dir, "attachments")
os.makedirs(att_dir, exist_ok=True)
root = self.pst.get_root_folder()
self._save_attachments_recursive(root, att_dir, max_size_mb)
def _save_attachments_recursive(self, folder, att_dir, max_size_mb):
for i in range(folder.number_of_sub_messages):
try:
message = folder.get_sub_message(i)
for j in range(message.number_of_attachments):
att = message.get_attachment(j)
if att.size and att.size < max_size_mb * 1024 * 1024:
name = att.name or f"unknown_{i}_{j}"
safe_name = "".join(c if c.isalnum() or c in ".-_" else "_" for c in name)
path = os.path.join(att_dir, safe_name)
try:
data = att.read_buffer(att.size)
with open(path, "wb") as f:
f.write(data)
except Exception:
continue
except Exception:
continue
for i in range(folder.number_of_sub_folders):
try:
self._save_attachments_recursive(folder.get_sub_folder(i), att_dir, max_size_mb)
except Exception:
continue
def generate_report(self) -> str:
"""포괄적인 PST 포렌식 분석 보고서를 생성합니다."""
root = self.pst.get_root_folder()
self.process_folder(root)
report = {
"analysis_timestamp": datetime.now().isoformat(),
"pst_file": self.pst_path,
"pst_size_bytes": os.path.getsize(self.pst_path),
"statistics": dict(self.stats),
"messages": self.messages[:500],
"attachments": self.attachments[:200],
}
report_path = os.path.join(self.output_dir, "pst_forensic_report.json")
with open(report_path, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"[*] 전체 메시지: {self.stats['total_messages']}")
print(f"[*] 전체 첨부 파일: {self.stats['total_attachments']}")
print(f"[*] 구문 분석 오류: {self.stats['parse_errors']}")
return report_path
def close(self):
self.pst.close()
def main():
if len(sys.argv) < 3:
print("사용법: python process.py <pst_file> <output_dir>")
sys.exit(1)
analyzer = PSTForensicAnalyzer(sys.argv[1], sys.argv[2])
analyzer.generate_report()
analyzer.close()
if __name__ == "__main__":
main()
이메일 헤더 분석
포렌식 조사를 위한 주요 헤더:
| 헤더 | 포렌식 가치 |
|---|---|
| Received | 메시지 라우팅 체인 (아래에서 위로 읽음) |
| X-Originating-IP | 발신자의 실제 IP 주소 |
| Message-ID | 상관 관계 분석을 위한 고유 식별자 |
| Date | 전송 타임스탬프 |
| Return-Path | 반송 주소 (보낸사람과 다를 수 있음) |
| DKIM-Signature | 도메인 인증 서명 |
| Authentication-Results | SPF, DKIM, DMARC 확인 결과 |
| X-Mailer | 사용된 이메일 클라이언트 |
참고 자료
- MailXaminer PST Forensics: https://www.mailxaminer.com/blog/outlook-pst-file-forensics/
- libpff 문서: https://github.com/libyal/libpff
- PST 파일 형식 사양: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-pst/
- SANS 이메일 포렌식: https://www.sans.org/blog/email-forensics/
예제 출력
$ pffexport /evidence/jsmith_archive.pst -t /analysis/pst_output
pffexport 20231205 - libpff PST/OST 내보내기 도구
=================================================
입력: /evidence/jsmith_archive.pst (2.3 GB)
PST 내용 내보내는 중...
폴더: 45
메시지: 12,456
첨부 파일: 3,234
연락처: 567
일정: 234
작업: 89
내보내기가 3분 42초 만에 완료되었습니다.
$ python3 pst_analyzer.py /analysis/pst_output /analysis/email_report
PST 포렌식 분석 보고서
==============================
출처: jsmith_archive.pst ([email protected])
날짜 범위: 2023-06-01 ~ 2024-01-18
--- 사서함 통계 ---
전체 이메일: 12,456
보낸 편지: 4,567
받은 편지함: 7,889
첨부 파일 있음: 3,234
삭제됨 (복구됨): 234
--- 피싱/의심 이메일 ---
이메일 #8923
날짜: 2024-01-15 14:30:22 UTC
보낸사람: "IT 지원" <[email protected]>
받는사람: [email protected]
제목: 긴급: 비밀번호 재설정 필요
헤더:
Return-Path: [email protected]
X-Originating-IP: 203.0.113.55
Received: from mail-relay.c0rporate-help.com (203.0.113.55)
SPF: 실패 (도메인 c0rporate-help.com)
DKIM: 없음
DMARC: 실패
첨부 파일:
- Password_Reset_Form.xlsm (245 KB) SHA-256: 7a3b8c9d...e1f2a3b4
본문 미리보기: "직원 여러분, 비밀번호가 24시간 이내에 만료됩니다.
첨부된 양식을 열어 자격 증명을 재설정하세요..."
--- 데이터 유출 징후 ---
이메일 #9102
날짜: 2024-01-16 03:15:45 UTC
보낸사람: [email protected]
받는사람: [email protected]
제목: (제목 없음)
첨부 파일:
- archive_part1.7z (24.5 MB) - 암호화됨
- archive_part2.7z (24.5 MB) - 암호화됨
이메일 #9103
날짜: 2024-01-16 03:18:22 UTC
보낸사람: [email protected]
받는사람: [email protected]
제목: Re:
첨부 파일:
- archive_part3.7z (18.2 MB) - 암호화됨
--- 키워드 적중 ---
"기밀": 45개 이메일
"비밀번호": 23개 이메일
"이체": 12개 이메일
"사직": 3개 이메일
"증거 삭제": 1개 이메일 (이메일 #9200, 2024-01-17 22:30:00 UTC)
요약:
탐지된 피싱 이메일: 1 (초기 침해 벡터)
의심스러운 발신 이메일: 5 (개인 계정으로 첨부 파일 포함)
암호화된 첨부 파일: 3 (총 67.2 MB - 유출 가능성)
보고서: /analysis/email_report/pst_forensic_report.json