lowell_core/formats/pe.rs
1//! PE/COFF helpers for Unified Kernel Images (UKI)
2//!
3//! Read-only introspection of PE/EFI images (UKIs) with small, ergonomic helpers.
4//! We **own** the file bytes and parse with `goblin` on demand; methods then
5//! slice into `self.data`, so the public API stays lifetime-free.
6//!
7//! ### UKI sections you’ll typically care about
8//! - `.linux` — kernel image (Image/bzImage)
9//! - `.initrd` — initramfs blob (often gzip/xz/zstd; can be concatenated cpio)
10//! - `.cmdline` — kernel command line (ASCII/UTF-8, NUL-padded)
11//! - `.osrel` — os-release contents (text)
12//! - `.sbat` — SBAT CSV (shim)
13//! - `.sdmagic` — systemd-stub marker
14//!
15//! ### Certificates / Authenticode (Secure Boot)
16//! - Presence is indicated by the **Security** data directory (index 4).
17//! - In PE/COFF, **only** this directory uses a **file offset** (not an RVA).
18//! - `goblin` already parses certificates into `pe.certificates`, so you can
19//! inspect counts, lengths, types, and get the raw blobs directly.
20//! - We DO NOT verify signatures here; presence ≠ validity.
21
22use anyhow::{Context, Result};
23use goblin::pe::{options::ParseOptions, PE};
24use std::path::Path;
25
26/// An owning wrapper around a PE/EFI image (UKI).
27///
28/// Holds the file bytes, parses with goblin on demand, and returns
29/// borrowed slices tied to `&self`. This avoids lifetimes in the
30/// public API and side-steps self-referential types.
31#[derive(Debug)]
32pub struct PeFile {
33 /// Entire image bytes (owned).
34 data: Box<[u8]>,
35}
36
37impl PeFile {
38 /// Read a PE/EFI image from disk and own its bytes.
39 pub fn from_path(path: &Path) -> Result<Self> {
40 let bytes = std::fs::read(path).with_context(|| format!("read {}", path.display()))?;
41 Ok(Self {
42 data: bytes.into_boxed_slice(),
43 })
44 }
45
46 /// Construct from a caller-provided byte vector.
47 pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
48 Ok(Self {
49 data: bytes.into_boxed_slice(),
50 })
51 }
52
53 /// Access the full image buffer (read-only).
54 pub fn image(&self) -> &[u8] {
55 &self.data
56 }
57
58 // ---------- Parsing & basics ----------
59
60 /// Parse PE headers using options appropriate for on-disk binaries.
61 /// (We explicitly enable attribute certificate parsing.)
62 fn parse_pe(&self) -> Result<PE<'_>> {
63 let mut opts = ParseOptions::default();
64 opts.parse_attribute_certificates = true; // ensure certs are parsed
65 PE::parse_with_opts(&self.data, &opts).context("not a valid PE/EFI image")
66 }
67
68 /// Return a human-oriented architecture label and PE32+ flag.
69 ///
70 /// Common results:
71 /// - `("x86_64", true)` for amd64 UKIs
72 /// - `("aarch64", true)` for ARM64 UKIs
73 /// - `("i386", false)` for 32-bit x86
74 pub fn arch_summary(&self) -> Result<(&'static str, bool)> {
75 use goblin::pe::header::*;
76 let pe = self.parse_pe()?;
77 let arch = match pe.header.coff_header.machine {
78 COFF_MACHINE_X86_64 => "x86_64",
79 COFF_MACHINE_ARM64 => "aarch64",
80 COFF_MACHINE_ARM => "arm",
81 COFF_MACHINE_X86 => "i386",
82 _ => "unknown",
83 };
84 Ok((arch, pe.is_64))
85 }
86
87 // ---------- Sections ----------
88 //
89 /// Offset and file size of a named section, if it exists.
90 /// (file_offset, file_size)
91 pub fn section_info(&self, name: &str) -> Result<Option<(usize, usize)>> {
92 let pe = self.parse_pe()?;
93 let sec = pe.sections.iter().find(|t| t.name().ok() == Some(name));
94 if let Some(s) = sec {
95 Ok(Some((
96 s.pointer_to_raw_data as usize,
97 s.size_of_raw_data as usize,
98 )))
99 } else {
100 Ok(None)
101 }
102 }
103
104 /// Borrow raw bytes of a named section (e.g., ".initrd", ".linux", ".cmdline").
105 ///
106 /// Returns `Ok(None)` if the section is missing or coordinates are invalid.
107 pub fn section_bytes(&self, name: &str) -> Result<Option<&[u8]>> {
108 let pe = self.parse_pe()?;
109 let sec = pe.sections.iter().find(|t| t.name().ok() == Some(name));
110 if let Some(s) = sec {
111 let off = usize::try_from(s.pointer_to_raw_data).ok();
112 let sz = usize::try_from(s.size_of_raw_data).ok();
113 if let (Some(off), Some(sz)) = (off, sz) {
114 let end = off.checked_add(sz);
115 return Ok(end.and_then(|e| self.data.get(off..e)));
116 }
117 }
118 Ok(None)
119 }
120
121 /// Read a section as text (trim at first NUL). Ideal for `.cmdline` / `.osrel`.
122 pub fn read_text(&self, name: &str) -> Result<Option<String>> {
123 Ok(self.section_bytes(name)?.map(|b| {
124 let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
125 String::from_utf8_lossy(&b[..end]).to_string()
126 }))
127 }
128
129 // ---------- Certificates (Authenticode) ----------
130
131 /// True if the image contains one or more Attribute Certificates.
132 ///
133 /// Presence indicates a **Certificate Table** exists; it does **not** mean
134 /// the signature is valid. Modifying sections (e.g., `.initrd`) will typically
135 /// invalidate verification in Secure Boot.
136 pub fn is_signed(&self) -> Result<bool> {
137 let pe = self.parse_pe()?;
138 Ok(!pe.certificates.is_empty())
139 }
140
141 /// Lightweight metadata for each attribute certificate: (length, revision, type).
142 ///
143 /// `revision` and `typ` come from the WIN_CERTIFICATE header. The blob itself is
144 /// usually PKCS#7 SignedData (`typ` 0x0002).
145 pub fn certificate_metadata(&self) -> Result<Vec<(u32, u16, u16)>> {
146 let pe = self.parse_pe()?;
147 Ok(pe
148 .certificates
149 .iter()
150 .map(|c| (c.length, c.revision as u16, c.certificate_type as u16))
151 .collect())
152 }
153
154 /// The raw certificate blobs (`&[u8]`) for each attribute certificate.
155 pub fn certificate_blobs(&self) -> Result<Vec<&[u8]>> {
156 let pe = self.parse_pe()?;
157 Ok(pe.certificates.iter().map(|c| c.certificate).collect())
158 }
159}