Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix fail on malformed certificate table parsing #417

Merged
merged 7 commits into from
Jan 5, 2025

Conversation

ideeockus
Copy link
Contributor

@ideeockus ideeockus commented Jul 29, 2024

Hello!

In some PE files Certificate Table can be malformed / contain invalid data. But if we use ParseOptions::parse_attribute_certificates = true, then whole parsing is failed.

I suggest use default CertificateDirectoryTable in case of error.

@ideeockus ideeockus marked this pull request as draft July 29, 2024 13:43
@ideeockus ideeockus force-pushed the fix/fail_on_malformed_cert_table_parsing branch from 2642c9f to 2c7f7b5 Compare July 30, 2024 07:11
@ideeockus ideeockus marked this pull request as ready for review July 30, 2024 07:12
Copy link
Owner

@m4b m4b left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need some clarification on default certificate directory; also formatting things are odd

src/pe/mod.rs Outdated
use alloc::borrow::Cow;
use alloc::string::String;
use alloc::vec::Vec;
use core::cmp::max;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised by these import changes, did the formatter do this?

src/pe/mod.rs Outdated
@@ -142,7 +140,7 @@ impl<'a> PE<'a> {
return Err(error::Error::Malformed(format!(
"Unsupported header magic ({:#x})",
magic
)))
)));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto here too, this is also surprising to me

src/pe/mod.rs Outdated
)
.unwrap_or_else(|err| {
warn!("Cannot parse CertificateTable: {:?}", err);
Default::default()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't remember, what is a default certificate directory in this case? is it going to cause other problems further down the line when parsing, or if a user accesses parts of it, will it panic? Does it have offsets into other parts of the PE file that are no longer valid, etc.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default is empty table (no certificates), so no wrong offsets there

@m4b
Copy link
Owner

m4b commented Aug 26, 2024

So while this is an easy merge, I'm on the fence about whether we should; in general the malformed binary is kind of important to know, and in general, we choose to fail in those cases. However, sometimes we don't, and maybe this is one of those times, but it feels like it's just sort of skipping a malformed thing, and putting a default value in its place, which may be ok, but it also might not be.

So I'd like to understand more about:

  1. why binaries have malformed certificates, is it common
  2. is it not a big deal if they're malformed, e.g., it's safe to just supply a dummy/default/empty version of the table when bad ones are encountered, and continue going on?

thanks for your patience!

@ideeockus
Copy link
Contributor Author

in general the malformed binary is kind of important to know, and in general, we choose to fail in those cases. However, sometimes we don't, and maybe this is one of those times, but it feels like it's just sort of skipping a malformed thing

OK, that sounds reasonable.

What if we add something like ParseMode::Strict and ParseMode::Permissive to ParseOptions?

@ideeockus ideeockus force-pushed the fix/fail_on_malformed_cert_table_parsing branch from b94d9a3 to ac97d4c Compare December 8, 2024 13:20
@ideeockus
Copy link
Contributor Author

@m4b can you review please

I think handling malformations may be related to #401

@@ -8,6 +8,16 @@ pub struct ParseOptions {
/// memory](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#other-contents-of-the-file).
/// For on-disk representations, leave as true. Default: true
pub parse_attribute_certificates: bool,
/// Whether or not to end with an error in case of incorrect data or continue parsing if able. Default: ParseMode::Strict
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debating whether we should add #[non_exhaustive] to this struct to make it future compatible if we need to add another field like this in the future (and not break people)

Copy link
Owner

@m4b m4b left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks good to me, except as mentioned, but since we're here, i'd like to see the:

  1. non_exhaustive attrib added to ParseOptions
  2. it actually implement Default trait
  3. add fn te() -> Self method for constructing the parse options that TE requires

@@ -16,6 +26,7 @@ impl ParseOptions {
ParseOptions {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also i don't know who added this but this does not implement Default, but has a method named that instead; this should be fixed to actually implement Default

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for review, just pushed fixes

@@ -16,6 +26,7 @@ impl ParseOptions {
ParseOptions {
resolve_rva: true,
parse_attribute_certificates: true,
parse_mode: ParseMode::Strict,
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you also add a method that returns what the TE parser uses; we can call it fn te() or something like that, instead of manually constructing it in TE portion.


impl ParseOptions {
pub fn te() -> Self {
Self {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more idiomatic would be something like:

Self {
  resolve_rva: false,
  parse_attribute_certificates: false,
  .. Self::default()
}

this way if new methods are added don't (maybe) need to update this location in source, but it's fine. I'm also wondering if we should make this non pub, just to reduce the api surface for now, i can fix this up though if you don't feel like pushing again.

Copy link
Owner

@m4b m4b left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is great, thank you!

fix private
@m4b m4b merged commit 50fa096 into m4b:master Jan 5, 2025
6 checks passed
@m4b
Copy link
Owner

m4b commented Jan 5, 2025

NOTE: breaking change #434

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants