$npx -y skills add killvxk/cybersecurity-skills-zh --skill analyzing-outlook-pst-for-email-forensics分析 Microsoft Outlook PST 和 OST 文件,提取电子邮件取证证据,包括邮件内容、邮件头、附件、已删除邮件及元数据,使用 libpff、pst-utils 和取证电子邮件分析工具,适用于法律调查和事件响应。
| 1 | # 分析 Outlook PST 进行电子邮件取证 |
| 2 | |
| 3 | ## 概述 |
| 4 | |
| 5 | Microsoft Outlook PST(个人存储表,Personal Storage Table)和 OST(离线存储表,Offline Storage Table)文件是数字取证调查中的关键证据来源。PST 文件以基于 MAPI(消息应用程序编程接口,Messaging Application Programming Interface)属性系统的专有二进制格式存储电子邮件、日历事件、联系人、任务和备注。对这些文件进行取证分析,可以恢复已删除的邮件(来自"可恢复邮件"文件夹)、提取邮件头以追踪邮件路由、分析附件中的恶意软件或外泄数据,以及重建通信模式。现代 PST 文件使用 Unicode 格式,页面大小 4KB,最大可达 50GB;而旧版 ANSI 格式限制为 2GB。 |
| 6 | |
| 7 | ## 前置条件 |
| 8 | |
| 9 | - libpff/pffexport(开源 PST 解析器) |
| 10 | - Python 3.8+,附带 pypff 或 libratom 库 |
| 11 | - MailXaminer、Forensic Email Collector 或 SysTools PST Forensics(商业工具) |
| 12 | - Microsoft Outlook(可选,用于原生 PST 访问) |
| 13 | - 足够的磁盘空间用于存放提取内容 |
| 14 | |
| 15 | ## PST 文件位置 |
| 16 | |
| 17 | | 来源 | 路径 | |
| 18 | |------|------| |
| 19 | | Outlook 2016+ 默认位置 | %USERPROFILE%\Documents\Outlook Files\*.pst | |
| 20 | | Outlook 旧版 | %LOCALAPPDATA%\Microsoft\Outlook\*.pst | |
| 21 | | OST 缓存 | %LOCALAPPDATA%\Microsoft\Outlook\*.ost | |
| 22 | | 归档文件 | %USERPROFILE%\Documents\Outlook Files\archive.pst | |
| 23 | |
| 24 | ## 使用开源工具分析 |
| 25 | |
| 26 | ### libpff / pffexport |
| 27 | |
| 28 | ```bash |
| 29 | # 从 PST 文件导出所有项目 |
| 30 | pffexport -m all evidence.pst -t exported_pst |
| 31 | |
| 32 | # 仅导出电子邮件 |
| 33 | pffexport -m items evidence.pst -t exported_emails |
| 34 | |
| 35 | # 导出已恢复/已删除项目 |
| 36 | pffexport -m recovered evidence.pst -t recovered_items |
| 37 | |
| 38 | # 获取 PST 文件信息 |
| 39 | pffinfo evidence.pst |
| 40 | ``` |
| 41 | |
| 42 | ### Python PST 分析 |
| 43 | |
| 44 | ```python |
| 45 | import pypff |
| 46 | import os |
| 47 | import json |
| 48 | import hashlib |
| 49 | import email |
| 50 | import sys |
| 51 | from datetime import datetime |
| 52 | from collections import defaultdict |
| 53 | |
| 54 | |
| 55 | class PSTForensicAnalyzer: |
| 56 | """Outlook PST/OST 文件的取证分析器。""" |
| 57 | |
| 58 | def __init__(self, pst_path: str, output_dir: str): |
| 59 | self.pst_path = pst_path |
| 60 | self.output_dir = output_dir |
| 61 | os.makedirs(output_dir, exist_ok=True) |
| 62 | self.pst = pypff.file() |
| 63 | self.pst.open(pst_path) |
| 64 | self.messages = [] |
| 65 | self.attachments = [] |
| 66 | self.stats = defaultdict(int) |
| 67 | |
| 68 | def process_folder(self, folder, folder_path: str = ""): |
| 69 | """递归处理 PST 文件夹并提取邮件。""" |
| 70 | folder_name = folder.name or "Root" |
| 71 | current_path = f"{folder_path}/{folder_name}" if folder_path else folder_name |
| 72 | |
| 73 | for i in range(folder.number_of_sub_messages): |
| 74 | try: |
| 75 | message = folder.get_sub_message(i) |
| 76 | msg_data = self.extract_message(message, current_path) |
| 77 | if msg_data: |
| 78 | self.messages.append(msg_data) |
| 79 | self.stats["total_messages"] += 1 |
| 80 | except Exception as e: |
| 81 | self.stats["parse_errors"] += 1 |
| 82 | |
| 83 | for i in range(folder.number_of_sub_folders): |
| 84 | try: |
| 85 | subfolder = folder.get_sub_folder(i) |
| 86 | self.process_folder(subfolder, current_path) |
| 87 | except Exception: |
| 88 | continue |
| 89 | |
| 90 | def extract_message(self, message, folder_path: str) -> dict: |
| 91 | """从单封邮件中提取取证元数据。""" |
| 92 | msg_data = { |
| 93 | "folder": folder_path, |
| 94 | "subject": message.subject or "", |
| 95 | "sender": message.sender_name or "", |
| 96 | "sender_email": "", |
| 97 | "creation_time": str(message.creation_time) if message.creation_time else None, |
| 98 | "delivery_time": str(message.delivery_time) if message.delivery_time else None, |
| 99 | "modification_time": str(message.modification_time) if message.modification_time else None, |
| 100 | "has_attachments": message.number_of_attachments > 0, |
| 101 | "attachment_count": message.number_of_attachments, |
| 102 | "body_size": len(message.plain_text_body or b""), |
| 103 | "html_size": len(message.html_body or b""), |
| 104 | } |
| 105 | |
| 106 | # 提取传输头用于路由分析 |
| 107 | headers = message.transport_headers |
| 108 | if headers: |
| 109 | msg_data["headers_present"] = True |
| 110 | msg_data["headers_size"] = len(headers) |
| 111 | # 解析关键头字段 |
| 112 | parsed = email.message_from_string(headers) |
| 113 | msg_data["from_header"] = parsed.get("From", "") |
| 114 | msg_data["to_header"] = parsed.get("To", "") |
| 115 | msg_data["date_header"] = parsed.get("Date", "") |
| 116 | msg_data["message_id"] = parsed.get("Message-ID", "") |
| 117 | msg_data["x_originating_ip"] = parsed.get("X-Originating-IP", "") |
| 118 | msg_data["received_headers"] = parsed.get_all("Received", []) |
| 119 | |
| 120 | # 处理附件 |
| 121 | for j in range(message.number_of_attachments): |
| 122 | try: |
| 123 | attachment = message.get_attachment(j) |
| 124 | att_data = { |
| 125 | "message_subject": msg_data["subject"], |
| 126 | "name": attachment.name or f"attachment_{j}", |
| 127 | "size": attachment.size, |
| 128 | "content_type": "", |
| 129 | } |
| 130 | self.attachments.append(att_data) |
| 131 | self.stats["total_attachments"] += 1 |
| 132 | except E |