use crate::{
cilassembly::writer::{context::WriteContext, output::Output},
file::pe::{constants::COR20_HEADER_SIZE, SectionTable},
utils::align_to,
Error, Result,
};
const METADATA_STREAM_NAMES: [&str; 5] = ["#~", "#Strings", "#US", "#GUID", "#Blob"];
pub fn apply_all_fixups(ctx: &mut WriteContext) -> Result<()> {
let pe_sig_offset = u32::try_from(ctx.pe_signature_offset)
.map_err(|_| Error::LayoutFailed("PE signature offset exceeds u32 range".to_string()))?;
let lfanew_pos = ctx
.dos_header_offset
.checked_add(0x3C)
.ok_or_else(|| Error::LayoutFailed("DOS header offset overflow".to_string()))?;
ctx.write_u32_at(lfanew_pos, pe_sig_offset)?;
fixup_optional_header(ctx)?;
fixup_section_table(ctx)?;
fixup_cor20_header(ctx)?;
fixup_data_directories(ctx)?;
zero_stripped_data_regions(ctx)?;
fixup_coff_characteristics(ctx)?;
fixup_checksum(ctx)?;
Ok(())
}
pub fn fixup_optional_header(ctx: &mut WriteContext) -> Result<()> {
let text_file_size = u32::try_from(align_to(
ctx.text_section_size,
u64::from(ctx.file_alignment),
))
.map_err(|_| Error::LayoutFailed("Text file size exceeds u32 range".to_string()))?;
let mut end_rva: u32 = 0;
for section in &ctx.sections {
if section.removed {
continue;
}
if let (Some(rva), Some(size)) = (section.rva, section.data_size) {
let section_end = rva.saturating_add(size);
if section_end > end_rva {
end_rva = section_end;
}
}
}
let image_size = u32::try_from(align_to(
u64::from(end_rva),
u64::from(ctx.section_alignment),
))
.map_err(|_| Error::LayoutFailed("Image size exceeds u32 range".to_string()))?;
let oh = ctx.optional_header_offset;
let oh_field = |off: u64| -> Result<u64> {
oh.checked_add(off)
.ok_or_else(|| Error::LayoutFailed("Optional header offset overflow".to_string()))
};
ctx.write_u32_at(oh_field(4)?, text_file_size)?;
if let Some(entry_rva) = ctx.native_entry_rva {
ctx.write_u32_at(oh_field(16)?, entry_rva)?;
}
ctx.write_u32_at(oh_field(56)?, image_size)?;
let headers_size = u32::try_from(ctx.text_section_offset)
.map_err(|_| Error::LayoutFailed("Headers size exceeds u32 range".to_string()))?;
ctx.write_u32_at(oh_field(60)?, headers_size)?;
Ok(())
}
pub fn fixup_section_table(ctx: &mut WriteContext) -> Result<()> {
let mut section_headers: Vec<[u8; SectionTable::SIZE]> = Vec::new();
for section in &ctx.sections {
if section.removed {
continue;
}
let (Some(data_offset), Some(rva), Some(data_size)) =
(section.data_offset, section.rva, section.data_size)
else {
continue; };
let file_size = u32::try_from(align_to(
u64::from(data_size),
u64::from(ctx.file_alignment),
))
.map_err(|_| {
Error::LayoutFailed(format!(
"Section {} file size exceeds u32 range",
section.name
))
})?;
let offset_u32 = u32::try_from(data_offset).map_err(|_| {
Error::LayoutFailed(format!("Section {} offset exceeds u32 range", section.name))
})?;
let mut header = [0u8; SectionTable::SIZE];
let name_bytes = section.name.as_bytes();
let copy_len = std::cmp::min(name_bytes.len(), 8);
let name_src = name_bytes.get(..copy_len).ok_or(out_of_bounds_error!())?;
header
.get_mut(..copy_len)
.ok_or(out_of_bounds_error!())?
.copy_from_slice(name_src);
header
.get_mut(8..12)
.ok_or(out_of_bounds_error!())?
.copy_from_slice(&data_size.to_le_bytes());
header
.get_mut(12..16)
.ok_or(out_of_bounds_error!())?
.copy_from_slice(&rva.to_le_bytes());
header
.get_mut(16..20)
.ok_or(out_of_bounds_error!())?
.copy_from_slice(&file_size.to_le_bytes());
header
.get_mut(20..24)
.ok_or(out_of_bounds_error!())?
.copy_from_slice(&offset_u32.to_le_bytes());
header
.get_mut(24..28)
.ok_or(out_of_bounds_error!())?
.copy_from_slice(&0u32.to_le_bytes());
header
.get_mut(28..32)
.ok_or(out_of_bounds_error!())?
.copy_from_slice(&0u32.to_le_bytes());
header
.get_mut(32..34)
.ok_or(out_of_bounds_error!())?
.copy_from_slice(&0u16.to_le_bytes());
header
.get_mut(34..36)
.ok_or(out_of_bounds_error!())?
.copy_from_slice(&0u16.to_le_bytes());
header
.get_mut(36..40)
.ok_or(out_of_bounds_error!())?
.copy_from_slice(§ion.characteristics.to_le_bytes());
section_headers.push(header);
}
let mut offset = ctx.section_table_offset;
for header in §ion_headers {
ctx.write_at(offset, header)?;
offset = offset
.checked_add(SectionTable::SIZE as u64)
.ok_or_else(|| Error::LayoutFailed("Section table offset overflow".to_string()))?;
}
let original_table_size = ctx
.sections
.len()
.checked_mul(SectionTable::SIZE)
.ok_or_else(|| Error::LayoutFailed("Section table size overflow".to_string()))?;
let new_table_size = section_headers
.len()
.checked_mul(SectionTable::SIZE)
.ok_or_else(|| Error::LayoutFailed("Section table size overflow".to_string()))?;
if new_table_size < original_table_size {
let zero_len = original_table_size
.checked_sub(new_table_size)
.ok_or_else(|| Error::LayoutFailed("Section table size underflow".to_string()))?;
let zeros = vec![0u8; zero_len];
ctx.write_at(offset, &zeros)?;
}
let new_count = u16::try_from(section_headers.len()).unwrap_or(0);
let coff_count_pos = ctx
.coff_header_offset
.checked_add(2)
.ok_or_else(|| Error::LayoutFailed("COFF header offset overflow".to_string()))?;
ctx.write_u16_at(coff_count_pos, new_count)?;
Ok(())
}
pub fn fixup_cor20_header(ctx: &mut WriteContext) -> Result<()> {
let metadata_rva = ctx.offset_to_rva(ctx.metadata_offset);
let metadata_size = u32::try_from(ctx.metadata_size)
.map_err(|_| Error::LayoutFailed("Metadata size exceeds u32 range".to_string()))?;
let cor20 = ctx.cor20_header_offset;
let cor20_field = |off: u64| -> Result<u64> {
cor20
.checked_add(off)
.ok_or_else(|| Error::LayoutFailed("COR20 header offset overflow".to_string()))
};
ctx.write_u32_at(cor20_field(8)?, metadata_rva)?; ctx.write_u32_at(cor20_field(12)?, metadata_size)?;
if ctx.entry_point_token != 0 && !ctx.token_remapping.is_empty() {
if let Some(&new_token) = ctx.token_remapping.get(&ctx.entry_point_token) {
ctx.write_u32_at(cor20_field(20)?, new_token)?;
}
}
if ctx.resource_data_size > 0 {
let resource_rva = ctx.offset_to_rva(ctx.resource_data_offset);
let resource_size = u32::try_from(ctx.resource_data_size)
.map_err(|_| Error::LayoutFailed("Resource size exceeds u32 range".to_string()))?;
ctx.write_u32_at(cor20_field(24)?, resource_rva)?; ctx.write_u32_at(cor20_field(28)?, resource_size)?; }
Ok(())
}
pub fn fixup_data_directories(ctx: &mut WriteContext) -> Result<()> {
let dd_offset: u64 = if ctx.is_pe32_plus { 112 } else { 96 };
let dd_base = ctx
.optional_header_offset
.checked_add(dd_offset)
.ok_or_else(|| Error::LayoutFailed("Data directory offset overflow".to_string()))?;
let dd_entry = |index: u64| -> Result<(u64, u64)> {
let rva_off = index
.checked_mul(8)
.and_then(|v| dd_base.checked_add(v))
.ok_or_else(|| {
Error::LayoutFailed("Data directory entry offset overflow".to_string())
})?;
let size_off = rva_off.checked_add(4).ok_or_else(|| {
Error::LayoutFailed("Data directory entry offset overflow".to_string())
})?;
Ok((rva_off, size_off))
};
let has_iat = ctx.iat_size > 0;
let (iat_rva_off, iat_size_off) = dd_entry(12)?;
let (clr_rva_off, clr_size_off) = dd_entry(14)?;
if has_iat {
let iat_rva = ctx.text_section_rva;
let iat_size = u32::try_from(ctx.iat_size).unwrap_or(8);
ctx.write_u32_at(iat_rva_off, iat_rva)?;
ctx.write_u32_at(iat_size_off, iat_size)?;
let clr_rva = ctx
.text_section_rva
.checked_add(iat_size)
.ok_or_else(|| Error::LayoutFailed("CLR header RVA overflow".to_string()))?;
ctx.write_u32_at(clr_rva_off, clr_rva)?;
ctx.write_u32_at(clr_size_off, COR20_HEADER_SIZE)?;
} else {
ctx.write_u32_at(iat_rva_off, 0)?;
ctx.write_u32_at(iat_size_off, 0)?;
let clr_rva = ctx.text_section_rva;
ctx.write_u32_at(clr_rva_off, clr_rva)?;
ctx.write_u32_at(clr_size_off, COR20_HEADER_SIZE)?;
}
let (imp_rva_off, imp_size_off) = dd_entry(1)?;
if let (Some(rva), Some(size)) = (ctx.import_data_rva, ctx.import_data_size) {
ctx.write_u32_at(imp_rva_off, rva)?;
ctx.write_u32_at(imp_size_off, size)?;
} else {
ctx.write_u32_at(imp_rva_off, 0)?;
ctx.write_u32_at(imp_size_off, 0)?;
}
let (exp_rva_off, exp_size_off) = dd_entry(0)?;
if let (Some(rva), Some(size)) = (ctx.export_data_rva, ctx.export_data_size) {
ctx.write_u32_at(exp_rva_off, rva)?;
ctx.write_u32_at(exp_size_off, size)?;
}
let (rsrc_rva_off, rsrc_size_off) = dd_entry(2)?;
let rsrc_section = ctx
.sections
.iter()
.find(|s| s.name.starts_with(".rsrc") && !s.removed);
if let Some(section) = rsrc_section {
if let (Some(rva), Some(size)) = (section.rva, section.data_size) {
ctx.write_u32_at(rsrc_rva_off, rva)?;
ctx.write_u32_at(rsrc_size_off, size)?;
}
} else if ctx.pe_resource_size > 0 {
let rva = ctx.offset_to_rva(ctx.pe_resource_offset);
ctx.write_u32_at(rsrc_rva_off, rva)?;
ctx.write_u32_at(rsrc_size_off, ctx.pe_resource_size)?;
} else {
ctx.write_u32_at(rsrc_rva_off, 0)?;
ctx.write_u32_at(rsrc_size_off, 0)?;
}
let (reloc_rva_off, reloc_size_off) = dd_entry(5)?;
let reloc_section = ctx
.sections
.iter()
.find(|s| s.name.starts_with(".reloc") && !s.removed);
if let Some(section) = reloc_section {
if let (Some(rva), Some(size)) = (section.rva, section.data_size) {
ctx.write_u32_at(reloc_rva_off, rva)?;
ctx.write_u32_at(reloc_size_off, size)?;
} else {
ctx.write_u32_at(reloc_rva_off, 0)?;
ctx.write_u32_at(reloc_size_off, 0)?;
}
} else {
ctx.write_u32_at(reloc_rva_off, 0)?;
ctx.write_u32_at(reloc_size_off, 0)?;
}
Ok(())
}
pub fn fixup_metadata_stream_headers(
ctx: &mut WriteContext,
metadata_root_offset: u64,
stream_headers_offset: u64,
) -> Result<()> {
let mut offset = stream_headers_offset;
let streams = [
(
ctx.tables_stream_offset,
ctx.tables_stream_size,
METADATA_STREAM_NAMES[0],
),
(
ctx.strings_heap_offset,
ctx.strings_heap_size,
METADATA_STREAM_NAMES[1],
),
(
ctx.us_heap_offset,
ctx.us_heap_size,
METADATA_STREAM_NAMES[2],
),
(
ctx.guid_heap_offset,
ctx.guid_heap_size,
METADATA_STREAM_NAMES[3],
),
(
ctx.blob_heap_offset,
ctx.blob_heap_size,
METADATA_STREAM_NAMES[4],
),
];
for (stream_offset, stream_size, name) in &streams {
let rel = stream_offset
.checked_sub(metadata_root_offset)
.ok_or_else(|| Error::LayoutFailed("Stream offset before metadata root".to_string()))?;
let relative_offset = u32::try_from(rel).map_err(|_| {
Error::LayoutFailed("Stream relative offset exceeds u32 range".to_string())
})?;
let aligned_size = u32::try_from(align_to(*stream_size, 4)).map_err(|_| {
Error::LayoutFailed("Stream aligned size exceeds u32 range".to_string())
})?;
ctx.write_u32_at(offset, relative_offset)?;
let size_off = offset
.checked_add(4)
.ok_or_else(|| Error::LayoutFailed("Stream header offset overflow".to_string()))?;
ctx.write_u32_at(size_off, aligned_size)?;
let name_with_null = name
.len()
.checked_add(1)
.ok_or_else(|| Error::LayoutFailed("Stream name length overflow".to_string()))?;
let aligned_name = align_to(name_with_null as u64, 4);
offset = aligned_name
.checked_add(8)
.and_then(|v| offset.checked_add(v))
.ok_or_else(|| Error::LayoutFailed("Stream header offset overflow".to_string()))?;
}
Ok(())
}
pub fn zero_stripped_data_regions(ctx: &mut WriteContext) -> Result<()> {
let _ = ctx.original_debug_dir;
if let Some((cert_offset, cert_size)) = ctx.original_certificate_dir {
let cert_offset_u64 = u64::from(cert_offset);
let cert_end = cert_offset_u64
.checked_add(u64::from(cert_size))
.ok_or_else(|| Error::LayoutFailed("Certificate region offset overflow".to_string()))?;
if cert_end <= ctx.bytes_written {
let zeros = vec![0u8; cert_size as usize];
ctx.write_at(cert_offset_u64, &zeros)?;
}
}
Ok(())
}
pub fn fixup_coff_characteristics(ctx: &mut WriteContext) -> Result<()> {
if ctx.relocs_stripped {
const IMAGE_FILE_RELOCS_STRIPPED: u16 = 0x0001;
let chars_offset = ctx
.coff_header_offset
.checked_add(18)
.ok_or_else(|| Error::LayoutFailed("COFF header offset overflow".to_string()))?;
let current_bytes = ctx.output.as_slice();
#[allow(clippy::cast_possible_truncation)]
let chars_offset_usize = chars_offset as usize;
let chars_end = chars_offset_usize.checked_add(2).ok_or_else(|| {
Error::LayoutFailed("COFF characteristics offset overflow".to_string())
})?;
if chars_end <= current_bytes.len() {
let chars_slice = current_bytes
.get(chars_offset_usize..chars_end)
.ok_or(out_of_bounds_error!())?;
let current = u16::from_le_bytes([
*chars_slice.first().ok_or(out_of_bounds_error!())?,
*chars_slice.get(1).ok_or(out_of_bounds_error!())?,
]);
ctx.write_u16_at(chars_offset, current | IMAGE_FILE_RELOCS_STRIPPED)?;
}
}
Ok(())
}
pub fn fixup_checksum(ctx: &mut WriteContext) -> Result<()> {
let checksum_offset = ctx
.optional_header_offset
.checked_add(64)
.ok_or_else(|| Error::LayoutFailed("Checksum offset overflow".to_string()))?;
let actual_size = usize::try_from(ctx.bytes_written)
.map_err(|_| Error::LayoutFailed("File size exceeds usize range".to_string()))?;
let checksum = calculate_pe_checksum(&ctx.output, checksum_offset, actual_size);
ctx.write_u32_at(checksum_offset, checksum)?;
Ok(())
}
fn calculate_pe_checksum(output: &Output, checksum_offset: u64, actual_size: usize) -> u32 {
let data = output.as_slice();
let file_size = actual_size.min(data.len()); let checksum_offset_usize = usize::try_from(checksum_offset).unwrap_or(usize::MAX);
let checksum_end_usize = checksum_offset_usize.saturating_add(4);
let mut sum: u64 = 0;
let mut i: usize = 0;
while i.saturating_add(1) < file_size {
if i >= checksum_offset_usize && i < checksum_end_usize {
i = i.saturating_add(2);
continue;
}
let word = match data
.get(i..i.saturating_add(2))
.and_then(|s| s.try_into().ok())
{
Some(arr) => u16::from_le_bytes(arr),
None => break,
};
sum = sum.saturating_add(u64::from(word));
i = i.saturating_add(2);
}
if i < file_size && (i < checksum_offset_usize || i >= checksum_end_usize) {
if let Some(&byte) = data.get(i) {
sum = sum.saturating_add(u64::from(byte));
}
}
while sum > 0xFFFF {
sum = (sum & 0xFFFF).saturating_add(sum >> 16);
}
#[allow(clippy::cast_possible_truncation)]
let checksum = (sum as u32).wrapping_add(file_size as u32);
checksum
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cilassembly::writer::generator::PeGenerator;
use crate::cilassembly::CilAssembly;
use crate::CilAssemblyView;
use std::path::Path;
use tempfile::NamedTempFile;
#[test]
fn test_checksum_excludes_checksum_field() {
let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let mut output = Output::create(temp_file.path(), 256).expect("Failed to create output");
let pattern: Vec<u8> = (0..256).map(|i| i as u8).collect();
output.write_at(0, &pattern).expect("Failed to write data");
let checksum1 = calculate_pe_checksum(&output, 64, 256);
output
.write_at(64, &[0xFF, 0xFF, 0xFF, 0xFF])
.expect("Failed to modify checksum area");
let checksum2 = calculate_pe_checksum(&output, 64, 256);
assert_eq!(
checksum1, checksum2,
"Checksum should be the same regardless of checksum field content"
);
}
#[test]
fn test_apply_fixups_integration() {
let view = CilAssemblyView::from_path(Path::new("tests/samples/crafted_2.exe"))
.expect("Failed to load test assembly");
let assembly: CilAssembly = view.to_owned();
let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let generator = PeGenerator::new(&assembly);
generator
.to_file(temp_file.path())
.expect("PE generation should succeed");
let reloaded = CilAssemblyView::from_path(temp_file.path())
.expect("Should be able to reload generated PE");
assert!(
reloaded.tables().is_some(),
"Reloaded PE should have tables"
);
}
}