Detecting Cobalt Strike by Fingerprinting Imageload Events

redhead0ntherun
6 min readOct 12, 2020

--

UPDATE — Check out a related detection technique to find execute-assembly activity https://link.medium.com/MXY9ntLs3db

While 2020 has been pretty miserable for many people one small silver lining is this year I’ve been fortunate enough to engage in multiple Purple Team exercises affording me the opportunity to observe some pretty interesting TTP courtesy our Adversary Emulation team. Quick background…

Background

Cobalt Strike, for those of you living under a rock, is a commercial penetration testing platform, developed by Raphael Mudge, used by many of today’s elite Red Teams and, unfortunately, nation state and criminal threat actors. For about $3,500 a bad guy gets access to a very advanced post-exploitation tool. Its basically Metasploit except 100x better — hence why you pay for it. A lot of criminals and APT groups have cracked this software and run pirated versions to infiltrate networks. In my opinion, the greatest strength of Cobalt Strike (henceforth CS) is that it gives attackers the ability to perform most of their operations within memory and/or using native Windows API calls. According to MITRE’s ATT&CK matrix, at least 9 APT groups including APT32, FIN6, and the Cobalt Group utilize this tool in their arsenal.

Back to the detection…

Since CS has the ability to perform most of its activity within memory space organizations either need to have an advanced EDR solution that can monitor process memory for malicious activity or detect the beacon executable’s actions on the endpoint. For example, the CS beacon will spawn a sacrificial process then hallow out its memory and inject whatever malicious code it needs to execute into that process. By default, CS will spawn rundll32.exe, however, this is easily customized to be whatever. But, at the end of the day, the beacon has to execute at least 1 time on disk. When it does, it will load specific DLLs and will reach back out to its C2 servers to establish a connection. This behavior is what we want to zero in on.

Loaded DLLS

To figure this out I will take a peek at a subset of the CS beacons launched by our Red Team during the last few operations where 7 different beacons were utilized.

| tstats summariesonly=t dc(dest) as dc_dest dc(file_name) as dc_file where index=main
AND process_name IN (AdobeARMTaskConfig.exe,7z-config.exe,AdobeARMTask.exe,spinkitX64.exe,vccmd.exe,DeviceUpdates.exe,SSMConfig.exe,”Terms and Conditions.exe”,bmicalc.exe)
by process_name,process_id
| where isnotnull(process_name)
| sort — dc_dest dc_file
| stats values(process_name) dc(process_name) by dc_file
# of DLLs Loaded by Process Name

We can see that the beacons all share a similar number of DLLs that are loaded when they are executed (~60). Now we need to figure out which DLLs they have in common.

To do that we’ll filter by file_name (which in this dataset is the name of the DLL loaded into the process).

| tstats summariesonly=t dc(dest) as dc_dest where index=main sourcetype=imageload 
AND process_name IN (AdobeARMTaskConfig.exe,7z-config.exe,AdobeARMTask.exe,spinkitX64.exe,vccmd.exe,DeviceUpdates.exe,SSMConfig.exe,”Terms and Conditions.exe”,bmicalc.exe)
by process_name,process_id,file_name,event_time
| stats first(event_time) as event_time by process_name,file_name
| stats values(process_name) as process_name dc(process_name) as dc_proc by file_name
| where dc_proc = 9
| stats values(file_name) as file_name

This provides us with 56 unique DLLs loaded by CS beacons when they are first launched.

“FWPUCLNT.DLL”,”IPHLPAPI.DLL”,”KernelBase.dll”,”NapiNSP.dll”,”OnDemandConnRouteHelper.dll”,”SHCore.dll”,”advapi32.dll”,”bcrypt.dll”,”bcryptprimitives.dll”,”cfgmgr32.dll”,”combase.dll”,”crypt32.dll”,”cryptbase.dll”,”cryptsp.dll”,”dnsapi.dll”,”dpapi.dll”,”gdi32.dll”,”gdi32full.dll”,”iertutil.dll”,”kernel.appcore.dll”,”kernel32.dll”,”msasn1.dll”,”mskeyprotect.dll”,”msvcp_win.dll”,”msvcrt.dll”,”mswsock.dll”,”ncrypt.dll”,”ncryptsslp.dll”,”nlaapi.dll”,”nsi.dll”,”ntasn1.dll”,”ntdll.dll”,”oleaut32.dll”,”pnrpnsp.dll”,”powrprof.dll”,”profapi.dll”,”rasadhlp.dll”,”rpcrt4.dll”,”rsaenh.dll”,”schannel.dll”,”sechost.dll”,”shlwapi.dll”,”sspicli.dll”,”ucrtbase.dll”,”urlmon.dll”,”user32.dll”,”win32u.dll”,”windows.storage.dll”,”winhttp.dll”,”wininet.dll”,”winmm.dll”,”winmmbase.dll”,”winnsi.dll”,”winrnr.dll”,”wintrust.dll”,”ws2_32.dll”

We can test that really quick to make sure we’re detecting the beacons we know about.

| tstats summariesonly=t dc(file_name) as dc_file where index=main sourcetype=imageload 
AND process_name IN (AdobeARMTaskConfig.exe,7z-config.exe,AdobeARMTask.exe,spinkitX64.exe,vccmd.exe,DeviceUpdates.exe,SSMConfig.exe,”Terms and Conditions.exe”,bmicalc.exe)
AND file_name IN (FWPUCLNT.DLL,IPHLPAPI.DLL,KernelBase.dll,NapiNSP.dll,OnDemandConnRouteHelper.dll,SHCore.dll,advapi32.dll,bcrypt.dll,bcryptprimitives.dll,cfgmgr32.dll,combase.dll,crypt32.dll,cryptbase.dll,cryptsp.dll,dnsapi.dll,dpapi.dll,gdi32.dll,gdi32full.dll,iertutil.dll,kernel.appcore.dll,kernel32.dll,msasn1.dll,mskeyprotect.dll,msvcp_win.dll,msvcrt.dll,mswsock.dll,ncrypt.dll,ncryptsslp.dll,nlaapi.dll,nsi.dll,ntasn1.dll,ntdll.dll,oleaut32.dll,pnrpnsp.dll,powrprof.dll,profapi.dll,rasadhlp.dll,rpcrt4.dll,rsaenh.dll,schannel.dll,sechost.dll,shlwapi.dll,sspicli.dll,ucrtbase.dll,urlmon.dll,user32.dll,win32u.dll,windows.storage.dll,winhttp.dll,wininet.dll,winmm.dll,winmmbase.dll,winnsi.dll,winrnr.dll,wintrust.dll,ws2_32.dll)
by process_name,process_id
| where dc_file = 56

It works!

Now, these DLLs are loaded by many other processes — so we need to add some additional filters to ensure we’re capturing ONLY processes that have network traffic associated with the process.

When it comes to statistical analysis/detection I’ve found using historical data very helpful in weeding out common processes within one’s environment. If we combine that methodology with the assumption that bad guys want to be stealthy and wont blast 100s of phishing emails with links to download their beacons we can filter processes we’re not interested in by looking for processes that show up rarely within an environment. How do we accomplish this? In Splunk — using distinct count via stats…

| where dc_dest <= 3 AND dc_file >= 54 AND dc_md5 = 1

The above where filter takes results and looks for processes on 3 or less hosts with the same md5 loading at least 54 of the 56 DLLs we identified previously.

The next step in filtering out noisy non-CS beacon processes is to only worry about processes making network connections. There’s a few ways to accomplish this (via join/subsearches), but, I’m always a fan of optimizing our SPL so I’m going to use tstats and common fields to get all the processes loading these DLLs using the above where filter as well. This is how that looks…

| tstats summariesonly=t count latest(process_start_time) as process_start_time latest(parent_process_path) as parent_process_path latest(parent_process) as parent_process values(file_path) as file_path latest(process) as process latest(process_id) as process_id values(user) as user last(event_time) as ftime first(event_time) as ltime values(dest) as dest dc(dest) as dc_dest dc(http_user_agent) as dc_ua values(http_user_agent) as http_user_agent earliest(_time) as e_acq_time values(uri) as uri values(http_method) as http_method values(http_host) as http_host dc(http_host) as dc_http_host values(md5) as md5 dc(md5) as dc_md5 dc(file_name) as dc_file values(protocol) as protocol dc(dest_ip) as dc_dest_ip values(dest_ip) as dest_ip values(dest_port) as dest_port dc(sourcetype) as dc_sourcetype values(sourcetype) as sourcetype values(process_path) as process_path
where
((index=main sourcetype=imageload
file_name IN (FWPUCLNT.DLL,IPHLPAPI.DLL,KernelBase.dll,NapiNSP.dll,OnDemandConnRouteHelper.dll,SHCore.dll,advapi32.dll,bcrypt.dll,bcryptprimitives.dll,cfgmgr32.dll,combase.dll,crypt32.dll,cryptbase.dll,cryptsp.dll,dnsapi.dll,dpapi.dll,gdi32.dll,gdi32full.dll,iertutil.dll,kernel.appcore.dll,kernel32.dll,msasn1.dll,mskeyprotect.dll,msvcp_win.dll,msvcrt.dll,mswsock.dll,ncrypt.dll,ncryptsslp.dll,nlaapi.dll,nsi.dll,ntasn1.dll,ntdll.dll,oleaut32.dll,pnrpnsp.dll,powrprof.dll,profapi.dll,rasadhlp.dll,rpcrt4.dll,rsaenh.dll,schannel.dll,sechost.dll,shlwapi.dll,sspicli.dll,ucrtbase.dll,urlmon.dll,user32.dll,win32u.dll,windows.storage.dll,winhttp.dll,wininet.dll,winmm.dll,winmmbase.dll,winnsi.dll,winrnr.dll,wintrust.dll,ws2_32.dll)
process_path IN (“C:\\Program*”,”C:\\Users\\*”,”C:\\Windows\\*”))
OR (((index=main sourcetype=processes event_type=start earliest=-30d) OR (index=main sourcetype=url http_host=* NOT http_host IN (“crl.*.com”,”ocsp.*.com”))
OR (index=main sourcetype=tcp))
AND file_path IN (“C:\\Program*”,”C:\\Users\\*”,”C:\\Windows\\*”))
by process_name,index
| where if(isnull(http_host),(dc_sourcetype >= 3 AND dc_dest <= 3 AND dc_file >= 54 AND dc_md5 = 1 AND md5_len > 1),(dc_sourcetype >= 3 AND dc_dest <= 3 AND dc_file >= 54 AND dc_md5 = 1 AND dc_ua = 1 AND md5_len > 1))
| search sourcetype=imageload AND sourcetype=processes AND (sourcetype IN (tcp,url))

Its a lot of SPL — but basically its taking the individual sourcetypes the EDR log types are split into — combining them into 1 tstats search then ONLY displaying results where a process has loaded at least 54 DLLs, has network traffic (url/tcp) is on 3 or less systems, located within user writable file paths (folders).

This is nice — but, if you’ve been following my other posts you know I’m a fan of enriching data (specifically VirusTotal and other threat intelligence sources). You can find those other stories: Part 1 and Part 2. By enriching data we have additional attributes to filter false positives.

| lookup vt_file query as md5
| where isnull(sig_info)

Using the above SPL I’m querying VirusTotal (via v3 API) to lookup information about a hash value. One of the fields our lookup brings back is sig_info which provides information relevant to a binaries digital signature (basically whether it was signed or not and if it was who is the CA). Assuming most attackers are unable to get their beacon’s signed by a reputable CA the above SPL ignores any binary that is signed. This drops the false positive ration down pretty dramatically as almost all Microsoft binaries will be ignored.

We’re left with what we’re after — the same processes we already knew our Red Team was using — Cobalt Strike beacons galore!

Pair this detection with a previous detection (detecting non-browser processes communicating with newly registered domains) and you just made an adversaries job that much harder.

Disclaimer — our Red Team never runs CS using the default configurations — however, the EDR tool deployed throughout our environment is pretty good about picking up generic default CS beacon activity.

--

--

redhead0ntherun

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