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

PE: parse thread local storage - TLS data #404

Merged
merged 2 commits into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/pe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub mod relocation;
pub mod section_table;
pub mod subsystem;
pub mod symbol;
pub mod tls;
pub mod utils;

use crate::container;
Expand Down Expand Up @@ -71,6 +72,8 @@ pub struct PE<'a> {
pub libraries: Vec<&'a str>,
/// Debug information, if any, contained in the PE header
pub debug_data: Option<debug::DebugData<'a>>,
/// TLS information, if any, contained in the PE header
pub tls_data: Option<tls::TlsData<'a>>,
/// Exception handling and stack unwind information, if any, contained in the PE header
pub exception_data: Option<exception::ExceptionData<'a>>,
/// Certificates present, if any, described by the Certificate Table
Expand Down Expand Up @@ -106,6 +109,7 @@ impl<'a> PE<'a> {
let mut import_data = None;
let mut libraries = vec![];
let mut debug_data = None;
let mut tls_data = None;
let mut exception_data = None;
let mut certificates = Default::default();
let mut is_64 = false;
Expand Down Expand Up @@ -216,6 +220,29 @@ impl<'a> PE<'a> {
)?);
}

if let Some(&tls_table) = optional_header.data_directories.get_tls_table() {
tls_data = if is_64 {
tls::TlsData::parse_with_opts::<u64>(
bytes,
image_base,
&tls_table,
kkent030315 marked this conversation as resolved.
Show resolved Hide resolved
&sections,
file_alignment,
opts,
)?
} else {
tls::TlsData::parse_with_opts::<u32>(
bytes,
image_base,
&tls_table,
&sections,
file_alignment,
opts,
)?
};
debug!("tls data: {:#?}", tls_data);
}

if header.coff_header.machine == header::COFF_MACHINE_X86_64 {
// currently only x86_64 is supported
debug!("exception data: {:#?}", exception_data);
Expand Down Expand Up @@ -275,6 +302,7 @@ impl<'a> PE<'a> {
imports,
libraries,
debug_data,
tls_data,
exception_data,
certificates,
})
Expand Down
247 changes: 247 additions & 0 deletions src/pe/tls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
use crate::error;
use alloc::vec::Vec;
use scroll::{Pread, Pwrite, SizeWith};

use crate::pe::data_directories;
use crate::pe::options;
use crate::pe::section_table;
use crate::pe::utils;

/// Represents the TLS directory `IMAGE_TLS_DIRECTORY64`.
#[repr(C)]
#[derive(Debug, PartialEq, Copy, Clone, Default, Pread, Pwrite, SizeWith)]
pub struct ImageTlsDirectory {
/// The starting address of the TLS raw data.
// NOTE: `u32` for 32-bit binaries, `u64` for 64-bit binaries.
pub start_address_of_raw_data: u64,
/// The ending address of the TLS raw data.
// NOTE: `u32` for 32-bit binaries, `u64` for 64-bit binaries.
pub end_address_of_raw_data: u64,
/// The address of the TLS index.
// NOTE: `u32` for 32-bit binaries, `u64` for 64-bit binaries.
pub address_of_index: u64,
/// The address of the TLS callback functions.
///
/// Terminated by a null pointer.
// NOTE: `u32` for 32-bit binaries, `u64` for 64-bit binaries.
pub address_of_callbacks: u64,
/// The size of the zero fill.
pub size_of_zero_fill: u32,
/// The characteristics of the TLS.
pub characteristics: u32,
}

/// TLS information.
#[repr(C)]
kkent030315 marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Debug, Clone, PartialEq, Default)]
pub struct TlsData<'a> {
/// TLS directory.
pub image_tls_directory: ImageTlsDirectory,
/// Raw data of the TLS.
pub raw_data: Option<&'a [u8]>,
/// TLS index.
pub slot: Option<u32>,
/// TLS callbacks.
pub callbacks: Vec<u64>,
}

impl ImageTlsDirectory {
#[allow(unused)]
kkent030315 marked this conversation as resolved.
Show resolved Hide resolved
pub fn parse<T: Sized>(
bytes: &[u8],
dd: data_directories::DataDirectory,
sections: &[section_table::SectionTable],
file_alignment: u32,
) -> error::Result<Self> {
Self::parse_with_opts::<T>(
bytes,
dd,
sections,
file_alignment,
&options::ParseOptions::default(),
)
}

pub fn parse_with_opts<T: Sized>(
bytes: &[u8],
dd: data_directories::DataDirectory,
sections: &[section_table::SectionTable],
file_alignment: u32,
opts: &options::ParseOptions,
) -> error::Result<Self> {
let rva = dd.virtual_address as usize;
let mut offset =
utils::find_offset(rva, sections, file_alignment, opts).ok_or_else(|| {
error::Error::Malformed(format!(
"Cannot map ImageTlsDirectory rva {:#x} into offset",
rva
))
})?;

let is_64 = core::mem::size_of::<T>() == 8;

let start_address_of_raw_data = if is_64 {
bytes.gread_with::<u64>(&mut offset, scroll::LE)?
} else {
bytes.gread_with::<u32>(&mut offset, scroll::LE)? as u64
};
let end_address_of_raw_data = if is_64 {
bytes.gread_with::<u64>(&mut offset, scroll::LE)?
} else {
bytes.gread_with::<u32>(&mut offset, scroll::LE)? as u64
};
let address_of_index = if is_64 {
bytes.gread_with::<u64>(&mut offset, scroll::LE)?
} else {
bytes.gread_with::<u32>(&mut offset, scroll::LE)? as u64
};
let address_of_callbacks = if is_64 {
bytes.gread_with::<u64>(&mut offset, scroll::LE)?
} else {
bytes.gread_with::<u32>(&mut offset, scroll::LE)? as u64
};
let size_of_zero_fill = bytes.gread_with::<u32>(&mut offset, scroll::LE)?;
let characteristics = bytes.gread_with::<u32>(&mut offset, scroll::LE)?;

let itd = Self {
start_address_of_raw_data,
end_address_of_raw_data,
address_of_index,
address_of_callbacks,
size_of_zero_fill,
characteristics,
};

Ok(itd)
}
}

impl<'a> TlsData<'a> {
pub fn parse<T: Sized>(
bytes: &'a [u8],
image_base: usize,
dd: &data_directories::DataDirectory,
sections: &[section_table::SectionTable],
file_alignment: u32,
) -> error::Result<Option<Self>> {
Self::parse_with_opts::<T>(
bytes,
image_base,
dd,
sections,
file_alignment,
&options::ParseOptions::default(),
)
}

pub fn parse_with_opts<T: Sized>(
bytes: &'a [u8],
image_base: usize,
dd: &data_directories::DataDirectory,
sections: &[section_table::SectionTable],
file_alignment: u32,
opts: &options::ParseOptions,
) -> error::Result<Option<Self>> {
let mut raw_data = None;
let mut slot = None;
let mut callbacks = Vec::new();

let is_64 = core::mem::size_of::<T>() == 8;

let itd =
ImageTlsDirectory::parse_with_opts::<T>(bytes, *dd, sections, file_alignment, opts)?;

// Parse the raw data if any
if itd.end_address_of_raw_data != 0 && itd.start_address_of_raw_data != 0 {
if itd.start_address_of_raw_data > itd.end_address_of_raw_data {
return Err(error::Error::Malformed(format!(
"tls start_address_of_raw_data ({:#x}) is greater than end_address_of_raw_data ({:#x})",
itd.start_address_of_raw_data,
itd.end_address_of_raw_data
)));
}

if (itd.start_address_of_raw_data as usize) < image_base {
return Err(error::Error::Malformed(format!(
"tls start_address_of_raw_data ({:#x}) is less than image base ({:#x})",
itd.start_address_of_raw_data, image_base
)));
}

// VA to RVA
let rva = itd.start_address_of_raw_data as usize - image_base;
let size = itd.end_address_of_raw_data - itd.start_address_of_raw_data;
let offset =
utils::find_offset(rva, sections, file_alignment, opts).ok_or_else(|| {
error::Error::Malformed(format!(
"cannot map tls start_address_of_raw_data rva ({:#x}) into offset",
rva
))
})?;
raw_data = Some(&bytes[offset..offset + size as usize]);
}

// Parse the index if any
if itd.address_of_index != 0 {
if (itd.address_of_index as usize) < image_base {
return Err(error::Error::Malformed(format!(
"tls address_of_index ({:#x}) is less than image base ({:#x})",
itd.address_of_index, image_base
)));
}

// VA to RVA
let rva = itd.address_of_index as usize - image_base;
let offset =
utils::find_offset(rva, sections, file_alignment, opts).ok_or_else(|| {
error::Error::Malformed(format!(
"cannot map tls address_of_index rva ({:#x}) into offset",
rva
))
})?;

slot = Some(bytes.pread_with::<u32>(offset, scroll::LE)?);
}

// Parse the callbacks if any
if itd.address_of_callbacks != 0 {
if (itd.address_of_callbacks as usize) < image_base {
return Err(error::Error::Malformed(format!(
"tls address_of_callbacks ({:#x}) is less than image base ({:#x})",
itd.address_of_callbacks, image_base
)));
}

// VA to RVA
let rva = itd.address_of_callbacks as usize - image_base;
let offset =
utils::find_offset(rva, sections, file_alignment, opts).ok_or_else(|| {
error::Error::Malformed(format!(
"cannot map tls address_of_callbacks rva ({:#x}) into offset",
rva
))
})?;
let mut i = 0;
// Read the callbacks until we find a null terminator
loop {
Copy link
Owner

Choose a reason for hiding this comment

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

on a malformed binary could this cause an infinite loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I think it would make sense to have a RVA validation there and return the malformed error if invalid RVAs found.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Owner

Choose a reason for hiding this comment

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

so what i meant specifically is: is this loop guaranteed to terminate? this comes up when there are malformed binaries, and the loop only terminates in a well-formed case, but if the loop has no upper bound, it might cause the library to run forever, which is unacceptable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The RVA check I previously introduced should be sufficient to address the malformations. In general, it does not result in an infinite loop. However, there might be instances where it could raise slice bound errors. Typically, the TLS callback array is located within the data sections—a common characteristic across multiple PECOFF compilers—and the data section is well-aligned with zeros. In my experience handling over 10,000 to 20,000 unique PECOFF binaries, I have not encountered any infinite loops with this implementation. Nonetheless, I believe setting a reasonable limit would be prudent. Is this the solution you were seeking?

Copy link
Owner

Choose a reason for hiding this comment

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

Yea I was just making sure there wasn't some obvious termination condition we could add, e.g., end of file, etc., that would guarantee we are done looping; my general reflex when i see loop is to ask how it can terminate, as we've had bugs in past of infinite loops without proper termination condition, but your answer is sufficient, thank you!

let callback: u64 = if is_64 {
bytes.pread_with::<u64>(offset + i * 8, scroll::LE)?
} else {
bytes.pread_with::<u32>(offset + i * 4, scroll::LE)? as u64
};
if callback == 0 {
break;
}
callbacks.push(callback);
i += 1;
}
}

Ok(Some(TlsData {
image_tls_directory: itd,
raw_data,
slot,
callbacks,
}))
}
}