$npx -y skills add killvxk/cybersecurity-skills-zh --skill analyzing-pdf-malware-with-pdfid使用 PDFiD、pdf-parser 和 peepdf 分析恶意 PDF 文件,在不打开文档的情况下 识别嵌入的 JavaScript、shellcode、漏洞利用代码和可疑对象。 确定攻击向量并提取嵌入的载荷以进行进一步分析。 适用于 PDF 恶意软件分析、恶意文档分析、PDF 漏洞利用调查或可疑附件分类等请求。
| 1 | # 使用 PDFiD 分析 PDF 恶意软件 |
| 2 | |
| 3 | ## 适用场景 |
| 4 | |
| 5 | - 可疑 PDF 附件被电子邮件安全系统标记或由用户报告 |
| 6 | - 需要确定 PDF 是否包含嵌入的 JavaScript、shellcode 或漏洞利用代码 |
| 7 | - 在沙箱或分析环境中打开 PDF 文件前进行分类筛查 |
| 8 | - 从恶意 PDF 对象中提取嵌入的可执行文件、脚本或 URL |
| 9 | - 分析针对 Adobe Reader 或其他 PDF 查看器漏洞的 PDF 漏洞利用工具包 |
| 10 | |
| 11 | **不适用于**分析 PDF 的视觉渲染内容;本流程用于对 PDF 文件格式进行结构化分析以检测恶意对象。 |
| 12 | |
| 13 | ## 前置条件 |
| 14 | |
| 15 | - Python 3.8+,安装 Didier Stevens 的 PDF 工具(`pip install pdfid pdf-parser`) |
| 16 | - peepdf,用于交互式 PDF 分析(`pip install peepdf`) |
| 17 | - poppler-utils 中的 pdftotext,用于安全提取文本内容 |
| 18 | - 带有 PDF 专用规则的 YARA,用于恶意软件家族识别 |
| 19 | - 隔离的分析虚拟机(未安装 PDF 阅读器,防止意外打开) |
| 20 | - CyberChef,用于解码嵌入的 Base64、十六进制或 deflate 流 |
| 21 | |
| 22 | ## 工作流程 |
| 23 | |
| 24 | ### 步骤 1:使用 PDFiD 进行初步分类 |
| 25 | |
| 26 | 扫描 PDF 中的可疑关键词和结构: |
| 27 | |
| 28 | ```bash |
| 29 | # 运行 PDFiD 识别可疑元素 |
| 30 | pdfid suspect.pdf |
| 31 | |
| 32 | # 预期输出分析: |
| 33 | # /JS - JavaScript(高风险) |
| 34 | # /JavaScript - JavaScript 对象(高风险) |
| 35 | # /AA - 打开时自动触发的动作(高风险) |
| 36 | # /OpenAction - 文档打开时的动作(高风险) |
| 37 | # /Launch - 启动外部应用程序(高风险) |
| 38 | # /EmbeddedFile - 嵌入文件(中风险) |
| 39 | # /RichMedia - Flash 内容(中风险) |
| 40 | # /ObjStm - 对象流(用于混淆) |
| 41 | # /URI - URL 引用(情境风险) |
| 42 | # /AcroForm - 交互式表单(中风险) |
| 43 | |
| 44 | # 运行带额外详情的模式 |
| 45 | pdfid -e suspect.pdf |
| 46 | |
| 47 | # 运行拆解模式(重命名可疑关键词) |
| 48 | pdfid -d suspect.pdf |
| 49 | ``` |
| 50 | |
| 51 | ``` |
| 52 | PDFiD 风险评估: |
| 53 | ━━━━━━━━━━━━━━━━━━━━━ |
| 54 | 高风险指标(任意计数 > 0): |
| 55 | /JS, /JavaScript -> 嵌入 JavaScript 代码 |
| 56 | /AA -> 自动动作(无需用户交互即触发) |
| 57 | /OpenAction -> 文档打开时执行代码 |
| 58 | /Launch -> 可启动外部可执行文件 |
| 59 | /JBIG2Decode -> 与 CVE-2009-0658 漏洞利用相关 |
| 60 | |
| 61 | 中风险指标: |
| 62 | /EmbeddedFile -> 包含嵌入文件(可能是 EXE/DLL) |
| 63 | /RichMedia -> Flash/多媒体(Flash 漏洞利用) |
| 64 | /AcroForm -> 带有可能提交动作的表单 |
| 65 | /XFA -> XML 表单架构(复杂攻击面) |
| 66 | |
| 67 | 低风险指标: |
| 68 | /ObjStm -> 对象流(混淆技术) |
| 69 | /URI -> 外部 URL 引用 |
| 70 | /Page -> 页面数量(仅供参考) |
| 71 | ``` |
| 72 | |
| 73 | ### 步骤 2:使用 pdf-parser 解析 PDF 结构 |
| 74 | |
| 75 | 检查 PDFiD 标记的可疑对象: |
| 76 | |
| 77 | ```bash |
| 78 | # 列出所有引用 JavaScript 的对象 |
| 79 | pdf-parser --search "/JavaScript" suspect.pdf |
| 80 | pdf-parser --search "/JS" suspect.pdf |
| 81 | |
| 82 | # 列出所有带 OpenAction 的对象 |
| 83 | pdf-parser --search "/OpenAction" suspect.pdf |
| 84 | |
| 85 | # 按 ID 提取特定对象(示例:对象 5) |
| 86 | pdf-parser --object 5 suspect.pdf |
| 87 | |
| 88 | # 提取并解压流内容 |
| 89 | pdf-parser --object 5 --filter --raw suspect.pdf |
| 90 | |
| 91 | # 搜索嵌入文件 |
| 92 | pdf-parser --search "/EmbeddedFile" suspect.pdf |
| 93 | |
| 94 | # 列出所有对象及其类型 |
| 95 | pdf-parser --stats suspect.pdf |
| 96 | ``` |
| 97 | |
| 98 | ### 步骤 3:提取并分析嵌入的 JavaScript |
| 99 | |
| 100 | 从 PDF 对象中提取 JavaScript 代码: |
| 101 | |
| 102 | ```bash |
| 103 | # 使用 pdf-parser 提取 JavaScript |
| 104 | pdf-parser --search "/JS" --raw --filter suspect.pdf > extracted_js.txt |
| 105 | |
| 106 | # 替代方案:使用 peepdf 交互式提取 JavaScript |
| 107 | peepdf -f -i suspect.pdf << 'EOF' |
| 108 | js_analyse |
| 109 | EOF |
| 110 | |
| 111 | # peepdf JS 分析交互命令: |
| 112 | # js_analyse - 提取并显示所有 JavaScript 代码 |
| 113 | # js_beautify - 格式化提取的 JavaScript |
| 114 | # js_eval <object> - 在沙箱环境中执行 JavaScript |
| 115 | # object <id> - 显示对象内容 |
| 116 | # rawobject <id> - 显示原始对象字节 |
| 117 | # stream <id> - 显示解压后的流 |
| 118 | # offsets - 显示文件中的对象偏移量 |
| 119 | ``` |
| 120 | |
| 121 | ```python |
| 122 | # 用于全面提取 PDF JavaScript 的 Python 脚本 |
| 123 | import subprocess |
| 124 | import re |
| 125 | |
| 126 | # 提取所有流并搜索 JavaScript |
| 127 | result = subprocess.run( |
| 128 | ["pdf-parser", "--stats", "suspect.pdf"], |
| 129 | capture_output=True, text=True |
| 130 | ) |
| 131 | |
| 132 | # 找到包含 JavaScript 引用的对象 ID |
| 133 | js_objects = [] |
| 134 | for line in result.stdout.split('\n'): |
| 135 | if '/JavaScript' in line or '/JS' in line: |
| 136 | obj_id = re.search(r'obj (\d+)', line) |
| 137 | if obj_id: |
| 138 | js_objects.append(obj_id.group(1)) |
| 139 | |
| 140 | # 提取每个包含 JavaScript 的对象 |
| 141 | for obj_id in js_objects: |
| 142 | result = subprocess.run( |
| 143 | ["pdf-parser", "--object", obj_id, "--filter", "--raw", "suspect.pdf"], |
| 144 | capture_output=True, text=True |
| 145 | ) |
| 146 | print(f"\n=== 对象 {obj_id} ===") |
| 147 | print(result.stdout[:2000]) |
| 148 | ``` |
| 149 | |
| 150 | ### 步骤 4:分析嵌入的 Shellcode |
| 151 | |
| 152 | 从 PDF 漏洞利用中提取并检查 shellcode: |
| 153 | |
| 154 | ```bash |
| 155 | # 提取原始流数据用于 shellcode 分析 |
| 156 | pdf-parser --object 7 --filter --raw --dump shellcode.bin suspect.pdf |
| 157 | |
| 158 | # 使用 scdbg(shellcode 调试器)分析 shellcode |
| 159 | scdbg /f shellcode.bin |
| 160 | |
| 161 | # 替代方案:使用 speakeasy 进行 shellcode 模拟 |
| 162 | python3 -c " |
| 163 | import speakeasy |
| 164 | |
| 165 | se = speakeasy.Speakeasy() |
| 166 | sc_addr = se.load_shellcode('shellcode.bin', arch='x86') |
| 167 | se.run_shellcode(sc_addr, count=1000) |
| 168 | |
| 169 | # 查看 shellcode 的 API 调用 |
| 170 | for event in se.get_report()['api_calls']: |
| 171 | print(f\"{event['api']}: {event['args']}\") |
| 172 | " |
| 173 | |
| 174 | # 使用 CyberChef 解码十六进制/Base64 编码的 shellcode |
| 175 | # 输入:提取的流数据 |
| 176 | # 配方:From Hex -> Disassemble x86 |
| 177 | ``` |
| 178 | |
| 179 | ### 步骤 5:提取嵌入的文件和 URL |
| 180 | |
| 181 | 提取嵌入的可执行文件和链接资源: |
| 182 | |
| 183 | ```python |
| 184 | # 从 PDF 中提取嵌入文件 |
| 185 | import subprocess |
| 186 | import hashlib |
| 187 | |
| 188 | # 查找嵌入文件对象 |
| 189 | result = subprocess.run( |
| 190 | ["pdf-parser", "--search", "/EmbeddedFile", "--raw", "--filter", "suspect.pdf"], |
| 191 | capture_output=True |
| 192 | ) |
| 193 | |
| 194 | # 通过搜索 MZ 头部提取嵌入的 PE 文件 |
| 195 | with open("suspect.pdf", "rb") as f: |
| 196 | data = f.read() |
| 197 | |
| 198 | # 搜索嵌入的 PE 文件 |
| 199 | offset = 0 |
| 200 | while True: |
| 201 | pos = data.find(b'MZ', offset) |
| 202 | if pos == -1: |
| 203 | break |
| 204 | # 验证 PE 签名 |
| 205 | if pos + 0x3C < len(data): |
| 206 | pe_offset = int.from_bytes(data[pos+0x3C:pos+0x40], 'little') |
| 207 | if pos + pe_offset + 2 < len(data) and data[pos+pe_offset:pos+pe_offset+2] == b'PE': |
| 208 | p |