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.

PEB Structure

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 PEB Structure

From the debugger we can see that:

  • _PEB_LDR_DATA is located at: 0x00007ff94471c4c0
  • RTL_USER_PROCESS_PARAMETERS is located at 0x00000000006627c0

This is however just the start, when inspecting the RTL_USER_PROCESS_PARAMETERS we can see the ImagePathName and the CommandLine: rtl_user_process_parameters

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. ldr_table

We take the first entry of the ldr data table and check it’s FullDllName and BaseDllName, wow look it’s explorer.exe LDR_DATA

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. LDR_DATA We can see the following information:

  • ImagePathName is C:\Windows\notepad.exe
  • CommandLine is C:\Windows\notepad.exe
  • FullDllName is C:\Windows\notepad.exe
  • BaseDllName 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: ldr_table 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 :)

References