Skip to content

Detection

Forrest edited this page Jan 24, 2019 · 3 revisions

As stated before, Luke Jennings' Countercept post on .NET Gargoyle makes for a good read and provides links to several other useful pages. Jennings' example uses timers to dynamically load and unload a malicious Assembly, assumed to be a call-back based implant. This avoids memory scanning detection by loading their payload for as little time as required before unloading it, chancing that scanners won't be scanning the assembly during that short period. In the referenced article, Jennings' detections seem based around detecting the callback mechanism, over the dynamic loading and executing of the Assembly. A collision between the module being loaded and the scanner scanning is bound to happen, given enough time, though we probably aren't that interested in beaconing implants and can find other ways to beacon.

Related posts by Jennings describe methods for detecting sketchy things through Event Tracing for Windows (ETW). Most screenshots from these articles feature the dotnet-runtime-etw script from Countercept's Github. This script works well and produces results, though mostly when the environment you're loading Assemblies into is PowerShell. I've found results to be not as verbose when loading Assemblies from my supplied example, with few details on what Assemblies were loaded and how/where they were loaded from:

{'EventHeader': {'Size': 266, 'HeaderType': 0, 'Flags': 832, 'EventProperty': 0, 'ThreadId': 14144, 'ProcessId': 1256, 'TimeStamp': 131927454892447564, 'ProviderId': '{3044F61A-99B0-4C21-B203-D39423C73B00}', 'EventDescriptor': {'Id': 0, 'Version': 0, 'Channel': 0, 'Level': 0, 'Opcode': 37, 'Task': 0, 'Keyword': 0}, 'KernelTime': 6, 'UserTime': 12, 'ActivityId': '{00000000-0000-0000-0000-000000000000}'}, 'Task Name': 'CLR METHOD', 'MethodIdentifier': '140724233255664', 'ModuleID': '140724233254424', 'MethodStartAddress': '140724233939280', 'MethodSize': '172', 'MethodToken': '100663326', 'MethodFlags': '8', 'MethodNameSpace': 'AppDomainExample.AssemblySandbox', 'Methodname': 'Load', 'MethodSig': 'instance void (class System.String,unsigned int8[])', 'Description': ''}

{'EventHeader': {'Size': 76, 'HeaderType': 0, 'Flags': 832, 'EventProperty': 0, 'ThreadId': 14144, 'ProcessId': 1256, 'TimeStamp': 131927454892468624, 'ProviderId': '{D00792DA-07B7-40F5-97EB-5D974E054740}', 'EventDescriptor': {'Id': 0, 'Version': 0, 'Channel': 0, 'Level': 0, 'Opcode': 33, 'Task': 0, 'Keyword': 0}, 'KernelTime': 6, 'UserTime': 12, 'ActivityId': '{00000000-0000-0000-0000-000000000000}'}, 'Task Name': 'CLR LOADER', 'ModuleId': '0x7FFCE9FC5F78', 'AssemblyId': '0x0', 'ModuleFlags': '0x0', 'ModuleILPath': '', 'ModuleNativePath': '', 'Description': ''}

In general, I've had a difficult time detecting the call to Assembly.Load() (the above Load method is the one I wrote). I'm not sure if this is because I'm incorrectly filtering the events or that they are more obscured by running in a .NET application, within an AppDomain, instead of initially loading in a PowerShell process. It could also be an issue with pywintrace, using .NET 3, and compiling a 64-bit application. Still working on figuring that out...

If I attach WinDBG to the AppDomainExample, use the SOS extension's DumpDomain, and break after loading the SharpSploit assembly into the second AppDomain, I can clearly see the second AppDomain and loaded SharpSploit assembly:

Domain 2: 000000001c067ff0 LowFrequencyHeap: 000000001c068038 HighFrequencyHeap: 000000001c0680c8 StubHeap: 000000001c068158 Stage: OPEN SecurityDescriptor: 000000001c069590 Name: 0e047281-4af2-44ff-95ef-cfd71bc64222 Assembly: 0000000000f67640 [C:\WINDOWS\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll] ClassLoader: 0000000000f67720 SecurityDescriptor: 0000000000f9ed40 Module Name 00007ffd43af1000 C:\WINDOWS\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll 00007ffce5352568 C:\WINDOWS\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\sortkey.nlp 00007ffce5352020 C:\WINDOWS\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\sorttbls.nlp

Assembly: 000000001c07d2c0 [C:\projects\Visual Studio 2017\Projects\AppDomainExample\AppDomainExample\bin\Debug\AppDomainExample.exe] ClassLoader: 000000001c07d4f0 SecurityDescriptor: 000000001c0195e0 Module Name 00007ffce5443160 C:\projects\Visual Studio 2017\Projects\AppDomainExample\AppDomainExample\bin\Debug\AppDomainExample.exe

Assembly: 000000001c07c960 [C:\WINDOWS\assembly\GAC_MSIL\System\2.0.0.0__b77a5c561934e089\System.dll] ClassLoader: 000000001c07cc30 SecurityDescriptor: 000000001c023a30 Module Name 00007ffce5444188 C:\WINDOWS\assembly\GAC_MSIL\System\2.0.0.0__b77a5c561934e089\System.dll

Assembly: 000000001c2e4200 [SharpSploit, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null] ClassLoader: 000000001c2e42e0 SecurityDescriptor: 000000001c2e4050 Module Name 00007ffce5515ac0 SharpSploit, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Assembly: 000000001c2cd500 [C:\WINDOWS\assembly\GAC_MSIL\System.Core\3.5.0.0__b77a5c561934e089\System.Core.dll] ClassLoader: 000000001c2cd5e0 SecurityDescriptor: 000000001c2cecd0 Module Name 00007ffce5516ad0 C:\WINDOWS\assembly\GAC_MSIL\System.Core\3.5.0.0__b77a5c561934e089\System.Core.dll

We could change the names of the Assemblies and methods to avoid blatantly proclaiming maliciousness. Upon unloading the AppDomain, the information about the assemblies is lost, though still evident (as described in the Gargoyle article):

Domain 2: 000000001c067ff0 LowFrequencyHeap: 000000001c068038 HighFrequencyHeap: 000000001c0680c8 StubHeap: 000000001c068158 Stage: HANDLETABLE_NOACCESS Name: 0e047281-4af2-44ff-95ef-cfd71bc64222 Assembly: 0000000000f67640 [C:\WINDOWS\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll] ClassLoader: 0000000000f67720 SecurityDescriptor: 0000000000f9ed40 Module Name 00007ffd43af1000 C:\WINDOWS\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll 00007ffce5352568 C:\WINDOWS\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\sortkey.nlp 00007ffce5352020 C:\WINDOWS\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\sorttbls.nlp

Assembly: 000000001c07d2c0 [] ClassLoader: 000000001c07d4f0 SecurityDescriptor: 000000001c0195e0 Module Name 00007ffce5443160 Dynamic Module

Assembly: 000000001c07c960 [] ClassLoader: 000000001c07cc30 SecurityDescriptor: 000000001c023a30 Module Name 00007ffce5444188 Dynamic Module

Assembly: 000000001c2e4200 [] ClassLoader: 000000001c2e42e0 SecurityDescriptor: 000000001c2e4050 Module Name 00007ffce5515ac0 Dynamic Module

Assembly: 000000001c2cd500 [] ClassLoader: 000000001c2cd5e0 SecurityDescriptor: 000000001c2cecd0 Module Name 00007ffce5516ad0 Dynamic Module

The outstanding issue with all of the ETW-based solutions is that they require a non-trivial amount of resources to consume and filter desired events. It also doesn't help that this is a reactive approach; not something we can use to prevent sketchy loading. Attaching debuggers to every .NET process and loading SOS probably isn't a solution, either.

Joe Desimone's post for Endgame, "Hunting For In-Memory .NET Attacks" (2017-10-10), is another great source, highlighting how all sides are handling this medium. The proposed method of detection is Endgame's ClrGuard; a tool that hooks the LoadImage() function called by the .NET Assembly.Load() function, logging and/or blocking anything attempting to dynamically load an Assembly. Endgame's CLRGuard is most likely a faster method for detecting and possibly preventing dynamic assembly loading than detection via ETW. However, it may be possible to bypass this detection by explicitly doing what Assembly.Load() does, to avoid the hooks. Or just stick to shellcode. IDK; add it to the TODO.

Clone this wiki locally