rustx86-64cross-compilingportable-executable

Wrong slice reference to PE file in manually added section


I'm making a packer in Rust. packer/src/main.rs:

#[link_section = ".pe"]
#[used]
static mut PE: &[u8] = &[];

fn main() {
    unsafe {
        rspe::reflective_loader(common::unpack_to_vec(PE)); // rspe v0.1.2
    }
}

rspe and the (un)packing systems are tested and work. The idea is to compile the packer and manually add a new section containing the packed PE. Then I update the slice in the .pe section. stager/src/main.rs:

use object::{pe::ImageNtHeaders64, read::pe::PeFile, Object as _, ObjectSection as _};

const PACKER_STUB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/packer.exe"));
const PACKED_PE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/pe.pack"));

#[cfg(target_arch = "x86_64")]
pub type IMAGE_NT_HEADER = win_h::IMAGE_NT_HEADERS64;
#[cfg(target_arch = "x86")]
pub type IMAGE_NT_HEADER = win_h::IMAGE_NT_HEADERS32;

// This is for debug purposes, when issue is solved, I'll remove `object` dependency and manually look for sections
fn section_file_range(file: &PeFile<ImageNtHeaders64>, name: &str) -> Option<(u64, u64)> {
    return file.sections().filter(|s| s.name().is_ok()).find_map(|s| {
        if s.name() == Ok(name) {
            s.file_range()
        } else {
            None
        }
    });
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut packer = PACKER_STUB.to_vec();
    let packed_pe = PACKED_PE.to_vec();

    // http://www.sunshine2k.de/reversing/tuts/tut_addsec.htm
    unsafe {
        let ptr = packer.as_mut_ptr();
        let dos_header = ptr as *mut win_h::IMAGE_DOS_HEADER;
        let nt_header =
            (ptr as usize + (*dos_header).e_lfanew as usize) as *mut IMAGE_NT_HEADER;

        let size_of_image = (*nt_header).OptionalHeader.SizeOfImage;
        let number_of_sections = (*nt_header).FileHeader.NumberOfSections;
        let section_alignment = (*nt_header).OptionalHeader.SectionAlignment;
        let file_alignment = (*nt_header).OptionalHeader.FileAlignment;

        // NumberOfSections++
        (*nt_header).FileHeader.NumberOfSections += 1;
        // SizeOfImage++
        (*nt_header).OptionalHeader.SizeOfImage +=
            (packed_pe.len() as f32 / section_alignment as f32).ceil() as u32
                * section_alignment;

        // Section++
        let sections = nt_header.offset(1) as *mut win_h::IMAGE_SECTION_HEADER;
        let last_section = sections.offset(number_of_sections as isize);
        *last_section = win_h::IMAGE_SECTION_HEADER {
            Name: *b".section",
            Misc: win_h::IMAGE_SECTION_HEADER_0 {
                VirtualSize: packed_pe.len() as u32,
            },
            VirtualAddress: size_of_image,
            SizeOfRawData: (packed_pe.len() as f32 / file_alignment as f32).ceil() as u32
                * file_alignment,
            PointerToRawData: packer.len() as u32,
            PointerToRelocations: 0,
            PointerToLinenumbers: 0,
            NumberOfRelocations: 0,
            NumberOfLinenumbers: 0,
            Characteristics: 0x40000040,
        };

        // Append the PE to the packer
        packer.extend(&packed_pe);

        // Set reference to new section
        let packer_pe = PeFile::<ImageNtHeaders64>::parse(&packer)?;
        let (offset, size) = section_file_range(&packer_pe, ".pe").unwrap();
        // I think my problem is here, explanation bellow
        packer[offset as usize..][..size as usize].copy_from_slice(
            &[
                ((*nt_header).OptionalHeader.ImageBase + size_of_image as u64).to_le_bytes(),
                packed_pe.len().to_le_bytes(),
            ]
            .concat(),
        );
    }

    // Write the packed PE
    fs::write("packed.exe", packer)?;
    Ok(())
}

The win_h structures are copied from RSPE's, it's classic Windows PE structures. PACKED_PE contains valid bytes, once unpacked the PE executes correctly. PACKER_STUB contains the compiled packer code of course. I use object v0.36.0 to get the packer's .pe section because I know I get the correct values and don't want to get rid of the dep before the problem is solved to avoid having other issues.

When compiling and executing the packer, nothing happens. I open the packer in PE-bear:

The .pe section is: 00 70 04 40 01 00 00 00 00 50 11 00 00 00 00 00. It looks correct, 0x140000000 is the image base, 0x47000 is the virtual address of my new section and 0x115000 is the size of my packed PE.

For debugging I added the following lines to the packer's code:

fn main() {
    unsafe {
        println!("{}", AGENT.len()); // Debug
        println!("{:?}", AGENT.as_ptr() as *const usize); // Debug
        rspe::reflective_loader(common::unpack_to_vec(PE)); // rspe v0.1.2
    }
}

When executing, I get:

1134592
0x140047000

Looks good to me. I modify the packer's code again, embedding the packed PE directly:

#[link_section = ".pe"]
#[used]
static mut PE: &[u8] = include_bytes!("../../target/x86_64-pc-windows-gnu/release/pe.pack"); // Yes the path is not OUT_DIR but the file is the same

I open the packer in PE-bear, the .pe section is: 14 79 03 40 01 00 00 00 00 50 11 00 00 00 00 00. The PE size is the same, the address 0x37914 points to .rdata section, it looks ok.

I run the packer and OH-OH!

1134592
0x7ff6228c7914 # Wtf?

I think it's the reason the packer didn't work with manually added PE in a section. If I understand correctly, the static mut PE: &[u8] is supposed to reference data in memory (virtual address w/ relocation) instead of physical memory on the PE file. But the memory layout in PE-bear look the same in both case. What am I missing?

I'm on macOS Sonoma 14.5, compile everything with --target x86_64-pc-windows-gnu and test on a Windows 11 VM running on Parallels.

❯ rustc --version

rustc 1.79.0 (129f3b996 2024-06-10)


Solution

  • I needed to add an entry in the .reloc section containing two blocks of WORD size: 0xA000 & 0x0. I had to make sure the section could fit another entry, resize it otherwise. The entries must then be sorted by ascending VA. Not forgetting to update the appropriate size fields.

    unsafe fn resize_reloc(mut bin: Vec<u8>) -> Vec<u8> {
        let base = bin.as_mut_ptr();
        let dos_header = base as *mut win_h::IMAGE_DOS_HEADER;
        let nt_header =
            (base as usize + (*dos_header).e_lfanew as usize) as *mut win_h::IMAGE_NT_HEADER;
        let file_alignment = (*nt_header).OptionalHeader.FileAlignment;
        let sections = nt_header.offset(1) as *mut win_h::IMAGE_SECTION_HEADER;
        let reloc_section = sections.offset(((*nt_header).FileHeader.NumberOfSections - 1) as isize);
    
        // If the .reloc section is too small to receive another entry, make it bigger
        if (*reloc_section).SizeOfRawData - (*reloc_section).Misc.VirtualSize < 0xC {
            (*reloc_section).SizeOfRawData += file_alignment;
            bin.extend(vec![0; file_alignment as usize]);
        }
    
        bin
    }
    
    unsafe fn add_reloc_entry(mut bin: Vec<u8>) -> Vec<u8> {
        let base = bin.as_mut_ptr();
        let dos_header = base as *mut win_h::IMAGE_DOS_HEADER;
        let nt_header =
            (base as usize + (*dos_header).e_lfanew as usize) as *mut win_h::IMAGE_NT_HEADER;
        let sections = nt_header.offset(1) as *mut win_h::IMAGE_SECTION_HEADER;
        let reloc_section_header =
            sections.offset(((*nt_header).FileHeader.NumberOfSections - 2) as isize);
        let pe_section_header = sections.offset(2 as isize); // Better looping through sections instead of hard coding
        let reloc_section = (bin.as_mut_ptr() as *mut u8)
            .offset((*reloc_section_header).PointerToRawData as isize)
            as *mut win_h::IMAGE_BASE_RELOCATION;
    
        // Build the list of the relocations
        let mut relocs = vec![];
        let mut relocation = reloc_section;
        while (*relocation).SizeOfBlock != 0 {
            relocs.push(
                std::slice::from_raw_parts(relocation as *mut u8, (*relocation).SizeOfBlock as usize)
                    .to_vec(),
            );
            relocation = (relocation as *const u8).add((*relocation).SizeOfBlock as usize)
                as *mut win_h::IMAGE_BASE_RELOCATION;
        }
    
        // Add the reloc block for the .pe section
        relocs.push(
            vec![
                (*pe_section_header)
                    .VirtualAddress
                    .to_le_bytes()
                    .as_slice(),
                (0xc as u32).to_le_bytes().as_slice(),
                (0xa000 as u16).to_le_bytes().as_slice(),
                (0x00 as u16).to_le_bytes().as_slice(),
            ]
            .concat(),
        );
    
        // Sort the relocs by VirtualAddress INC
        relocs.sort_by(|a, b| {
            let a = a.as_ptr() as *const win_h::IMAGE_BASE_RELOCATION;
            let b = b.as_ptr() as *const win_h::IMAGE_BASE_RELOCATION;
            (*a).VirtualAddress
                .partial_cmp(&(*b).VirtualAddress)
                .unwrap()
        });
    
        // Copy the newly computed relocations to the .reloc section header
        let relocations_header = relocs.concat();
        std::slice::from_raw_parts_mut(reloc_section as *mut u8, relocations_header.len())
            .copy_from_slice(&relocations_header);
    
        // Adjust .reloc section header sizes
        (*nt_header).OptionalHeader.DataDirectory[5].Size = relocations_header.len() as u32;
        (*reloc_section_header).Misc.VirtualSize = relocations_header.len() as u32;
    
        bin
    }