fix(pe): reject truncated certificate tables and PKCS#7

SignedData now requires one SignerInfo with a non-empty signature and a
complete issuerAndSerialNumber. Certificate names share a 64 KiB decoded-byte
budget and are copied once. A security directory that ends in a short
WIN_CERTIFICATE header fails instead of reporting a complete table. The
certificate CLI forwards the caller abort signal into cleanup.

Co-authored-by: morluto <morluto@users.noreply.github.com>
This commit is contained in:
Cursor Agentandmorluto committed 2026-10-22 05:56:41 +00:00
1 parent fefe46ac3a
commit 77e6fbcb79
11 files changed
+307 -95

No files matched your search

+1 -1
View File
@@ -50,7 +50,7 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
| Task | Guides |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) |
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [PE certificates](pe-certificates.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [PE certificates](pe-certificates.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [Kuna](kuna-provider.md), [JEB](jeb-analysis.md) |
| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md) |
+5 -1
View File
@@ -39,7 +39,11 @@ image is unsigned in every catalog or overlay sense.
The selected file is limited to 64 MiB by default (512 MiB maximum). At most
eight `WIN_CERTIFICATE` entries are examined by default. Malformed PE headers,
truncated certificate entries, and PKCS#7 that is not structurally signedData
fail without partial success.
fail without partial success. A security directory that ends in fewer than
eight header bytes, including zeros, is a truncated `WIN_CERTIFICATE` and
fails the same way. SignedData must contain at least one `SignerInfo`, and
each `SignerInfo` must carry a non-empty signature value. Decoded certificate
name text shares a 64 KiB budget per PKCS#7 payload.
This workflow does not verify signatures, certificate chains, timestamps, or
Authenticode digest coverage of image bytes.
+1 -1
View File
@@ -153,7 +153,7 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](../binary-diagnostics.md) |
| Go 바이너리 | 내장된 컴파일러 버전, 모듈, `replace` 지시어, 빌드 설정 | REA 내장 ELF/PE/thin Mach-O 정적 검사; Go 설치 불필요 |
| Rust 바이너리 | cargo-auditable이 내장한 크레이트 이름, 버전, 소스, 의존성 관계 | REA 내장 ELF/PE/thin Mach-O 정적 검사; [Rust 바이너리](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| WASM 아티팩트 | 유효성 검사, 섹션, import/export의 정확한 WAT 형식, 디코딩된 WAT | 호출자 제공 WABT 1.0.42; `inspect_wasm_artifact` |
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
| SQLite 데이터베이스 | 테이블, 열, 인덱스, 뷰, 트리거, 선택한 일반 테이블의 행 | 로컬 데이터베이스/WAL 스냅샷; Node.js 24.15 이상의 24.x 또는 26 이상(네이티브 SQLite 한도 적용) |
+1 -1
View File
@@ -145,7 +145,7 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](../binary-diagnostics.md) |
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
| Установленные настольные приложения | Возможные среды выполнения, файлы с доказательствами, компоненты и дальнейшие шаги | Ограниченная проверка каталогов и заголовков;[Настольные приложения](../desktop-applications.md) |
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](../javascript-application-workflows.md) |
+1 -1
View File
@@ -145,7 +145,7 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](../binary-diagnostics.md) |
| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](../recorded-crashes.md) |
| Thư mục cài đặt ứng dụng máy tính | Các runtime có khả năng được dùng, tệp bằng chứng, dấu hiệu thành phần và bước tiếp theo | Kiểm tra thư mục và phần đầu tệp có giới hạn;[Ứng dụng máy tính](../desktop-applications.md) |
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](../javascript-application-workflows.md) |
+26 -26
View File
@@ -141,33 +141,33 @@ AI 代理通過 MCP 呼叫 REA,檢查目標並追蹤相關程式碼。REA 返
REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。額外工具和主機支援取決於分析目標:
| 目標 | REA 返回的內容 | 要求與指南 |
| --------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
| 離線 ELF 佈局 | 節、段、原始符號/重定位資訊及靜態防護機制候選項 | Linux x64 上由呼叫方提供的 pwntools;[二進位診斷](../binary-diagnostics.md) |
| Go 二進位檔案 | 內嵌編譯器版本、模組、替換項和建置設定 | 內建靜態 ELF/PE/thin Mach-O 檢查;無需安裝 Go |
| WASM 工件 | 驗證、節、精確的匯入/匯出 WAT 形式及解碼後的 WAT | 呼叫方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 目標 | REA 返回的內容 | 要求與指南 |
| --------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
| 離線 ELF 佈局 | 節、段、原始符號/重定位資訊及靜態防護機制候選項 | Linux x64 上由呼叫方提供的 pwntools;[二進位診斷](../binary-diagnostics.md) |
| Go 二進位檔案 | 內嵌編譯器版本、模組、替換項和建置設定 | 內建靜態 ELF/PE/thin Mach-O 檢查;無需安裝 Go |
| WASM 工件 | 驗證、節、精確的匯入/匯出 WAT 形式及解碼後的 WAT | 呼叫方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
| 桌面安裝目錄 | 執行階段候選、支持檔案、元件標記與後續操作 | 有界的目錄與檔頭檢查;[桌面應用](../desktop-applications.md) |
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](../javascript-application-workflows.md) |
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](../browser-observation.md) |
| 已保存的網路擷取 | 請求、回應、可訪問的酬載和來源位置 | HAR;原生 mitmproxy 擷取需要 Linux 上的 mitmdump;[擷取指南](../web-network-captures.md) |
| 封包擷取 | 端點、DNS、TLS SNI、通訊協定分佈和 HTTP 請求 | 呼叫端提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
| .NET 程式集 | 中繼資料、CIL 指令、聲明的原生依賴和建置對比 | 靜態檢查;[托管程式碼指南](../managed-code-analysis.md) |
| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Android APK | 清單聲明、類別、反編譯的方法和引用 | Linux/macOS/Windows x64 上的無界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
| Android 裝置 | 裝置、套件、程序、日誌、傾印、螢幕擷取以及 APK/檔案傳輸 | 呼叫端提供的 adb(模擬器或實體裝置);[ADB 指南](../adb-device-analysis.md) |
| JEB 專案 | 引擎支援的專案單元、成品摘要和型別/方法的虛擬碼 | 由呼叫端啟動、提供 MCP 的 JEB 用戶端,透過 REA_JEB_MCP_URL 選擇;[JEB 指南](../jeb-analysis.md) |
| Android 資源 | 解碼後的清單、字串表、語系和版本資訊 | 呼叫端提供的 Apktool 啟動器;[資源指南](../apktool-resource-analysis.md) |
| Flutter 應用 | Dart 快照雜湊、依 ABI 的程式庫雜湊及引擎建置來源 | 無(靜態 APK 解析);[Flutter 指南](../flutter-analysis.md) |
| 韌體 | 區域、提取結果和轉交原生分析的內容 | Linux 上的 Binwalk / Unblob;[韌體指南](../firmware-analysis.md) |
| 封包記錄 | 指定欄位、重複值、篩選條件與解碼規則 | Linux: TShark 4.x + prlimit; [封包分析指南](../packet-captures.md) |
| 錄製的邏輯訊號 | 解碼註解、樣本範圍與診斷資訊 | Linux: sigrok-cli 0.7.2 + prlimit; [訊號記錄指南](../signal-recordings.md) |
| 軟體包與資源 | 檔案清單、摘要、plist、Apple bundle 結構和提取的資源 | [製品與 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 應用](../apple-application-analysis.md) |
| 處理程序行為 | 終端輸出、互動、退出和檔案系統觀察結果,以及執行對比 | 支援原生 PTY 的 Linux/macOS;[處理程序擷取](../process-capture.md) |
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
| 桌面安裝目錄 | 執行階段候選、支持檔案、元件標記與後續操作 | 有界的目錄與檔頭檢查;[桌面應用](../desktop-applications.md) |
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](../javascript-application-workflows.md) |
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](../browser-observation.md) |
| 已保存的網路擷取 | 請求、回應、可訪問的酬載和來源位置 | HAR;原生 mitmproxy 擷取需要 Linux 上的 mitmdump;[擷取指南](../web-network-captures.md) |
| 封包擷取 | 端點、DNS、TLS SNI、通訊協定分佈和 HTTP 請求 | 呼叫端提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
| .NET 程式集 | 中繼資料、CIL 指令、聲明的原生依賴和建置對比 | 靜態檢查;[托管程式碼指南](../managed-code-analysis.md) |
| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Android APK | 清單聲明、類別、反編譯的方法和引用 | Linux/macOS/Windows x64 上的無界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
| Android 裝置 | 裝置、套件、程序、日誌、傾印、螢幕擷取以及 APK/檔案傳輸 | 呼叫端提供的 adb(模擬器或實體裝置);[ADB 指南](../adb-device-analysis.md) |
| JEB 專案 | 引擎支援的專案單元、成品摘要和型別/方法的虛擬碼 | 由呼叫端啟動、提供 MCP 的 JEB 用戶端,透過 REA_JEB_MCP_URL 選擇;[JEB 指南](../jeb-analysis.md) |
| Android 資源 | 解碼後的清單、字串表、語系和版本資訊 | 呼叫端提供的 Apktool 啟動器;[資源指南](../apktool-resource-analysis.md) |
| Flutter 應用 | Dart 快照雜湊、依 ABI 的程式庫雜湊及引擎建置來源 | 無(靜態 APK 解析);[Flutter 指南](../flutter-analysis.md) |
| 韌體 | 區域、提取結果和轉交原生分析的內容 | Linux 上的 Binwalk / Unblob;[韌體指南](../firmware-analysis.md) |
| 封包記錄 | 指定欄位、重複值、篩選條件與解碼規則 | Linux: TShark 4.x + prlimit; [封包分析指南](../packet-captures.md) |
| 錄製的邏輯訊號 | 解碼註解、樣本範圍與診斷資訊 | Linux: sigrok-cli 0.7.2 + prlimit; [訊號記錄指南](../signal-recordings.md) |
| 軟體包與資源 | 檔案清單、摘要、plist、Apple bundle 結構和提取的資源 | [製品與 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 應用](../apple-application-analysis.md) |
| 處理程序行為 | 終端輸出、互動、退出和檔案系統觀察結果,以及執行對比 | 支援原生 PTY 的 Linux/macOS;[處理程序擷取](../process-capture.md) |
靜態 JavaScript 和 .NET 檢查讀取提供的檔案,不執行應用。執行階段擷取會以你的使用者權限執行目標或與之互動;各執行階段指南說明瞭具體影響。
+26 -26
View File
@@ -141,33 +141,33 @@ npx rea-agents@latest setup
REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。额外工具和主机支持取决于分析目标:
| 目标 | REA 返回的内容 | 要求与指南 |
| --------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](../binary-diagnostics.md) |
| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
| WASM 工件 | 验证、节、精确的导入/导出 WAT 形式及解码后的 WAT | 调用方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 目标 | REA 返回的内容 | 要求与指南 |
| --------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](../binary-diagnostics.md) |
| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
| WASM 工件 | 验证、节、精确的导入/导出 WAT 形式及解码后的 WAT | 调用方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
| 桌面安装目录 | 运行时候选、支持文件、组件标记和后续操作 | 有界的目录与文件头检查;[桌面应用](../desktop-applications.md) |
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-application-workflows.md) |
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](../browser-observation.md) |
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](../web-network-captures.md) |
| 数据包捕获 | 端点、DNS、TLS SNI、协议分布和 HTTP 请求 | 调用方提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](../managed-code-analysis.md) |
| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
| Android 设备 | 设备、软件包、进程、日志、转储、屏幕截图以及 APK/文件传输 | 调用方提供的 adb(模拟器或实体设备);[ADB 指南](../adb-device-analysis.md) |
| JEB 项目 | 引擎支持的项目单元、工件摘要和类型/方法的伪代码 | 由调用方启动、提供 MCP 的 JEB 客户端,通过 REA_JEB_MCP_URL 选择;[JEB 指南](../jeb-analysis.md) |
| Android 资源 | 解码后的清单、字符串表、语言区域和版本信息 | 调用方提供的 Apktool 启动器;[资源指南](../apktool-resource-analysis.md) |
| Flutter 应用 | Dart 快照哈希、按 ABI 的库摘要及引擎构建溯源 | 无(静态 APK 解析);[Flutter 指南](../flutter-analysis.md) |
| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](../firmware-analysis.md) |
| 抓包文件 | 指定字段、重复值、过滤条件与解码规则 | Linux: TShark 4.x + prlimit; [抓包分析指南](../packet-captures.md) |
| 录制的逻辑信号 | 解码注释、样本范围和诊断信息 | Linux: sigrok-cli 0.7.2 + prlimit; [信号记录指南](../signal-recordings.md) |
| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 应用](../apple-application-analysis.md) |
| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](../process-capture.md) |
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
| 桌面安装目录 | 运行时候选、支持文件、组件标记和后续操作 | 有界的目录与文件头检查;[桌面应用](../desktop-applications.md) |
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-application-workflows.md) |
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](../browser-observation.md) |
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](../web-network-captures.md) |
| 数据包捕获 | 端点、DNS、TLS SNI、协议分布和 HTTP 请求 | 调用方提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](../managed-code-analysis.md) |
| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
| Android 设备 | 设备、软件包、进程、日志、转储、屏幕截图以及 APK/文件传输 | 调用方提供的 adb(模拟器或实体设备);[ADB 指南](../adb-device-analysis.md) |
| JEB 项目 | 引擎支持的项目单元、工件摘要和类型/方法的伪代码 | 由调用方启动、提供 MCP 的 JEB 客户端,通过 REA_JEB_MCP_URL 选择;[JEB 指南](../jeb-analysis.md) |
| Android 资源 | 解码后的清单、字符串表、语言区域和版本信息 | 调用方提供的 Apktool 启动器;[资源指南](../apktool-resource-analysis.md) |
| Flutter 应用 | Dart 快照哈希、按 ABI 的库摘要及引擎构建溯源 | 无(静态 APK 解析);[Flutter 指南](../flutter-analysis.md) |
| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](../firmware-analysis.md) |
| 抓包文件 | 指定字段、重复值、过滤条件与解码规则 | Linux: TShark 4.x + prlimit; [抓包分析指南](../packet-captures.md) |
| 录制的逻辑信号 | 解码注释、样本范围和诊断信息 | Linux: sigrok-cli 0.7.2 + prlimit; [信号记录指南](../signal-recordings.md) |
| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 应用](../apple-application-analysis.md) |
| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](../process-capture.md) |
静态 JavaScript 和 .NET 检查读取提供的文件,不运行应用。运行时捕获会以你的用户权限运行目标或与之交互;各运行时指南说明了具体影响。
+1
View File
@@ -52,6 +52,7 @@ export const registerBinaryDiagnosticsCommands = (
pe,
"rea-pe-certificates",
"inspect_pe_certificates",
signal,
),
);
}),
+113 -33
View File
@@ -113,6 +113,22 @@ export const oidName = (oid: string): string | null => OID_NAMES[oid] ?? null;
export const MAX_OID_BYTES = 256;
export const MAX_OID_ARC_BYTES = 32;
// One PKCS#7 payload shares this ceiling. Real issuer and subject names are
// far smaller; the cap stops one wide string from occupying the file budget.
export const MAX_DECODED_NAME_BYTES = 65_536;
interface DecodedNameBudget {
remaining: number;
}
const chargeDecodedName = (budget: DecodedNameBudget, bytes: number): void => {
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > budget.remaining)
fail(
`Certificate name text exceeds the ${String(MAX_DECODED_NAME_BYTES)}-byte decoded budget.`,
);
budget.remaining -= bytes;
};
/** Decode an OBJECT IDENTIFIER content buffer as dotted decimal. */
export const decodeOid = (content: Buffer): string => {
if (content.length === 0) return fail("Empty OBJECT IDENTIFIER.");
@@ -144,41 +160,64 @@ export const decodeOid = (content: Buffer): string => {
return [first, combined - first * 40n, ...rest].join(".");
};
const decodeUtf16Be = (bytes: Buffer): string => {
const decodeUtf16Be = (bytes: Buffer, budget: DecodedNameBudget): string => {
if (bytes.length % 2 !== 0) return fail("BMPString length is odd.");
let text = "";
for (let index = 0; index < bytes.length; index += 2)
text += String.fromCharCode(bytes.readUInt16BE(index));
return text;
// Charge before the copy. Appending one character at a time retains a
// cons-string chain of about sixteen heap bytes per source byte.
chargeDecodedName(budget, bytes.length);
if (bytes.length === 0) return "";
const decoded = Buffer.allocUnsafe(bytes.length);
for (let index = 0; index < bytes.length; index += 2) {
decoded[index] = bytes[index + 1] ?? 0;
decoded[index + 1] = bytes[index] ?? 0;
}
return decoded.toString("utf16le");
};
const decodeUtf32Be = (bytes: Buffer): string => {
const decodeUtf32Be = (bytes: Buffer, budget: DecodedNameBudget): string => {
if (bytes.length % 4 !== 0)
return fail("UniversalString length is not a multiple of 4.");
let text = "";
// Four source bytes become at most four UTF-16 bytes, so the source length
// bounds the decoded string before any code point is copied.
chargeDecodedName(budget, bytes.length);
const decoded = Buffer.allocUnsafe(bytes.length);
let at = 0;
for (let index = 0; index < bytes.length; index += 4) {
const codePoint = bytes.readUInt32BE(index);
if (codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff))
return fail("UniversalString holds an invalid code point.");
text += String.fromCodePoint(codePoint);
if (codePoint > 0xffff) {
const shifted = codePoint - 0x10000;
decoded.writeUInt16LE(0xd800 + (shifted >> 10), at);
decoded.writeUInt16LE(0xdc00 + (shifted & 0x3ff), at + 2);
at += 4;
} else {
decoded.writeUInt16LE(codePoint, at);
at += 2;
}
}
return text;
return decoded.subarray(0, at).toString("utf16le");
};
const decodePrintable = (node: DerNode): string => {
const decodeDirectoryString = (
node: DerNode,
budget: DecodedNameBudget,
): string => {
switch (node.tag) {
case 0x1c:
return decodeUtf32Be(node.content);
return decodeUtf32Be(node.content, budget);
case 0x1e:
return decodeUtf16Be(node.content, budget);
case 0x0c:
case 0x13:
case 0x16:
case 0x17:
case 0x18:
case 0x1b:
chargeDecodedName(budget, node.content.length);
return node.content.toString("utf8");
case 0x1e:
return decodeUtf16Be(node.content);
default:
chargeDecodedName(budget, node.content.length);
return node.content.toString("latin1");
}
};
@@ -210,6 +249,7 @@ interface Attribute {
const decodeName = (
node: DerNode,
budget: DecodedNameBudget,
): {
readonly text: string;
readonly attributes: readonly Attribute[];
@@ -231,7 +271,7 @@ const decodeName = (
attributes.push({
oid,
oid_name: oidName(oid),
value: decodePrintable(valueNode),
value: decodeDirectoryString(valueNode, budget),
});
}
}
@@ -247,7 +287,7 @@ const decodeName = (
const validityTime = (node: DerNode): string => {
if (node.tag !== 0x17 && node.tag !== 0x18)
return fail("Validity instant is not UTCTime or GeneralizedTime.");
return decodePrintable(node);
return node.content.toString("utf8");
};
export interface ParsedCertificate {
@@ -262,7 +302,10 @@ export interface ParsedCertificate {
}
/** Decode one X.509 Certificate SEQUENCE at a known file-relative DER node. */
export const parseCertificate = (node: DerNode): ParsedCertificate => {
export const parseCertificate = (
node: DerNode,
names: DecodedNameBudget = { remaining: MAX_DECODED_NAME_BYTES },
): ParsedCertificate => {
const children = sequence(node, "Certificate");
const tbs = children[0];
const signatureAlgorithm = children[1];
@@ -294,8 +337,8 @@ export const parseCertificate = (node: DerNode): ParsedCertificate => {
start: node.start,
end: node.end,
serial_hex: integerHex(serial.content),
issuer: decodeName(issuer),
subject: decodeName(subject),
issuer: decodeName(issuer, names),
subject: decodeName(subject, names),
not_before: validityTime(notBefore),
not_after: validityTime(notAfter),
signature_algorithm_oid: algorithmOid(signatureAlgorithm),
@@ -310,7 +353,40 @@ export interface ParsedSigner {
readonly authenticated_attributes: boolean;
}
const parseSigner = (node: DerNode): ParsedSigner => {
const signerIdentifier = (
sid: DerNode,
names: DecodedNameBudget,
): {
readonly issuer: ReturnType<typeof decodeName> | null;
readonly serial_hex: string | null;
} => {
if (sid.tag === 0x30) {
const name = sid.children[0];
const serial = sid.children[1];
if (
sid.children.length !== 2 ||
name === undefined ||
name.tag !== 0x30 ||
serial === undefined ||
serial.tag !== 0x02
)
return fail("SignerInfo issuerAndSerialNumber is truncated.");
return {
issuer: decodeName(name, names),
serial_hex: integerHex(serial.content),
};
}
if (sid.tag === 0x80) {
if (sid.content.length === 0)
return fail("SignerInfo subjectKeyIdentifier is empty.");
return { issuer: null, serial_hex: null };
}
return fail(
"SignerInfo sid is not issuerAndSerialNumber or subjectKeyIdentifier.",
);
};
const parseSigner = (node: DerNode, names: DecodedNameBudget): ParsedSigner => {
const children = sequence(node, "SignerInfo");
const version = children[0];
const sid = children[1];
@@ -319,16 +395,7 @@ const parseSigner = (node: DerNode): ParsedSigner => {
return fail("SignerInfo is missing version.");
if (sid === undefined || digestAlg === undefined)
return fail("SignerInfo is missing sid or digestAlgorithm.");
let issuer = null;
let serial_hex: string | null = null;
if (sid.tag === 0x30) {
const name = sid.children[0];
const serial = sid.children[1];
if (name !== undefined && serial !== undefined && serial.tag === 0x02) {
issuer = decodeName(name);
serial_hex = integerHex(serial.content);
}
}
const { issuer, serial_hex } = signerIdentifier(sid, names);
let cursor = 3;
const authenticated = children[cursor]?.tag === 0xa0;
if (authenticated) cursor += 1;
@@ -338,6 +405,8 @@ const parseSigner = (node: DerNode): ParsedSigner => {
const signature = children[cursor + 1];
if (signature === undefined || signature.tag !== 0x04)
return fail("SignerInfo is missing its signature OCTET STRING.");
if (signature.content.length === 0)
return fail("SignerInfo signature OCTET STRING is empty.");
cursor += 2;
if (children[cursor]?.tag === 0xa1) cursor += 1;
if (cursor !== children.length)
@@ -358,7 +427,10 @@ export interface ParsedSignedData {
readonly signers: readonly ParsedSigner[];
}
const parseSignedData = (node: DerNode): ParsedSignedData => {
const parseSignedData = (
node: DerNode,
names: DecodedNameBudget,
): ParsedSignedData => {
const children = sequence(node, "SignedData");
const [version, digestSet, encapContentInfo] = children;
if (version === undefined || version.tag !== 0x02)
@@ -382,10 +454,16 @@ const parseSignedData = (node: DerNode): ParsedSignedData => {
const signerSet = children[cursor];
if (signerSet === undefined || signerSet.tag !== 0x31)
return fail("SignedData is missing signerInfos.");
if (signerSet.children.length === 0)
return fail("SignedData signerInfos contains no SignerInfo.");
if (cursor !== children.length - 1)
return fail("SignedData has unexpected trailing fields.");
const certificates = (certificateSet?.children ?? []).map(parseCertificate);
const signers = signerSet.children.map(parseSigner);
const certificates = (certificateSet?.children ?? []).map((certificate) =>
parseCertificate(certificate, names),
);
const signers = signerSet.children.map((signer) =>
parseSigner(signer, names),
);
return {
content_type_oid: "1.2.840.113549.1.7.2",
digest_algorithm_oids,
@@ -408,5 +486,7 @@ export const parsePkcs7SignedData = (bytes: Buffer): ParsedSignedData => {
const signedData = content?.children[0];
if (content === undefined || content.tag !== 0xa0 || signedData === undefined)
return fail("signedData content is missing.");
return parseSignedData(signedData);
return parseSignedData(signedData, {
remaining: MAX_DECODED_NAME_BYTES,
});
};
+128
View File
@@ -9,6 +9,7 @@ import {
} from "./PeCertificates.fixture.js";
import {
DER_NODE_BUDGET,
MAX_DECODED_NAME_BYTES,
MAX_OID_ARC_BYTES,
MAX_OID_BYTES,
decodeOid,
@@ -78,6 +79,25 @@ it("fails truncated WIN_CERTIFICATE payloads without partial success", () => {
expect(() => parse(bytes)).toThrow(ArtifactReaderFailure);
});
const securityDirectoryAt = 0x98 + 96 + 4 * 8;
it("rejects a short security table instead of reporting it complete", () => {
for (const size of [4, 7]) {
const bytes = peCertificateFixture();
const tableOffset = bytes.readUInt32LE(securityDirectoryAt);
bytes.writeUInt32LE(size, securityDirectoryAt + 4);
bytes.fill(0, tableOffset, tableOffset + size);
expect(() => parse(bytes)).toThrow(/header is truncated/u);
}
const bytes = peCertificateFixture();
const tableOffset = bytes.readUInt32LE(securityDirectoryAt);
const tableSize = bytes.readUInt32LE(securityDirectoryAt + 4);
const extended = Buffer.concat([bytes, Buffer.alloc(4)]);
extended.writeUInt32LE(tableSize + 4, securityDirectoryAt + 4);
expect(extended.readUInt32LE(tableOffset)).toBeGreaterThan(0);
expect(() => parse(extended)).toThrow(/header is truncated/u);
});
it("fails PKCS#7 payloads that are not signedData", () => {
const bytes = peCertificateFixture(false, Buffer.from("not-pkcs7"), 0x0002);
expect(() => parse(bytes)).toThrow(ArtifactReaderFailure);
@@ -152,6 +172,47 @@ it("fails a SignerInfo without its signature", () => {
);
});
it("fails a SignerInfo whose signature bytes were removed", () => {
const signer = derSeq(
derInt(1),
derSeq(derSeq(), derInt(1)),
SHA256_ALG,
SHA256_ALG,
der(0x04, Buffer.alloc(0)),
);
const payload = contentInfo(
derSeq(derInt(1), derSet(SHA256_ALG), derSeq(DATA_OID), derSet(signer)),
);
expect(() => parse(peCertificateFixture(false, payload))).toThrow(
/signature OCTET STRING is empty/u,
);
});
it("fails signedData whose signerInfos set is empty", () => {
const payload = contentInfo(
derSeq(derInt(1), derSet(SHA256_ALG), derSeq(DATA_OID), derSet()),
);
expect(() => parse(peCertificateFixture(false, payload))).toThrow(
/no SignerInfo/u,
);
});
it("fails a truncated issuerAndSerialNumber instead of dropping the signer", () => {
const signer = derSeq(
derInt(1),
derSeq(derSeq()),
SHA256_ALG,
SHA256_ALG,
der(0x04, Buffer.from([1])),
);
const payload = contentInfo(
derSeq(derInt(1), derSet(SHA256_ALG), derSeq(DATA_OID), derSet(signer)),
);
expect(() => parse(peCertificateFixture(false, payload))).toThrow(
/issuerAndSerialNumber is truncated/u,
);
});
it("stops decoding a certificate payload at the DER node budget", () => {
const values = Buffer.alloc(2 * (DER_NODE_BUDGET + 1));
for (let at = 0; at < values.length; at += 2) values[at] = 0x05;
@@ -190,6 +251,66 @@ it("rejects an oversized OBJECT IDENTIFIER before growing an arc", () => {
);
});
const cn = Buffer.from([0x06, 0x03, 0x55, 0x04, 0x03]);
const directoryName = (value: Buffer) => derSeq(derSet(derSeq(cn, value)));
const namedCertificate = (issuer: Buffer, subject: Buffer) =>
derSeq(
derSeq(
derInt(1),
SHA256_ALG,
issuer,
derSeq(
der(0x17, Buffer.from("250101000000Z")),
der(0x17, Buffer.from("260101000000Z")),
),
subject,
),
SHA256_ALG,
der(0x03, Buffer.from([0])),
);
it("decodes BMPString names once and shares the decoded-byte budget", () => {
const bmp = (text: string) => {
const content = Buffer.alloc(text.length * 2);
[...text].forEach((character, index) =>
content.writeUInt16BE(character.charCodeAt(0), index * 2),
);
return der(0x1e, content);
};
const certificate = readDer(
namedCertificate(directoryName(bmp("REA")), directoryName(bmp("Fixture"))),
);
expect(parseCertificate(certificate).subject.text).toBe("commonName=Fixture");
const half = MAX_DECODED_NAME_BYTES / 2;
const within = readDer(
namedCertificate(
directoryName(der(0x1e, Buffer.alloc(half))),
directoryName(der(0x1e, Buffer.alloc(half))),
),
);
expect(parseCertificate(within).issuer.text.startsWith("commonName=")).toBe(
true,
);
const over = readDer(
namedCertificate(
directoryName(der(0x1e, Buffer.alloc(half + 2))),
directoryName(der(0x1e, Buffer.alloc(half))),
),
);
expect(() => parseCertificate(over)).toThrow(/decoded budget/u);
const universal = Buffer.alloc(MAX_DECODED_NAME_BYTES + 4);
expect(() =>
parseCertificate(
readDer(
namedCertificate(
directoryName(der(0x1c, universal)),
directoryName(bmp("A")),
),
),
),
).toThrow(/decoded budget/u);
});
it("decodes UniversalString names as UTF-32BE", () => {
const universal = (text: string) => {
const content = Buffer.alloc(text.length * 4);
@@ -214,6 +335,13 @@ it("decodes UniversalString names as UTF-32BE", () => {
derSeq(tbs, SHA256_ALG, der(0x03, Buffer.from([0]))),
);
expect(parseCertificate(certificate).subject.text).toBe("commonName=REA");
const emoji = readDer(
namedCertificate(
directoryName(der(0x1c, Buffer.from([0x00, 0x01, 0xf6, 0x00]))),
directoryName(der(0x1c, Buffer.from([0x00, 0x01, 0xf6, 0x00]))),
),
);
expect(parseCertificate(emoji).subject.text).toBe("commonName=😀");
const odd = derSeq(derSet(derSeq(cn, der(0x1c, Buffer.from([0, 0, 0])))));
const broken = readDer(
derSeq(
+4 -5
View File
@@ -89,11 +89,10 @@ export const parsePeCertificates = (
let at = 0;
let examined = 0;
while (at < table.length) {
if (table.length - at < 8) {
if (table.subarray(at).some((byte) => byte !== 0))
fail("WIN_CERTIFICATE header is truncated.");
break;
}
// Alignment padding sits inside the gap before the next 8-byte boundary.
// Bytes that remain after that boundary are a short header, including
// zeros at the start of the table.
if (table.length - at < 8) fail("WIN_CERTIFICATE header is truncated.");
examined += 1;
if (examined > input.max_entries)
throw new ArtifactReaderFailure(