-
-
Notifications
You must be signed in to change notification settings - Fork 7
Detection
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.