Skip to main content

goblin/pe/
utils.rs

1use crate::error::{self};
2use crate::options::Permissive;
3use alloc::string::ToString;
4use alloc::vec::Vec;
5use scroll::Pread;
6
7use super::options;
8use super::section_table;
9
10use crate::pe::data_directories::DataDirectory;
11use core::cmp;
12
13use log::debug;
14
15pub fn is_in_range(rva: usize, r1: usize, r2: usize) -> bool {
16    r1 <= rva && rva < r2
17}
18
19// reference: Peter Ferrie. Reliable algorithm to extract overlay of a PE. https://bit.ly/2vBX2bR
20#[inline]
21fn aligned_pointer_to_raw_data(pointer_to_raw_data: usize) -> usize {
22    const PHYSICAL_ALIGN: usize = 0x1ff;
23    pointer_to_raw_data & !PHYSICAL_ALIGN
24}
25
26#[inline]
27fn section_read_size(section: &section_table::SectionTable, file_alignment: u32) -> usize {
28    fn round_size(size: usize) -> usize {
29        const PAGE_MASK: usize = 0xfff;
30        (size + PAGE_MASK) & !PAGE_MASK
31    }
32
33    // Paraphrased from https://reverseengineering.stackexchange.com/a/4326 (by Peter Ferrie).
34    //
35    // Handles the corner cases such as mis-aligned pointers (round down) and sizes (round up)
36    // Further rounding corner cases:
37    // - the physical pointer should be rounded down to a multiple of 512, regardless of the value in the header
38    // - the read size is rounded up by using a combination of the file alignment and 4kb
39    // - the virtual size is always rounded up to a multiple of 4kb, regardless of the value in the header.
40    //
41    // Reference C implementation:
42    //
43    // long pointerToRaw = section.get(POINTER_TO_RAW_DATA);
44    // long alignedpointerToRaw = pointerToRaw & ~0x1ff;
45    // long sizeOfRaw = section.get(SIZE_OF_RAW_DATA);
46    // long readsize = ((pointerToRaw + sizeOfRaw) + filealign - 1) & ~(filealign - 1)) - alignedpointerToRaw;
47    // readsize = min(readsize, (sizeOfRaw + 0xfff) & ~0xfff);
48    // long virtsize = section.get(VIRTUAL_SIZE);
49    //
50    // if (virtsize)
51    // {
52    //     readsize = min(readsize, (virtsize + 0xfff) & ~0xfff);
53    // }
54
55    let file_alignment = file_alignment as usize;
56    let size_of_raw_data = section.size_of_raw_data as usize;
57    let virtual_size = section.virtual_size as usize;
58    let read_size = {
59        let read_size =
60            ((section.pointer_to_raw_data as usize + size_of_raw_data + file_alignment - 1)
61                & !(file_alignment - 1))
62                - aligned_pointer_to_raw_data(section.pointer_to_raw_data as usize);
63        cmp::min(read_size, round_size(size_of_raw_data))
64    };
65
66    if virtual_size == 0 {
67        read_size
68    } else if read_size == 0 {
69        virtual_size
70    } else {
71        cmp::min(read_size, round_size(virtual_size))
72    }
73}
74
75fn rva2offset(rva: usize, section: &section_table::SectionTable) -> usize {
76    (section.pointer_to_raw_data as usize) + (rva - section.virtual_address as usize)
77}
78
79fn is_in_section(rva: usize, section: &section_table::SectionTable, file_alignment: u32) -> bool {
80    let section_rva = section.virtual_address as usize;
81    is_in_range(
82        rva,
83        section_rva,
84        section_rva + section_read_size(section, file_alignment),
85    )
86}
87
88pub fn find_offset(
89    rva: usize,
90    sections: &[section_table::SectionTable],
91    file_alignment: u32,
92    opts: &options::ParseOptions,
93) -> Option<usize> {
94    if opts.resolve_rva {
95        if file_alignment == 0 || file_alignment & (file_alignment - 1) != 0 {
96            return None;
97        }
98        for (i, section) in sections.iter().enumerate() {
99            debug!(
100                "Checking {} for {:#x} ∈ {:#x}..{:#x}",
101                section.name().unwrap_or(""),
102                rva,
103                section.virtual_address,
104                section.virtual_address + section.virtual_size
105            );
106            if is_in_section(rva, &section, file_alignment) {
107                let offset = rva2offset(rva, &section);
108                debug!(
109                    "Found in section {}({}), remapped into offset {:#x}",
110                    section.name().unwrap_or(""),
111                    i,
112                    offset
113                );
114                return Some(offset);
115            }
116        }
117        None
118    } else {
119        Some(rva)
120    }
121}
122
123pub fn find_offset_or(
124    rva: usize,
125    sections: &[section_table::SectionTable],
126    file_alignment: u32,
127    opts: &options::ParseOptions,
128    msg: &str,
129) -> error::Result<usize> {
130    find_offset(rva, sections, file_alignment, opts)
131        .ok_or_else(|| error::Error::Malformed(msg.to_string()))
132}
133
134pub fn try_name<'a>(
135    bytes: &'a [u8],
136    rva: usize,
137    sections: &[section_table::SectionTable],
138    file_alignment: u32,
139    opts: &options::ParseOptions,
140) -> error::Result<&'a str> {
141    match find_offset(rva, sections, file_alignment, opts) {
142        Some(offset) => Ok(bytes.pread::<&str>(offset)?),
143        None => Err(error::Error::Malformed(format!(
144            "Cannot find name from rva {:#x} in sections: {:?}",
145            rva, sections
146        ))),
147    }
148}
149
150/// Safe version of try_name that handles packed binaries gracefully
151pub(crate) fn safe_try_name<'a>(
152    bytes: &'a [u8],
153    rva: usize,
154    sections: &[section_table::SectionTable],
155    file_alignment: u32,
156    opts: &options::ParseOptions,
157) -> error::Result<Option<&'a str>> {
158    match find_offset(rva, sections, file_alignment, opts) {
159        Some(offset) => {
160            if offset >= bytes.len() {
161                Err(error::Error::Malformed(format!(
162                    "Name RVA {:#x} maps to offset {:#x} beyond file bounds (file size: {:#x}). \
163                    This may indicate a packed binary.",
164                    rva,
165                    offset,
166                    bytes.len()
167                )))
168                .or_permissive_and_default(
169                    opts.parse_mode.is_permissive(),
170                    "Name RVA maps beyond file bounds; treating as missing",
171                )
172            } else {
173                // Try to read the string, but handle potential scroll errors gracefully
174                match bytes.pread::<&str>(offset) {
175                    Ok(name) => Ok(Some(name)),
176                    Err(e) => Err(error::Error::Malformed(format!(
177                        "Failed to read name at offset {:#x} (RVA {:#x}): {}. \
178                        This may indicate a packed binary.",
179                        offset, rva, e
180                    )))
181                    .or_permissive_and_default(
182                        opts.parse_mode.is_permissive(),
183                        "Failed to read name; treating as missing",
184                    ),
185                }
186            }
187        }
188        None => Err(error::Error::Malformed(format!(
189            "Cannot find name from rva {:#x} in sections: {:?}. \
190                This may be a packed binary or malformed sections.",
191            rva, sections
192        )))
193        .or_permissive_and_default(
194            opts.parse_mode.is_permissive(),
195            "Cannot map RVA to name; treating as missing",
196        ),
197    }
198}
199
200pub fn get_data<'a, T>(
201    bytes: &'a [u8],
202    sections: &[section_table::SectionTable],
203    directory: DataDirectory,
204    file_alignment: u32,
205) -> error::Result<T>
206where
207    T: scroll::ctx::TryFromCtx<'a, scroll::Endian, Error = scroll::Error>,
208{
209    get_data_with_opts(
210        bytes,
211        sections,
212        directory,
213        file_alignment,
214        &options::ParseOptions::default(),
215    )
216}
217
218pub fn get_data_with_opts<'a, T>(
219    bytes: &'a [u8],
220    sections: &[section_table::SectionTable],
221    directory: DataDirectory,
222    file_alignment: u32,
223    opts: &options::ParseOptions,
224) -> error::Result<T>
225where
226    T: scroll::ctx::TryFromCtx<'a, scroll::Endian, Error = scroll::Error>,
227{
228    let rva = directory.virtual_address as usize;
229    let offset = find_offset(rva, sections, file_alignment, opts)
230        .ok_or_else(|| error::Error::Malformed(directory.virtual_address.to_string()))?;
231    let result: T = bytes.pread_with(offset, scroll::LE)?;
232    Ok(result)
233}
234
235pub(crate) fn pad(length: usize, alignment: Option<usize>) -> Option<Vec<u8>> {
236    match alignment {
237        Some(alignment) => {
238            let overhang = length % alignment;
239            if overhang != 0 {
240                let repeat = alignment - overhang;
241                Some(vec![0u8; repeat])
242            } else {
243                None
244            }
245        }
246        None => None,
247    }
248}
249
250/// Performs arbitrary alignment of values based on homogeneous numerical types.
251#[inline]
252pub(crate) fn align_up<N>(value: N, align: N) -> N
253where
254    N: core::ops::Add<Output = N>
255        + core::ops::Not<Output = N>
256        + core::ops::BitAnd<Output = N>
257        + core::ops::Sub<Output = N>
258        + core::cmp::PartialEq
259        + core::marker::Copy,
260    u8: Into<N>,
261{
262    debug_assert!(align != 0u8.into(), "Align must be non-zero");
263    (value + align - 1u8.into()) & !(align - 1u8.into())
264}