Detecting .NET/C# injection (Execute-Assembly)

In a previous article, Detecting Cobalt Strike by Fingerprinting Imageload Events, we discussed how we can potentially identify Cobalt Strike being initially executed on a system by focusing on the common DLLs loaded at runtime.

Additional research into CS led me to some pretty interesting research posts, one by The Wover, MDSec, and MichaelKoczwara which talk about .NET Assembly injection techniques. .NET injection is actually something Microsoft considers a feature and happens quiet a bit within a normal production environment. Turns out there are a few very unique DLLs that will be loaded every time a .NET assembly is loaded. These DLLs are, clr.dll, clrjit.dll, msco*.dll” (ex: mscorlib.dll or mscoree.dll). Focusing on these indicators can help narrow the focus of a threat hunt or incident response activity when its process injection might be suspected. One thing to note is because this happens a lot in production the detection will have to be tuned/customized for your environment to exclude some common processes that you know should be doing this type of behavior. Obviously in a perfect world this would be easy but in reality its difficult so I suggest using this search to either kick off threat hunts or to add context to other alerts (sequenced alerting/risk alarms).

Using some historical Red Team activity in which the RT guys used CS I was able to pick up the odd processes being spawned and injected with their .NET assembly code. Lets get started with the SPL (Search Processing Language — Splunk)

| tstats summariesonly=t dc(sourcetype) as dc_s values(md5) as md5 values(process) as process values(parent_process) as parent_process values(parent_process_id) as parent_process_id values(process_path) as process_path values(parent_process_path) as parent_process_path values(file_name) as file_name values(sourcetype) as sourcetype latest(_time) as ltime dc(file_name) as dc_fn
where index=main
(sourcetype=processes parent_process=* (parent_process!=services.exe process!=*-k* process_name=svchost.exe) NOT (parent_process=userinit.exe AND process_name=explorer.exe))
OR (sourcetype=imageload file_name IN (clr.dll, clrjit.dll, msco*.dll) process_path!=”*.net\\*”)
by process_name,process_id,index,host,user

First off we’re counting the # of distinct sourcetypes because we ONLY want to investigate events were we ALSO have process events/information for the process that loaded the DLLs we’re after. This is optional, but, I’ve found the detection is a little higher fidelity when I can start applying filtering logic to parent process activity. The other distinct count is for the file_name (DLLs) loaded into the process. You can see some initial filtering is applied, this is also optional but these values were causing a lot of FP and unnecessary IO).

| stats values(*) as * by process_name,parent_process,parent_process_path,process_id,parent_process_id,index,dest,user
| rex field=parent_process_path “(?P<filename>[^\\\]+)$”
| where dc_s = 2 AND dc_fn >= 5 AND like(filename,parent_process)

Some readers might notice we’re doing stats again even after the tstats. This is because, in our data, the imageload events do not contain a parent_process so we couldnt group those events together with that field in the tstats portion. Essentially we’re filtering only events where the distinct number of sourcetypes (processes & imageload) is equal to 2 and the distinct number of file_names (DLLs) is greater than or equal to 5.

| eventstats count as total
| eventstats dc(process_id) as dc_childprocid-to-parentprocid by parent_process_id,host
| eventstats dc(host) as uniqueDest_by_process_name by process_name
| eventstats dc(host) as uniqueDest_by_parent_process by parent_process
| eval unique_process = (uniqueDest_by_process_name / total) * 100
| eval unique_parent_process = (uniqueDest_by_parent_process / total) * 100

Next we’re performing some overall statistics on the returned event data.
* Total = the total number of events
* dc_childprocid-to-parentprocid = distinct # of PID to PPID
* uniqueHost_by_process_name =distinct # of systems with that same process name
* uniqueHost_by_parent_process = distinct # of systems with that same parent process
* unique_process = the percent of the total number of systems with the same process name
* unique_parent_process = the percent of the total number of systems with the same parent process

| eval alert_type = if(like(parent_process_path,”%\\users\\%”) OR like(parent_process_path,”%\\programdata\\%”) OR like(parent_process_path,”%\\Program Files%”) OR dc_childprocid-t0-parentprocid > 10 OR unique_parent_process < 1,”hf”,”lf”)

Next we set some conditions to determine if the alert is high fidelity (HF) or low fidelity (LF) based on the parent process path OR whether the parent process had a high # of child processes associated with it OR if the parent process associated with this activity has been observed on less than 1% of systems.

We get some unique findings. In my data we get the two events (above) where svchost.exe is spawned by processes not normally responsible for spawning svchost.exe. Specifically explorer.exe is of interest — I don't know about you but explorer.exe is never running svchost.exe on my system. For those of you not sure — svchost.exe is typically (99.99% of the time) spawned by services.exe and will typically be spawned with -k in the command line.

From here you can customize the filtering to what fits your environment. Happy Hunting!

Cyber Security enthusiast, detection developer and engineer, researcher, consultant.