Skip to content

Commit 69dba8b

Browse files
authored
elf, pe: Add/Extend ELF/PE permissive parsing mode to better handle packed, broken, or malware samples (#479)
Add a new permissive internal trait to allow non-strict parsing of elf and pe binaries. Add a new options module that exposes ParseMode and ParseOptions. ParseOptions and ParseMode implement Default, which defaults to the current strict form of goblin's parsing of binaries. Add two new pub functions that allow users to pass ParseOptions to e.g., turn off strict parsing, etc. Permissive parsing defaults unparseable structures to defaults or optionals in general, and allows the parse of the binary to continue. Some inline git commit messages: * fix: invalid utf8 * fix: wrong reloc directory size * skip tls parsing on error * load binaries with broken/packed sections in permissive mode * skipping basereloc in packed binaries in permisive mode * do not map the debug directory in permissive mode if it has been removed or does not exist * ELF permissive mode + malformed sections handling in permissive mode * skip rich and dos stub parssing on fail in permissive mode * allow ImageDebugDirectory size 0 in permissive mode (packed/stripped binaries) * continue parsing on missing section headers in permissive mode * skip non-utf8 strings in sections and strtab * added more permissive parsing to handle packed/malformed elfs * added persmisive mode for import table * pe: added better error handling with Permissive trait to HintNameTableEntry
1 parent 04d69ad commit 69dba8b

File tree

15 files changed

+904
-155
lines changed

15 files changed

+904
-155
lines changed

src/elf/mod.rs

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ if_sylvan! {
6767
use scroll::{ctx, Pread, Endian};
6868
use crate::strtab::Strtab;
6969
use crate::error;
70+
use crate::options::{Permissive, ParseOptions};
7071
use crate::container::{Container, Ctx};
7172
use alloc::vec::Vec;
7273
use core::cmp;
@@ -260,9 +261,15 @@ if_sylvan! {
260261

261262
/// Parses the contents of the byte stream in `bytes`, and maybe returns a unified binary
262263
pub fn parse(bytes: &'a [u8]) -> error::Result<Self> {
264+
Self::parse_with_opts(bytes, &ParseOptions::default())
265+
}
266+
267+
/// Parses the contents of the byte stream in `bytes` with options, and maybe returns a unified binary
268+
pub fn parse_with_opts(bytes: &'a [u8], opts: &ParseOptions) -> error::Result<Self> {
263269
let header = Self::parse_header(bytes)?;
264270
let misc = parse_misc(&header)?;
265271
let ctx = misc.ctx;
272+
let permissive = opts.parse_mode.is_permissive();
266273

267274
let program_headers = ProgramHeader::parse(bytes, header.e_phoff as usize, header.e_phnum as usize, ctx)?;
268275

@@ -275,7 +282,8 @@ if_sylvan! {
275282
}
276283
}
277284

278-
let section_headers = SectionHeader::parse(bytes, header.e_shoff as usize, header.e_shnum as usize, ctx)?;
285+
let section_headers = SectionHeader::parse(bytes, header.e_shoff as usize, header.e_shnum as usize, ctx)
286+
.or_permissive_and_default(permissive, "Failed to parse section headers")?;
279287

280288
let get_strtab = |section_headers: &[SectionHeader], mut section_idx: usize| {
281289
if section_idx == section_header::SHN_XINDEX as usize {
@@ -290,8 +298,8 @@ if_sylvan! {
290298
Ok(Strtab::default())
291299
} else {
292300
let shdr = &section_headers[section_idx];
293-
shdr.check_size(bytes.len())?;
294-
Strtab::parse(bytes, shdr.sh_offset as usize, shdr.sh_size as usize, 0x0)
301+
shdr.check_size_with_opts(bytes.len(), permissive)?;
302+
Strtab::parse_with_opts(bytes, shdr.sh_offset as usize, shdr.sh_size as usize, 0x0, opts)
295303
}
296304
};
297305

@@ -302,8 +310,27 @@ if_sylvan! {
302310
let mut strtab = Strtab::default();
303311
if let Some(shdr) = section_headers.iter().rfind(|shdr| shdr.sh_type as u32 == section_header::SHT_SYMTAB) {
304312
let size = shdr.sh_entsize;
305-
let count = if size == 0 { 0 } else { shdr.sh_size / size };
306-
syms = Symtab::parse(bytes, shdr.sh_offset as usize, count as usize, ctx)?;
313+
let initial_count = if size == 0 { 0 } else { shdr.sh_size / size };
314+
315+
// Check for extremely large counts that exceed usize capacity
316+
let count = if initial_count > usize::MAX as u64 {
317+
318+
Err(crate::error::Error::Malformed(
319+
format!(
320+
"Symbol table count ({}) from section header exceeds maximum possible value",
321+
initial_count
322+
)
323+
))
324+
.or_permissive_and_then(
325+
permissive,
326+
"Symbol table count exceeds maximum; truncating",
327+
|| usize::MAX as u64,
328+
)?
329+
} else {
330+
initial_count
331+
};
332+
333+
syms = Symtab::parse_with_opts(bytes, shdr.sh_offset as usize, count as usize, ctx, opts)?;
307334
strtab = get_strtab(&section_headers, shdr.sh_link as usize)?;
308335
}
309336

@@ -317,15 +344,17 @@ if_sylvan! {
317344
let mut dynrels = RelocSection::default();
318345
let mut pltrelocs = RelocSection::default();
319346
let mut dynstrtab = Strtab::default();
320-
let dynamic = Dynamic::parse(bytes, &program_headers, ctx)?;
347+
let dynamic = Dynamic::parse(bytes, &program_headers, ctx)
348+
.or_permissive_and_default(permissive, "Failed to parse dynamic section")?;
321349
if let Some(ref dynamic) = dynamic {
322350
let dyn_info = &dynamic.info;
323351

324352
is_pie = dyn_info.flags_1 & dynamic::DF_1_PIE != 0;
325-
dynstrtab = Strtab::parse(bytes,
326-
dyn_info.strtab,
327-
dyn_info.strsz,
328-
0x0)?;
353+
dynstrtab = Strtab::parse_with_opts(bytes,
354+
dyn_info.strtab,
355+
dyn_info.strsz,
356+
0x0, opts)
357+
.or_permissive_and_default(permissive, "Failed to parse dynamic string table")?;
329358

330359
if dyn_info.soname != 0 {
331360
// FIXME: warn! here
@@ -346,15 +375,22 @@ if_sylvan! {
346375
}
347376
}
348377
// parse the dynamic relocations
349-
dynrelas = RelocSection::parse(bytes, dyn_info.rela, dyn_info.relasz, true, ctx)?;
350-
dynrels = RelocSection::parse(bytes, dyn_info.rel, dyn_info.relsz, false, ctx)?;
378+
dynrelas = RelocSection::parse(bytes, dyn_info.rela, dyn_info.relasz, true, ctx)
379+
.or_permissive_and_default(permissive, "Failed to parse dynamic RELA relocations")?;
380+
381+
dynrels = RelocSection::parse(bytes, dyn_info.rel, dyn_info.relsz, false, ctx)
382+
.or_permissive_and_default(permissive, "Failed to parse dynamic REL relocations")?;
383+
351384
let is_rela = dyn_info.pltrel as u64 == dynamic::DT_RELA;
352-
pltrelocs = RelocSection::parse(bytes, dyn_info.jmprel, dyn_info.pltrelsz, is_rela, ctx)?;
385+
pltrelocs = RelocSection::parse(bytes, dyn_info.jmprel, dyn_info.pltrelsz, is_rela, ctx)
386+
.or_permissive_and_default(permissive, "Failed to parse PLT relocations")?;
353387

354388
let mut num_syms = if let Some(gnu_hash) = dyn_info.gnu_hash {
355-
gnu_hash_len(bytes, gnu_hash as usize, ctx)?
389+
gnu_hash_len(bytes, gnu_hash as usize, ctx)
390+
.or_permissive_and_default(permissive, "Failed to parse GNU hash table")?
356391
} else if let Some(hash) = dyn_info.hash {
357-
hash_len(bytes, hash as usize, header.e_machine, ctx)?
392+
hash_len(bytes, hash as usize, header.e_machine, ctx)
393+
.or_permissive_and_default(permissive, "Failed to parse hash table")?
358394
} else {
359395
0
360396
};
@@ -365,22 +401,36 @@ if_sylvan! {
365401
if max_reloc_sym != 0 {
366402
num_syms = cmp::max(num_syms, max_reloc_sym + 1);
367403
}
368-
dynsyms = Symtab::parse(bytes, dyn_info.symtab, num_syms, ctx)?;
404+
dynsyms = Symtab::parse_with_opts(bytes, dyn_info.symtab, num_syms, ctx, opts)?;
369405
}
370406

371407
let mut shdr_relocs = vec![];
372408
for (idx, section) in section_headers.iter().enumerate() {
373409
let is_rela = section.sh_type == section_header::SHT_RELA;
374410
if is_rela || section.sh_type == section_header::SHT_REL {
375-
section.check_size(bytes.len())?;
376-
let sh_relocs = RelocSection::parse(bytes, section.sh_offset as usize, section.sh_size as usize, is_rela, ctx)?;
377-
shdr_relocs.push((idx, sh_relocs));
411+
412+
section.check_size_with_opts(bytes.len(), permissive)?;
413+
let sh_relocs_opt = RelocSection::parse(bytes, section.sh_offset as usize, section.sh_size as usize, is_rela, ctx)
414+
.map(Some)
415+
.or_permissive_and_default(
416+
permissive,
417+
"Failed to parse section relocation; skipping",
418+
)?;
419+
420+
if let Some(sh_relocs) = sh_relocs_opt {
421+
shdr_relocs.push((idx, sh_relocs));
422+
}
378423
}
379424
}
380425

381-
let versym = symver::VersymSection::parse(bytes, &section_headers, ctx)?;
382-
let verdef = symver::VerdefSection::parse(bytes, &section_headers, ctx)?;
383-
let verneed = symver::VerneedSection::parse(bytes, &section_headers, ctx)?;
426+
let versym = symver::VersymSection::parse(bytes, &section_headers, ctx)
427+
.or_permissive_and_default(permissive, "Failed to parse version symbol section")?;
428+
429+
let verdef = symver::VerdefSection::parse(bytes, &section_headers, ctx)
430+
.or_permissive_and_default(permissive, "Failed to parse version definition section")?;
431+
432+
let verneed = symver::VerneedSection::parse(bytes, &section_headers, ctx)
433+
.or_permissive_and_default(permissive, "Failed to parse version need section")?;
384434

385435
let is_lib = misc.is_lib && !is_pie;
386436

src/elf/section_header.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,8 @@ pub mod section_header64 {
370370
///////////////////////////////
371371

372372
if_alloc! {
373-
use crate::error;
373+
use crate::error::{self};
374+
use crate::options::Permissive;
374375
use core::fmt;
375376
use core::result;
376377
use core::ops::Range;
@@ -477,20 +478,26 @@ if_alloc! {
477478
Ok(section_headers)
478479
}
479480
pub fn check_size(&self, size: usize) -> error::Result<()> {
481+
self.check_size_with_opts(size, false)
482+
}
483+
484+
pub(crate) fn check_size_with_opts(&self, size: usize, permissive: bool) -> error::Result<()> {
480485
if self.sh_type == SHT_NOBITS || self.sh_size == 0 {
481486
return Ok(());
482487
}
483488
let (end, overflow) = self.sh_offset.overflowing_add(self.sh_size);
484489
if overflow || end > size as u64 {
485490
let message = format!("Section {} size ({}) + offset ({}) is out of bounds. Overflowed: {}",
486491
self.sh_name, self.sh_offset, self.sh_size, overflow);
487-
return Err(error::Error::Malformed(message));
492+
return Err(error::Error::Malformed(message))
493+
.or_permissive_and_value(permissive, "Malformed section header", ());
488494
}
489495
let (_, overflow) = self.sh_addr.overflowing_add(self.sh_size);
490496
if overflow {
491497
let message = format!("Section {} size ({}) + addr ({}) is out of bounds. Overflowed: {}",
492498
self.sh_name, self.sh_addr, self.sh_size, overflow);
493-
return Err(error::Error::Malformed(message));
499+
return Err(error::Error::Malformed(message))
500+
.or_permissive_and_value(permissive, "Malformed section header", ());
494501
}
495502
Ok(())
496503
}

src/elf/sym.rs

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ pub mod sym64 {
330330
use crate::container::{Container, Ctx};
331331
#[cfg(feature = "alloc")]
332332
use crate::error::Result;
333+
use crate::options::Permissive;
333334
#[cfg(feature = "alloc")]
334335
use alloc::vec::Vec;
335336
use core::fmt;
@@ -515,14 +516,77 @@ if_alloc! {
515516
impl<'a> Symtab<'a> {
516517
/// Parse a table of `count` ELF symbols from `offset`.
517518
pub fn parse(bytes: &'a [u8], offset: usize, count: usize, ctx: Ctx) -> Result<Symtab<'a>> {
518-
let size = count
519+
Self::parse_with_opts(bytes, offset, count, ctx, &crate::options::ParseOptions::default())
520+
}
521+
522+
/// Parse a table of `count` ELF symbols from `offset` with options.
523+
pub(crate) fn parse_with_opts(bytes: &'a [u8], offset: usize, count: usize, ctx: Ctx, opts: &crate::options::ParseOptions) -> Result<Symtab<'a>> {
524+
525+
// Validate offset is within bounds
526+
if offset >= bytes.len() {
527+
return Err(crate::error::Error::Malformed(
528+
format!("Symbol table offset ({}) is beyond file boundary ({})", offset, bytes.len())
529+
)).or_permissive_and_value(
530+
opts.parse_mode.is_permissive(),
531+
"Symbol table offset is beyond file boundary, returning empty symbol table",
532+
Symtab { bytes: &[], count: 0, ctx, start: offset, end: offset }
533+
);
534+
}
535+
536+
// Check for extremely large counts
537+
let mut actual_count = if count > usize::MAX {
538+
Err(crate::error::Error::Malformed(
539+
format!("Symbol count ({}) exceeds maximum possible value", count)
540+
))
541+
.or_permissive_and_then(
542+
opts.parse_mode.is_permissive(),
543+
"Symbol count exceeds maximum; truncating",
544+
|| usize::MAX,
545+
)?
546+
} else {
547+
count
548+
};
549+
550+
let mut requested_size = actual_count
519551
.checked_mul(Sym::size_with(&ctx))
520552
.ok_or_else(|| crate::error::Error::Malformed(
521-
format!("Too many ELF symbols (offset {:#x}, count {})", offset, count)
553+
format!("Too many ELF symbols (offset {:#x}, count {})", offset, actual_count)
522554
))?;
523-
// TODO: make this a better error message when too large
524-
let bytes = bytes.pread_with(offset, size)?;
525-
Ok(Symtab { bytes, count, ctx, start: offset, end: offset+size })
555+
556+
// Check if the requested size extends beyond the file
557+
let available_bytes = bytes.len().saturating_sub(offset);
558+
if requested_size > available_bytes {
559+
let sym_size = Sym::size_with(&ctx);
560+
let adjusted_count = if sym_size > 0 { available_bytes / sym_size } else { 0 };
561+
562+
let (new_size, new_count) = Err(crate::error::Error::Malformed(
563+
format!("Symbol table extends beyond file boundary (requested: {}, available: {})",
564+
requested_size, available_bytes)
565+
))
566+
.or_permissive_and_then(
567+
opts.parse_mode.is_permissive(),
568+
"Symbol table extends beyond file; truncating",
569+
|| (available_bytes, adjusted_count),
570+
)?;
571+
572+
requested_size = new_size;
573+
actual_count = new_count;
574+
}
575+
576+
bytes.pread_with(offset, requested_size)
577+
.map_err(Into::into)
578+
.map(|symbol_bytes| Symtab {
579+
bytes: symbol_bytes,
580+
count: actual_count,
581+
ctx,
582+
start: offset,
583+
end: offset + requested_size
584+
})
585+
.or_permissive_and_value(
586+
opts.parse_mode.is_permissive(),
587+
"Failed to read symbol table data",
588+
Symtab { bytes: &[], count: 0, ctx, start: offset, end: offset },
589+
)
526590
}
527591

528592
/// Try to parse a single symbol from the binary, at `index`.

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ macro_rules! if_alloc {
110110

111111
#[cfg(feature = "alloc")]
112112
pub mod error;
113-
113+
pub mod options;
114114
pub mod strtab;
115115

116116
/// Binary container size information and byte-order context

0 commit comments

Comments
 (0)