Skip to main content

dnfile/
lib.rs

1// Several private helpers take `&u32` / `&usize` arguments for historical
2// reasons (these types are trivially `Copy`). Changing them ripples through
3// many call sites for no behavioural benefit.
4#![allow(clippy::trivially_copy_pass_by_ref)]
5// `ClrHeaderFlags::new` deliberately returns a `BTreeSet<Self>` (bitfield
6// expansion) rather than `Self` — this is part of the public API.
7#![allow(clippy::new_ret_no_self)]
8
9use serde::{Deserialize, Serialize};
10
11pub mod error;
12pub mod lang;
13pub mod resource;
14/// 0.5.1: typed decoders for `#Blob`-heap signature payloads.
15/// Currently covers `CustomAttribute` (ECMA-335 II.23.3) and
16/// `MarshalSpec` (II.23.4). The full ECMA-335 II.23.2
17/// signature-blob grammar (MethodDef/Ref/FieldSig/PropertySig/
18/// LocalVarSig/TypeSpec/MethodSpec + recursive TypeSig) is
19/// intentionally NOT here yet — see the module docs.
20pub mod signatures;
21pub mod stream;
22pub mod utils;
23
24use crate::{
25    error::Error,
26    stream::meta_data_tables::mdtables::{enums::*, *},
27};
28
29pub type Result<T> = std::result::Result<T, Error>;
30
31/// A parsed .NET PE file.
32///
33/// `DnPe` borrows the underlying file buffer (`&'a [u8]`) and exposes the
34/// CLR header, metadata streams, tables and CIL method bodies. The caller
35/// owns the buffer; back it with `std::fs::read` or `memmap2::Mmap`.
36///
37/// # Example
38///
39/// ```no_run
40/// let data = std::fs::read("Sample.exe")?;
41/// let pe = dnfile::DnPe::parse(&data)?;
42/// let clr = pe.net()?;
43/// println!("{} functions", clr.functions().len());
44/// # Ok::<(), dnfile::error::Error>(())
45/// ```
46#[derive(Debug, Serialize)]
47pub struct DnPe<'a> {
48    #[serde(skip_serializing)]
49    data: &'a [u8],
50    /// PE section table cached at construction.
51    /// Avoids re-parsing the whole PE on every `offset()` / `get_data()` call,
52    /// which is in the hot path of every metadata-table row and every CIL instruction.
53    #[serde(skip_serializing)]
54    sections: Vec<goblin::pe::section_table::SectionTable>,
55    /// PE optional-header `file_alignment` cached at construction (same reason).
56    #[serde(skip_serializing)]
57    file_alignment: u32,
58    /// CLR resources directory RVA (from `ClrStruct.resources_rva`). Used by
59    /// `resources()` to locate the bytes of each `ManifestResource` entry.
60    #[serde(skip_serializing)]
61    pub(crate) resources_rva: u32,
62    /// CLR resources directory size.
63    #[serde(skip_serializing)]
64    pub(crate) resources_size: u32,
65    net: Option<ClrData<'a>>,
66}
67
68impl<'a> DnPe<'a> {
69    pub fn net(&self) -> Result<&ClrData<'a>> {
70        self.net.as_ref().ok_or(Error::NotImplementedError)
71    }
72
73    /// Re-parse and return a fresh `goblin::pe::PE` view over the file bytes.
74    ///
75    /// Internal hot-path code uses the cached `sections` + `file_alignment`
76    /// fields directly; this method is kept on the public surface for
77    /// consumers that need the full `goblin::pe::PE` value. Calling it is
78    /// linear in file size, so cache the result yourself if you need it
79    /// repeatedly.
80    pub fn pe(&self) -> Result<goblin::pe::PE<'_>> {
81        Ok(goblin::pe::PE::parse(self.data)?)
82    }
83
84    /// Parse a .NET PE from a borrowed byte buffer.
85    ///
86    /// Zero-copy: heaps, streams and method-body readers all hold slices
87    /// into `data` rather than copies.
88    pub fn parse(data: &'a [u8]) -> Result<Self> {
89        // Parse the PE exactly once and extract the bits we'll need on the
90        // hot path. Previously every `offset()` / `get_data()` call re-parsed
91        // the entire binary via `self.pe()`.
92        let (sections, file_alignment, clr_directory) = {
93            let pe = goblin::pe::PE::parse(data)?;
94            let opt_header = pe
95                .header
96                .optional_header
97                .ok_or(Error::UnsupportedBinaryFormat("optional header absence"))?;
98            let file_alignment = opt_header.windows_fields.file_alignment;
99            let clr_directory = opt_header
100                .data_directories
101                .get_clr_runtime_header()
102                .copied()
103                .ok_or(Error::UnsupportedBinaryFormat("ClR runtime header absence"))?;
104            (pe.sections.clone(), file_alignment, clr_directory)
105        };
106
107        let mut res = DnPe {
108            data,
109            sections,
110            file_alignment,
111            resources_rva: 0,
112            resources_size: 0,
113            net: None,
114        };
115        let clr_struct: ClrStruct = res.get_data(
116            &clr_directory.virtual_address,
117            &(clr_directory.size as usize),
118        )?;
119        res.resources_rva = clr_struct.resources_rva;
120        res.resources_size = clr_struct.resources_size;
121        res.net = Some(res.new_clrdata(clr_struct)?);
122        Ok(res)
123    }
124
125    fn offset(&self, rva: u32) -> Result<usize> {
126        goblin::pe::utils::find_offset(
127            rva as usize,
128            &self.sections,
129            self.file_alignment,
130            &goblin::pe::options::ParseOptions::default(),
131        )
132        .ok_or(Error::UnresolvedRvaError(rva))
133    }
134
135    fn get_data<'b, T>(&'b self, rva: &'b u32, size: &'b usize) -> Result<T>
136    where
137        T: scroll::ctx::TryFromCtx<'b, goblin::container::Endian, Error = scroll::Error>,
138    {
139        Ok(goblin::pe::utils::get_data(
140            self.data,
141            &self.sections,
142            goblin::pe::data_directories::DataDirectory {
143                virtual_address: *rva,
144                size: *size as u32,
145            },
146            self.file_alignment,
147        )?)
148    }
149
150    /// Defensive null-terminated string reader. Bounds-checked indexing,
151    /// checked-add on RVA, and a hard cap on length (1 KiB — covers every
152    /// legitimate ECMA-335 use; stream names are at most 31 characters,
153    /// metadata-version strings even less).
154    fn get_nullterminated_string(&self, rva: &u32) -> Result<String> {
155        const MAX_LEN: usize = 1024;
156        let mut res_buf = Vec::with_capacity(32);
157        let mut rrva = *rva;
158        loop {
159            let off = self.offset(rrva)?;
160            let c = *self.data.get(off).ok_or(Error::UnresolvedRvaError(rrva))?;
161            if c == 0 {
162                break;
163            }
164            if res_buf.len() >= MAX_LEN {
165                return Err(Error::UnresolvedRvaError(rrva));
166            }
167            res_buf.push(c);
168            rrva = rrva.checked_add(1).ok_or(Error::UnresolvedRvaError(rrva))?;
169        }
170        Ok(String::from_utf8(res_buf)?)
171    }
172
173    /// Borrowed slice into the file buffer. Replaces the prior `Vec<u8>`-returning
174    /// helper to keep stream/heap construction zero-copy.
175    fn get_slice(&self, rva: &u32, size: usize) -> Result<&'a [u8]> {
176        let offset = self.offset(*rva)?;
177        let end = offset
178            .checked_add(size)
179            .ok_or(Error::UnresolvedRvaError(*rva))?;
180        self.data
181            .get(offset..end)
182            .ok_or(Error::UnresolvedRvaError(*rva))
183    }
184
185    /// Owned variant kept for callers (mdtables) that intentionally take ownership.
186    fn get_vec(&self, rva: &u32, size: &usize) -> Result<Vec<u8>> {
187        Ok(self.get_slice(rva, *size)?.to_vec())
188    }
189
190    fn get_dword_at_rva(&self, rva: &u32) -> Result<u32> {
191        self.get_data(rva, &4)
192    }
193
194    /// Returns the RVA of the CLR resources directory (from the CLR header).
195    /// 0 if the binary has no resources directory.
196    #[must_use]
197    pub fn resources_rva(&self) -> u32 {
198        self.resources_rva
199    }
200
201    /// Returns the size (bytes) of the CLR resources directory.
202    #[must_use]
203    pub fn resources_size(&self) -> u32 {
204        self.resources_size
205    }
206
207    fn new_clrdata(&self, clr_struct: ClrStruct) -> Result<ClrData<'a>> {
208        let metadata_struct: MetaDataStruct = self.get_data(
209            &clr_struct.meta_data_rva,
210            &(clr_struct.meta_data_size as usize),
211        )?;
212        let metadata = self.new_metadata(&clr_struct.meta_data_rva, metadata_struct)?;
213        let flags = ClrHeaderFlags::new(clr_struct.flags as usize);
214        let functions = self.parse_functions(&metadata)?;
215        Ok(ClrData {
216            metadata,
217            flags,
218            functions,
219        })
220    }
221
222    fn parse_functions(
223        &self,
224        metadata: &MetaData<'a>,
225    ) -> Result<Vec<lang::cil::function::Function>> {
226        let mut res = vec![];
227        let method_def_table = metadata.md_table("MethodDef")?;
228        for i in 0..method_def_table.row_count() {
229            let row = method_def_table.row::<MethodDef>(i)?;
230            if !row
231                .impl_flags
232                .contains(&ClrMethodImpl::MethodCodeType(CorMethodCodeType::IL))
233                || row
234                    .flags
235                    .contains(&ClrMethodAttr::AttrFlag(CorMethodAttrFlag::Abstract))
236                || row
237                    .flags
238                    .contains(&ClrMethodAttr::AttrFlag(CorMethodAttrFlag::PinvokeImpl))
239            {
240                continue;
241            }
242            res.push(self.parse_function(row)?);
243        }
244        Ok(res)
245    }
246
247    fn parse_function(&self, row: &MethodDef) -> Result<lang::cil::function::Function> {
248        let mut reader = lang::cil::function::reader::Reader::new(self.data);
249        reader.seek(self.offset(row.rva)?)?;
250        lang::cil::function::Function::new(&mut reader)
251    }
252
253    fn new_metadata(
254        &self,
255        metadata_rva: &u32,
256        metadata_struct: MetaDataStruct,
257    ) -> Result<MetaData<'a>> {
258        // All RVA arithmetic here is over attacker-controlled u32s.
259        let after_signature_rva = metadata_rva
260            .checked_add(16)
261            .ok_or(Error::UnresolvedRvaError(*metadata_rva))?;
262        let version_offset = self.offset(after_signature_rva)?;
263        let version_end = version_offset
264            .checked_add(metadata_struct.version_length as usize)
265            .ok_or(Error::UnresolvedRvaError(after_signature_rva))?;
266        let version = self
267            .data
268            .get(version_offset..version_end)
269            .ok_or(Error::UnresolvedRvaError(after_signature_rva))?
270            .to_vec();
271        let after_version_rva = after_signature_rva
272            .checked_add(metadata_struct.version_length)
273            .ok_or(Error::UnresolvedRvaError(after_signature_rva))?;
274        let flags: u16 = self.get_data(&after_version_rva, &2)?;
275        let after_flags_rva = after_version_rva
276            .checked_add(2)
277            .ok_or(Error::UnresolvedRvaError(after_version_rva))?;
278        let number_of_streams: u16 = self.get_data(&after_flags_rva, &2)?;
279        let streams_table_rva = after_flags_rva
280            .checked_add(2)
281            .ok_or(Error::UnresolvedRvaError(after_flags_rva))?;
282        let streams = if number_of_streams > 0 {
283            self.new_streams(
284                metadata_rva,
285                &streams_table_rva,
286                &(number_of_streams as usize),
287            )?
288        } else {
289            std::collections::HashMap::new()
290        };
291        Ok(MetaData {
292            _version: String::from_utf8(version)?,
293            flags,
294            streams,
295        })
296    }
297
298    fn new_streams(
299        &self,
300        metadata_rva: &u32,
301        streams_table_rva: &u32,
302        number_of_streams: &usize,
303    ) -> Result<std::collections::HashMap<String, stream::ClrStream<'a>>> {
304        // Defensive cap. ECMA-335 has no fixed upper bound but real files
305        // have ≤ ~6 streams; a malformed `number_of_streams` (u16, so ≤ 65535)
306        // could otherwise drive a 65k-iteration loop full of failing parses.
307        const MAX_STREAMS: usize = 64;
308        let n = (*number_of_streams).min(MAX_STREAMS);
309        let mut res = std::collections::HashMap::with_capacity(n);
310        let mut stream_entry_rva = *streams_table_rva;
311        for _i in 0..n {
312            let stream = self.new_clr_stream(&stream_entry_rva, metadata_rva)?;
313            stream_entry_rva = stream_entry_rva
314                .checked_add(stream.stream_table_entry_size as u32)
315                .ok_or(Error::UnresolvedRvaError(stream_entry_rva))?;
316            res.insert(stream.name().to_string(), stream);
317        }
318        let mut rres = std::collections::HashMap::with_capacity(res.len());
319        for (n, s) in &res {
320            rres.insert(n.to_string(), self.parse_clr_stream(s, &res)?);
321        }
322        Ok(rres)
323    }
324
325    fn new_clr_stream(
326        &self,
327        stream_table_entry_rva: &u32,
328        metadata_rva: &u32,
329    ) -> Result<stream::ClrStream<'a>> {
330        let stream_offset: u32 = self.get_data(stream_table_entry_rva, &4)?;
331        let size_rva = stream_table_entry_rva
332            .checked_add(4)
333            .ok_or(Error::UnresolvedRvaError(*stream_table_entry_rva))?;
334        let stream_size: u32 = self.get_data(&size_rva, &4)?;
335        let name_rva = stream_table_entry_rva
336            .checked_add(8)
337            .ok_or(Error::UnresolvedRvaError(*stream_table_entry_rva))?;
338        let stream_name = self.get_nullterminated_string(&name_rva)?;
339        let data_rva = metadata_rva
340            .checked_add(stream_offset)
341            .ok_or(Error::UnresolvedRvaError(*metadata_rva))?;
342        let stream_data = self.get_slice(&data_rva, stream_size as usize)?;
343        self.nnew_clr_stream(
344            metadata_rva,
345            &stream_offset,
346            &(stream_size as usize),
347            &stream_name,
348            stream_data,
349        )
350    }
351}
352
353#[repr(C)]
354#[derive(Deserialize, Debug, scroll::Pread)]
355pub struct ClrStruct {
356    cb: u32,
357    majorr_runtime_version: u16,
358    minor_runtime_version: u16,
359    meta_data_rva: u32,
360    meta_data_size: u32,
361    flags: u32,
362    entry_point_token_or_rva: u32,
363    resources_rva: u32,
364    resources_size: u32,
365    strong_name_signature_rva: u32,
366    strong_name_signature_size: u32,
367    code_manager_table_rva: u32,
368    code_manager_table_size: u32,
369    v_table_fixups_rva: u32,
370    v_table_fixups_size: u32,
371    export_address_table_jumps_rva: u32,
372    export_address_table_jumps_size: u32,
373    managed_native_header_rva: u32,
374    managed_native_header_size: u32,
375}
376
377#[derive(Debug, Serialize, PartialOrd, Ord, PartialEq, Eq)]
378pub enum ClrHeaderFlags {
379    IlOnly,
380    BitRequired32,
381    IlLibrary,
382    StrongNamesSigned,
383    NativeEntryPiont,
384    TrackDebugData,
385    Prefer32Bit,
386}
387
388impl ClrHeaderFlags {
389    pub fn new(value: usize) -> std::collections::BTreeSet<Self> {
390        let mut res = std::collections::BTreeSet::new();
391        if value & 1 != 0 {
392            res.insert(Self::IlOnly);
393        }
394        if value & 2 != 0 {
395            res.insert(Self::BitRequired32);
396        }
397        if value & 4 != 0 {
398            res.insert(Self::IlLibrary);
399        }
400        if value & 8 != 0 {
401            res.insert(Self::StrongNamesSigned);
402        }
403        if value & 0x10 != 0 {
404            res.insert(Self::NativeEntryPiont);
405        }
406        if value & 0x10000 != 0 {
407            res.insert(Self::TrackDebugData);
408        }
409        if value & 0x20000 != 0 {
410            res.insert(Self::Prefer32Bit);
411        }
412        res
413    }
414}
415
416#[derive(Debug, Serialize)]
417pub struct ClrData<'a> {
418    pub metadata: MetaData<'a>,
419    pub flags: std::collections::BTreeSet<ClrHeaderFlags>,
420    pub functions: Vec<lang::cil::function::Function>,
421}
422
423impl<'a> ClrData<'a> {
424    pub fn md_table(
425        &self,
426        name: &'static str,
427    ) -> Result<&stream::meta_data_tables::mdtables::MetaDataTable> {
428        self.metadata.md_table(name)
429    }
430
431    pub fn md_table_by_index(
432        &self,
433        index: &usize,
434    ) -> Result<&stream::meta_data_tables::mdtables::MetaDataTable> {
435        self.metadata.md_table_by_index(index)
436    }
437
438    pub fn resolve_coded_index<T>(
439        &self,
440        index: &dyn stream::meta_data_tables::mdtables::codedindex::CodedIndex,
441    ) -> Result<&T>
442    where
443        T: stream::meta_data_tables::mdtables::MDTableRowTrait + 'static,
444    {
445        let table = self.md_table(index.table())?;
446        // ECMA-335 row indices are 1-based; a 0 indicates "no reference".
447        let idx = index
448            .row_index()
449            .checked_sub(1)
450            .ok_or(Error::UndefinedMetaDataTableName(index.table()))?;
451        table.row(idx)
452    }
453
454    pub fn functions(&self) -> &Vec<lang::cil::function::Function> {
455        &self.functions
456    }
457
458    pub fn get_us(&self, rid: usize) -> Result<String> {
459        self.metadata.get_us(rid)
460    }
461
462    /// Returns the binary's `Assembly` table row 0, if it has one.
463    ///
464    /// A .NET assembly (an `.exe` or a top-level `.dll` that defines an
465    /// assembly identity) always has exactly one row here. Module-only DLLs
466    /// (rare; e.g. `netmodule`s linked into an assembly) have an empty
467    /// `Assembly` table and this returns `Err(UndefinedMetaDataTableName)`.
468    pub fn assembly(&self) -> Result<&stream::meta_data_tables::mdtables::Assembly> {
469        let table = self.md_table("Assembly")?;
470        if table.row_count() == 0 {
471            return Err(Error::UndefinedMetaDataTableName("Assembly"));
472        }
473        table.row::<stream::meta_data_tables::mdtables::Assembly>(0)
474    }
475}
476
477#[repr(C)]
478#[derive(Deserialize, Debug, Clone, scroll::Pread)]
479pub struct MetaDataStruct {
480    signature: u32,
481    major_version: u16,
482    minor_version: u16,
483    reserved: u32,
484    version_length: u32,
485}
486
487#[derive(Debug, Serialize)]
488pub struct MetaData<'a> {
489    #[serde(skip_serializing)]
490    _version: String,
491    flags: u16,
492    pub streams: std::collections::HashMap<String, stream::ClrStream<'a>>,
493}
494
495impl<'a> MetaData<'a> {
496    pub fn md_table(
497        &self,
498        name: &'static str,
499    ) -> Result<&stream::meta_data_tables::mdtables::MetaDataTable> {
500        for s in self.streams.values() {
501            if let stream::Stream::MetaDataTables(mt) = &s.stream {
502                match mt
503                    .tables
504                    .get(&stream::meta_data_tables::mdtables::table_name_2_index(
505                        name,
506                    )?) {
507                    Some(s) => return Ok(s),
508                    None => return Err(Error::UndefinedMetaDataTableName(name)),
509                }
510            }
511        }
512        Err(Error::UndefinedMetaDataTableName(name))
513    }
514
515    pub fn md_table_by_index(
516        &self,
517        index: &usize,
518    ) -> Result<&stream::meta_data_tables::mdtables::MetaDataTable> {
519        for s in self.streams.values() {
520            if let stream::Stream::MetaDataTables(mt) = &s.stream {
521                match mt.tables.get(index) {
522                    Some(s) => return Ok(s),
523                    None => return Err(Error::UndefinedMetaDataTableIndex(*index as u32)),
524                }
525            }
526        }
527        Err(Error::UndefinedMetaDataTableIndex(*index as u32))
528    }
529
530    pub fn get_us(&self, rid: usize) -> Result<String> {
531        for s in self.streams.values() {
532            if let stream::Stream::UserStringHeap(us) = &s.stream {
533                return us.get_us(rid);
534            }
535        }
536        Err(Error::UndefinedMetaDataTableName("US"))
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    #[test]
545    fn clr_header_flags_decodes_bits() {
546        let flags = ClrHeaderFlags::new(0b0000_0001);
547        assert!(flags.contains(&ClrHeaderFlags::IlOnly));
548        assert!(!flags.contains(&ClrHeaderFlags::BitRequired32));
549    }
550
551    #[test]
552    fn clr_header_flags_decodes_all_low_bits() {
553        let flags = ClrHeaderFlags::new(0x1F); // bits 0-4
554        for v in [
555            ClrHeaderFlags::IlOnly,
556            ClrHeaderFlags::BitRequired32,
557            ClrHeaderFlags::IlLibrary,
558            ClrHeaderFlags::StrongNamesSigned,
559            ClrHeaderFlags::NativeEntryPiont,
560        ] {
561            assert!(flags.contains(&v), "missing flag: {v:?}");
562        }
563    }
564
565    #[test]
566    fn clr_header_flags_decodes_high_bits() {
567        let flags = ClrHeaderFlags::new(0x30000); // bits 16 + 17
568        assert!(flags.contains(&ClrHeaderFlags::TrackDebugData));
569        assert!(flags.contains(&ClrHeaderFlags::Prefer32Bit));
570    }
571
572    #[test]
573    fn clr_header_flags_zero_value_is_empty() {
574        let flags = ClrHeaderFlags::new(0);
575        assert!(flags.is_empty());
576    }
577}