Back

/ 13 min read

使用Rust在内存中运行PE格式文件

前言

PE文件的内存加载通常用于恶意软件的隐藏或者壳程序的加压缩执行。本文将使用Rust在x86架构的Windows系统中加载一个PE文件到内存中,并通过节表映射、IAT修复、重定位修复后正常运行。 本次实现所使用的依赖有:

  • windows,微软官方提供的Windows API库。
  • object,提供了一些PE中的数据结构。
  • 可选thiserror,用于自定义错误类型。

步骤

  1. 读取PE文件到内存
  2. 计算映像大小并申请空间
  3. 拷贝PE文件头到内存
  4. 遍历节表,将节表数据映射到内存
  5. 修复IAT表
  6. 修复重定位表
  7. 修复节区属性
  8. 跳转到入口点执行

首先定义我们的函数签名:

pub fn load_exe(exe_path: &str, _args: Option<Vec<&str>>) -> Result<(), LoadPEError>;

LoadPEError是我们自定义的错误类型,用于返回错误信息。

1. 读取PE文件到内存

let data = fs::read(exe_path)?;
#[cfg(target_arch = "x86")]
let file: PeFile32 = pe::PeFile::parse(&data[..])?;
#[cfg(target_arch = "x86_64")]
let file: PeFile64 = pe::PeFile::parse(&data[..])?;

2. 计算映像大小并申请空间

let image_size = file.nt_headers().optional_header.size_of_image.get(endian::LittleEndian);
// 3. 为镜像分配内存
// 分配的内存,后续需要修改内存属性
// 对齐值一定要写0x1000,否则会出现问题
let layout = Layout::from_size_align(image_size as usize, 0x1000)?;
let buf = unsafe { alloc(layout) };
if buf.is_null() {
return Err(LoadPEError::MemoryAllocFailed);
}
dbg!("分配的内存地址: {:?}", buf);

需要注意这里的对齐值,对齐值最好使用PE文件中的节对齐值。在一般情况下,节对齐值都是一个内存页的大小,所以这里方便,直接写定了0x1000

3. 拷贝PE文件头到内存

为了PE文件的正常解析,我们需要原封不动的拷贝PE文件头到内存。从文件的0偏移处开始拷贝,拷贝的大小为可选头中的SizeOfHeaders字段值。

// 进行数据的拷贝。
{
// 拷贝整个头部
let head_size = file.nt_headers().optional_header.size_of_headers.get(endian::LittleEndian);
let head_data = &data[..head_size as usize];
unsafe {
std::ptr::copy_nonoverlapping(head_data.as_ptr(), buf, head_size as usize);
}
}

4. 遍历节表,将节表数据映射到内存

{
// 分块拷贝节区数据
let section_alignment = file.nt_headers().optional_header.section_alignment();
let file_alignment = file.nt_headers().optional_header.file_alignment();
for section in file.sections() {
// 如果节区没有映射到内存中,那么就不需要拷贝
if section.pe_section().size_of_raw_data.get(LittleEndian) == 0 {
continue;
}
// VA对齐值
let va = align_to!(
section.pe_section().virtual_address.get(LittleEndian),
section_alignment
);
// pointer_to_raw_data对齐值
let pointer_to_raw_data = align_to!(
section.pe_section().pointer_to_raw_data.get(LittleEndian),
file_alignment
);
// size_of_raw_data对齐值
let size_of_raw_data = align_to!(
section.pe_section().size_of_raw_data.get(LittleEndian),
file_alignment
);
// 从data[pointer_to_raw_data..pointer_to_raw_data + size_of_raw_data]中拷贝数据到buf[va..va + size_of_raw_data]
let section_data = &data
[pointer_to_raw_data as usize..(pointer_to_raw_data + size_of_raw_data) as usize];
unsafe {
let dst = (buf as usize + va as usize) as *mut u8;
std::ptr::copy(section_data.as_ptr(), dst, size_of_raw_data as usize);
};
}
}

这部分需要注意:

  • size_of_raw_data需要对齐到file_alignment
  • 如果size_of_raw_data为0,那么这个节不需要映射到内存中。
  • VirtualAddress需要对齐到section_alignment

5. 修复IAT表

IAT表的修复需要我们遍历导入表结构,所以我们必须先检查是否有导入表。正常的程序都会存在导入表。

// 修复IAT表
{
// 获取导入表的数据目录
let import_directory = file.data_directory(IMAGE_DIRECTORY_ENTRY_IMPORT);
if import_directory.is_none() {
return Err(LoadPEError::ExecutableWithoutImportTable);
}
let base_import_table_rva = import_directory.unwrap().virtual_address.get(LittleEndian);
let mut import_table_index = 0;
loop {
let descriptor = unsafe {
&*((buf as usize
+ base_import_table_rva as usize
+ import_table_index * std::mem::size_of::<object::pe::ImageImportDescriptor>())
as *const object::pe::ImageImportDescriptor)
};
if descriptor.original_first_thunk.get(LittleEndian) == 0 {
break;
}
let module_name_rva = descriptor.name.get(LittleEndian);
let p_module_name = (buf as usize + module_name_rva as usize) as *const u8;
// 使用LoadLibraryA加载模块
let h_module = unsafe {
LoadLibraryA(PCSTR::from_raw(p_module_name))
.map_err(|err| LoadPEError::LoadLibraryError(err))?
};
if h_module.is_invalid() {
// 这个逻辑应该走不到
unimplemented!("LoadLibraryA failed");
}
// 遍历IAT表,修复IAT表
// 加载每一个方法。使用GetProcAddress,获取地址。
// 然后修复IAT表。如果其中一个环节出现问题,我们应该:释放所有加载的Module,然后返回错误
// 获取Image_thunk_data数组的首地址
let base_original_first_thunk_va =
if descriptor.original_first_thunk.get(LittleEndian) != 0 {
descriptor.original_first_thunk.get(LittleEndian)
} else {
// 如果OriginalFirstThunk不存在,则使用FirstThunk 的值
descriptor.first_thunk.get(LittleEndian)
};
let base_import_address_table_va = descriptor.first_thunk.get(LittleEndian);
let mut index = 0;
loop {
// 1. 从文件的数据中取出一个ImageThunkData
let origin_thunk_data = unsafe {
#[cfg(target_arch = "x86")]
let thunk_data = &*((buf as usize
+ base_original_first_thunk_va as usize
+ index * std::mem::size_of::<object::pe::ImageThunkData32>())
as *const object::pe::ImageThunkData32);
#[cfg(target_arch = "x86_64")]
let thunk_data = &*((buf as usize
+ base_original_first_thunk_va as usize
+ index * std::mem::size_of::<object::pe::ImageThunkData64>())
as *const object::pe::ImageThunkData64);
thunk_data
};
// 2. 判断是否是最后一个ImageThunkData
let origin_thunk_data_value = origin_thunk_data.0.get(LittleEndian) as u64;
if origin_thunk_data_value == 0 {
break;
}
// 如果不是最后一个,现判断是序号,还是函数名
#[cfg(target_arch = "x86")]
let is_ordinal = origin_thunk_data_value & 0x80000000 != 0;
#[cfg(target_arch = "x86_64")]
let is_ordinal = origin_thunk_data_value & 0x8000000000000000 != 0;
// 函数地址指针
let function_address;
if is_ordinal {
// 如果最高位是1,0-15位是序号
let ordinal = origin_thunk_data_value as u16;
let pstr = PCSTR(ordinal as *const u8);
// 使用GetProcAddress获取函数地址
let p_function = unsafe { GetProcAddress(h_module, pstr) };
if p_function.is_none() {
// 如果函数地址是null,表示获取失败
return Err(LoadPEError::GetProcAddressError(format!(
"GetProcAddress failed, ordinal: {:?}",
ordinal
)));
}
function_address = p_function.unwrap() as *const u8;
} else {
// 否则是名称表的RVA
let name_table_rva = origin_thunk_data_value;
let p_function_name = (buf as usize + name_table_rva as usize + 2) as *const u8;
// 构造一个PSTR
let p_function_name = PCSTR(p_function_name);
// 使用GetProcAddress获取函数地址
let p_function = unsafe { GetProcAddress(h_module, p_function_name) };
if p_function.is_none() {
// 如果函数地址是null,表示获取失败
return Err(LoadPEError::GetProcAddressError(format!(
"GetProcAddress failed, function name: {:?}",
p_function_name
)));
}
function_address = p_function.unwrap() as *const u8;
}
// 修复IAT表
// 需要获取到IAT表的地址
let iat_address = (buf as usize
+ base_import_address_table_va as usize
+ std::mem::size_of::<u32>() * index)
as *mut u8;
// 修复IAT表
// 将函数地址写入IAT表 如果是x86_64,需要写入8字节
// 如果是x86,需要写入4字节
unsafe {
#[cfg(target_arch = "x86")]
{
*(iat_address as *mut u32) = function_address as u32;
}
#[cfg(target_arch = "x86_64")]
{
*(iat_address as *mut u64) = function_address as u64;
}
}
index += 1;
}
import_table_index += 1;
}
}

修复IAT时注意:

  • 导入的方式是序号还是函数名。如果最高位是1,那么是序号,否则是函数名。
  • 如果导入表中的ILT表不存在,那么要使用IAT表的值。

6. 修复重定位表

我们运行的程序必须存在重定位表,否则无法正常运行。如果程序不存在重定位表,则它是一个固定基址的程序。而固定基址的程序必须运行在指定的基址处。我们在分配内存的时候无法确保程序需要的基址是可用的,所以我们不支持固定基址的程序。 那么程序就必须存在重定位表,否则我们无法运行。

// 修复重定向表
{
// 1. 获取重定向表的数据目录
let relocation_table_directory = file.data_directory(IMAGE_DIRECTORY_ENTRY_BASERELOC);
if relocation_table_directory.is_none() {
return Err(LoadPEError::NoRelocationTable);
}
let base_relocation_table_rva = relocation_table_directory
.unwrap()
.virtual_address
.get(LittleEndian);
let mut relocation_offset = 0;
let image_base = file
.nt_headers()
.optional_header
.image_base
.get(LittleEndian);
loop {
// 读取一个IMAGE_BASE_RELOCATION
let relocation = unsafe {
&*((buf as usize + base_relocation_table_rva as usize + relocation_offset)
as *const object::pe::ImageBaseRelocation)
};
// 如果SizeOfBlock为0,表示已经到了最后一个IMAGE_BASE_RELOCATION
if relocation.size_of_block.get(LittleEndian) == 0 {
break;
}
// 获取重定位表的VirtualAddress和SizeOfBlock
let virtual_address = relocation.virtual_address.get(LittleEndian);
let size_of_block = relocation.size_of_block.get(LittleEndian);
let base_block = base_relocation_table_rva as usize + relocation_offset + 8;
let block_num = (size_of_block - 8) / 2;
for i in 0..block_num {
// 读取一个TypeOffset
let type_offset = unsafe {
&*((buf as usize
+ base_block as usize
+ i as usize * std::mem::size_of::<u16>())
as *mut u16)
};
// 高四位
let r_type = (type_offset >> 12) as u8;
// 剩余12位
let r_offset = type_offset & 0x0FFF;
// 需要修复的地址偏移
let offset_va = virtual_address + r_offset as u32;
// 若要应用基址重定位,会计算首选基址与在其中实际加载映像的基址之间的差异。 如果在首选基址处加载映像,则差异为零,因此无需应用基址重定位。
// 修复后的值: 按类型
let reloc_offset = buf as usize - image_base as usize;
// 被修复的地址:
let p_address = (buf as usize + offset_va as usize) as *mut u32;
let address_value = unsafe { *p_address };
match r_type {
0 => {}
1 => {
// IMAGE_REL_BASED_HIGH 基址重定位在偏移量处将差值的高 16 位添加到 16 位字段。 16 位字段表示 32 位单词的高值。
// 取reloc_offset的高16位
let high = (reloc_offset >> 16) as u16;
let new_address = address_value + high as u32;
unsafe {
*p_address = new_address;
}
}
2 => {
// IMAGE_REL_BASED_LOW 基准重定位将差值的低 16 位加到偏移的 16 位字段。 16 位字段代表 32 位字的低半部分。
// 取reloc_offset的低16位
let low = reloc_offset as u16;
let new_address = address_value + low as u32;
unsafe {
*p_address = new_address;
}
}
3 => {
// 如果r_type是3,表示这个TypeOffset是一个IMAGE_REL_BASED_HIGHLOW
// 这个时候需要修复一个32位的地址
let new_address = address_value + reloc_offset as u32;
unsafe {
*p_address = new_address;
}
}
10 => {
// IMAGE_REL_BASED_DIR64 基址重定位将差值添加到偏移的 64 位字段。
let new_address = address_value + reloc_offset as u32;
unsafe {
*(p_address as *mut u64) = new_address as u64;
}
}
_ => {
// 其他的类型,暂时不支持
unimplemented!("未实现的重定位类型: {:?}", r_type);
}
}
}
// 计算下一个IMAGE_BASE_RELOCATION的偏移
relocation_offset += size_of_block as usize;
}
}

修复重定位表需要注意:

  • 先通过TypeOffset获取r_typer_offset
  • 根据r_type的不同,修复地址。

7. 修复节区属性

{
let section_alignment = file.nt_headers().optional_header.section_alignment();
// 遍历节区,设置节区属性
for section in file.sections() {
let va = section.pe_section().virtual_address.get(LittleEndian);
// let size_of_raw_data = section.pe_section().size_of_raw_data.get(LittleEndian);
let section_name = section.name().unwrap();
let virtual_size = section.pe_section().virtual_size.get(LittleEndian);
let characteristics = section.pe_section().characteristics.get(LittleEndian);
let section_va = buf as usize + va as usize;
let section_size = align_to!(virtual_size, section_alignment);
// 设置节区属性
let protect;
if characteristics & IMAGE_SCN_MEM_EXECUTE != 0
&& characteristics & IMAGE_SCN_MEM_READ == 0
&& characteristics & IMAGE_SCN_MEM_WRITE == 0
{
protect = PAGE_EXECUTE.0;
} else if characteristics & IMAGE_SCN_MEM_EXECUTE != 0
&& characteristics & IMAGE_SCN_MEM_READ != 0
&& characteristics & IMAGE_SCN_MEM_WRITE == 0
{
protect = PAGE_EXECUTE_READ.0;
} else if characteristics & IMAGE_SCN_MEM_EXECUTE != 0
&& characteristics & IMAGE_SCN_MEM_READ != 0
&& characteristics & IMAGE_SCN_MEM_WRITE != 0
{
protect = PAGE_EXECUTE_READWRITE.0;
} else if characteristics & IMAGE_SCN_MEM_EXECUTE == 0
&& characteristics & IMAGE_SCN_MEM_READ != 0
&& characteristics & IMAGE_SCN_MEM_WRITE == 0
{
protect = PAGE_READONLY.0;
} else if characteristics & IMAGE_SCN_MEM_EXECUTE == 0
&& characteristics & IMAGE_SCN_MEM_READ != 0
&& characteristics & IMAGE_SCN_MEM_WRITE != 0
{
protect = PAGE_READWRITE.0;
} else {
return Err(LoadPEError::ChangeMemoryProtectFailed);
}
// 设置属性
let mut old_protect = PAGE_PROTECTION_FLAGS(0);
println!("{section_name} va: {:x}, section_size: {:x} address: {:x} characteristics: {:x} protect: {:x}", va, section_size, section_va, characteristics, protect);
let result = unsafe {
VirtualProtect(
section_va as *mut _,
section_size as usize,
PAGE_PROTECTION_FLAGS(protect),
&mut old_protect,
)
};
if result.is_err() {
return Err(LoadPEError::ChangeMemoryProtectFailed);
}
}
}

恢复节区属性时需要注意:

  • 根据节区的characteristics字段和设置内存保护属性的值并不是一一对应的,所以需要根据characteristics字段的值来设置内存保护属性。

8. 跳转到入口点执行

{
let entry_point = file
.nt_headers()
.optional_header
.address_of_entry_point
.get(LittleEndian);
let entry_point = buf as usize + entry_point as usize;
let entry_point: extern "system" fn() = unsafe { std::mem::transmute(entry_point) };
dbg!("entry_point: {:p}", entry_point);
entry_point();
}

最后测试

use loadpe_rs::load_exe;
#[cfg(target_arch = "x86_64")]
const EXE_PATH: &str = r#"tests\Testx64.exe"#;
#[cfg(target_arch = "x86")]
const EXE_PATH: &str = r#"tests\Testx32R.exe"#;
fn main() {
match load_exe(EXE_PATH, None) {
Ok(_) => {
println!("Load exe success!");
}
Err(e) => {
eprintln!("Load exe failed: {}", e);
}
}
}

至此一个PE文件在内存中的加载就完成了。