Skip to main content

dotnetdll/
dll.rs

1use super::{
2    binary::{
3        cli::{Header, Metadata, RVASize},
4        heap::Reader,
5        metadata, method,
6    },
7    resolution::{read, Resolution},
8};
9use object::{
10    endian::{LittleEndian, U32Bytes},
11    pe::{self, ImageDataDirectory},
12    read::{
13        pe::{PeFile32, PeFile64, SectionTable},
14        Error as ObjectReadError, FileKind,
15    },
16};
17use scroll::{Error as ScrollError, Pread};
18use thiserror::Error;
19use DLLError::*;
20
21/// Represents a binary DLL file. Used for binary introspection, metadata resolution, and resolution compilation.
22#[derive(Debug)]
23pub struct DLL<'a> {
24    buffer: &'a [u8],
25    /// The CLI header of the DLL, read from the 15th PE data directory. See ECMA-335, II.25.3.3 (page 283) for more information.
26    pub cli: Header,
27    sections: SectionTable<'a>,
28}
29
30// TODO: now that Resolution is the typical entry point, move this into maybe its own module
31// TODO: also, eventually we need to expand CLI and the ScrollError into our own meaningful variants
32/// The general error type for all dotnetdll operations.
33#[derive(Debug, Error)]
34pub enum DLLError {
35    /// Errors from parsing the PE binary format.
36    /// This might happen if you try to load an invalid DLL with [`DLL::parse`].
37    #[error("PE parsing: {0}")]
38    PERead(#[from] ObjectReadError),
39    /// Errors from CLI metadata reading or writing.
40    /// Messages are communicated through the [`ScrollError::Custom`] enum variant.
41    #[error("CLI metadata: {0}")]
42    CLI(#[from] ScrollError),
43    /// Errors from DLL parsing that are not PE format errors, such as .NET metadata and method bodies.
44    /// This might happen if you try to load an invalid DLL with [`DLL::parse`].
45    #[error("Other parsing: {0}")]
46    Other(&'static str),
47}
48
49pub type Result<T> = std::result::Result<T, DLLError>;
50
51impl<'a> DLL<'a> {
52    /// Parses a binary DLL from a byte slice.
53    ///
54    /// This method only parses the PE (Portable Executable) file structure and the CLI header.
55    /// To resolve the metadata into a high-level representation, use [`DLL::resolve`].
56    pub fn parse(bytes: &'a [u8]) -> Result<DLL<'a>> {
57        let (sections, dir) = match FileKind::parse(bytes)? {
58            FileKind::Pe32 => {
59                let file = PeFile32::parse(bytes)?;
60                (
61                    file.section_table(),
62                    file.data_directory(pe::IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR),
63                )
64            }
65            FileKind::Pe64 => {
66                let file = PeFile64::parse(bytes)?;
67                (
68                    file.section_table(),
69                    file.data_directory(pe::IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR),
70                )
71            }
72            _ => return Err(Other("invalid object type, must be PE32 or PE64")),
73        };
74
75        let cli_b = dir
76            .ok_or(Other("missing CLI metadata data directory in PE image"))?
77            .data(bytes, &sections)?;
78        Ok(DLL {
79            buffer: bytes,
80            cli: cli_b.pread_with(0, scroll::LE)?,
81            sections,
82        })
83    }
84
85    pub fn at_rva(&self, rva: &RVASize) -> Result<&'a [u8]> {
86        let dir = ImageDataDirectory {
87            virtual_address: U32Bytes::new(LittleEndian, rva.rva),
88            size: U32Bytes::new(LittleEndian, rva.size),
89        };
90        dir.data(self.buffer, &self.sections).map_err(PERead)
91    }
92
93    pub(crate) fn raw_rva(&self, rva: u32) -> Result<&'a [u8]> {
94        self.sections
95            .pe_data_at(self.buffer, rva)
96            .ok_or(Other("bad stream offset"))
97    }
98
99    fn get_stream(&self, name: &'static str) -> Result<Option<&'a [u8]>> {
100        let meta = self.get_cli_metadata()?;
101        let Some(header) = meta.stream_headers.iter().find(|h| h.name == name) else {
102            return Ok(None);
103        };
104        let data = self.raw_rva(self.cli.metadata.rva + header.offset)?;
105        Ok(Some(&data[..header.size as usize]))
106    }
107
108    pub fn get_heap<T: Reader<'a>>(&self) -> Result<T> {
109        // heap names from the traits are known to be good
110        // so if we can't find them, assume they are empty
111        Ok(T::new(self.get_stream(T::NAME)?.unwrap_or(&[])))
112    }
113
114    pub fn get_cli_metadata(&self) -> Result<Metadata<'a>> {
115        self.at_rva(&self.cli.metadata)?.pread(0).map_err(CLI)
116    }
117
118    pub fn get_logical_metadata(&self) -> Result<metadata::header::Header> {
119        self.get_stream("#~")?
120            .ok_or(Other("unable to find metadata stream"))?
121            .pread(0)
122            .map_err(CLI)
123    }
124
125    #[allow(clippy::nonminimal_bool)]
126    pub fn get_method(&self, def: &metadata::table::MethodDef) -> Result<method::Method> {
127        let bytes = self.raw_rva(def.rva)?;
128        let mut offset = 0;
129        // if we don't see a method header at the beginning, we need to align
130        if !check_bitmask!(bytes[0], 0x2) {
131            offset = 4 - (def.rva as usize % 4);
132        }
133        bytes.pread(offset).map_err(CLI)
134    }
135
136    /// Resolves the CLI metadata within the DLL into a high-level [`Resolution`] struct.
137    pub fn resolve(&self, opts: read::Options) -> Result<Resolution<'a>> {
138        read::read_impl(self, opts)
139    }
140}