PowerView is evil, but PowerVi and ew are legit, right? - Missing signature-based detections due to PowerShell Script Block Logging Fragmentation

Update [15/08/2024]:

In a short discussion on X the source code of the PowerShell Script Block Fragmentation was linked . Looking at the comment in the code, it becomes clear that the size of a script block fragment is intentionally set to a random value in order to deny attackers the easy possibility to split their scripts as they wish. If a script block is larger than 20000 (Unicode) characters, it is split into fragments with sizes 10000 plus a random value between 0 and 10000 - resulting in script block sizes from 10000 to 20000 characters. Further research is needed to answer the question if and how the fragmentation of PowerShell script blocks can still be exploited.

TL;DR: Sigma rules and similar signature-based threat detection measures may miss malicious PowerShell scripts due to unpredictable fragmentation of script block logs.

Introduction

Sigma offers more than 3000 rules for signature-based threat detection. 140 of these rules aim to detect suspicious/malicious PowerShell scripts by looking into PowerShell script block logs. Fragmentation of script blocks during Script Block Logging results in varying number of alerts when loading the same script multiple times. On the one hand, there is a trend of more alerts being generated when the script is split into more fragments (which is fine), but on the other hand, the fragmentation of scripts into blocks may result in missed detections.

I know this is a lot, but bear with me as I tell you the whole story. If you are only interested in the juicy part, you can skip to ‘The case of split “PowerVi/ew”’.

The Uncertainty of Script Block Logging

It is known that when loading a very large script, PowerShell breaks it into multiple parts before logging them - sometimes resulting in dozens of fragments. To illustrate this behavior, we loaded the well-known PowerView script a total of 10 times (on the same machine and configuration) and recorded into how many block fragments it was broken. The results are shown in the table below.

Run12345678910
# Blocks54765749645755693947

We can see that the number of blocks ranges from 39 to 76, which is quite a significant difference.

More script blocks -> More alerts?

Now, when using Sigma rules that operate on single logged ScriptBlockTexts, the number of generated alerts might differ because the number of logged blocks differs. More specific, the number of generated alerts usually increases with increasing number of blocks, because the malicious/suspicious strings were found in more blocks. Using “all rules” from Sigma release r2024-03-11 and the 10 recorded PowerView loadings, the following number of alerts were generated using Chainsaw (sorted by number of blocks).

Blocks39474954555757646976
Alarms79919410399106107110119126
… raised on … blocks39464853535656606570

Here, we see that the number of alarms usually increases with the number of blocks - that is the expected behavior. The only run that does not match this trend is the one that generated 55 script blocks. Here, less alerts are generated than in the run generating 54 script blocks. Although this behavior leads to inconsistency, it can be considered “not too bad” since in some cases more alerts are generated than in other cases, but overall we still catch everything, right?

More script blocks -> Less alerts??

To investigate how the number of blocks influences the number of generated alerts, we further looked into the generated alarms. Below, the number of generated alerts for each triggered rule is listed for each of the 10 runs.

Rule / Run#Blocks9#3910#474#491#547#553#576#575#648#692#76AVG
Total79919410399106107110119126103.4
Execute Invoke-command on Remote Host5666676 [2]7776.3
Malicious PowerShell Commandlets - ScriptBlock3542434946515153586148.9
Malicious PowerShell Keywords32222232322.3
Manipulation of User Computer or Group Security Principals Across AD4446 [3]44444<5>4.3
Potential In-Memory Execution Using Reflection.Assembly11111111111
Potential Suspicious PowerShell Keywords1 [1]2222222221.9
PowerView PowerShell Cmdlets - ScriptBlock2730323435353638404535.2
Request A Single Ticket via PowerShell11111111<2> +1 because of script block cut-off11.1
Usage Of Web Request Commands And Cmdlets - ScriptBlock11111111111

First, let’s look at some results that were expected.

[1] Potential Suspicious PowerShell Keywords: When having only 39 script block fragments, only 1 alarm is generated because all the “suspicious” strings fitted into the first block - because it is larger compared to the other cases.

[2] Execute Invoke-command on Remote Host: Goes from 5 to 7 raised alerts - increasing with the number of blocks because the search strings are found in more blocks. Only run 6 with 57 blocks is an outlier, producing less alerts than run 3 with the same amount of 57 blocks. This is getting suspicious..

[3] Manipulation of User Computer or Group Security Principals Across AD: In all but two runs exactly 4 alarms are generated. The run that raised 5 alarms was the one with the largest number of blocks - so this behavior is expected - but the one with the most alarms (6) only created 54 blocks. Further investigation showed that this is the result of the “random” script fragmentation, where all 6 “suspicious” strings were found in 6 different blocks, where in the other runs multiple strings where found in a single block resulting in less alerts.

Okay, so these results are kind of expected and not too bad. So we should be fine, right?

Well, when investigating the results of the rule Malicious PowerShell Commandlets - ScriptBlock , a case came true that we thought was extremely unlikely.

The case of split “PowerVi/ew

Among others, the rule Malicious PowerShell Commandlets - ScriptBlock , detects the string “PowerView” inside script blocks. Now, comparing two different runs, run3 with 57 blocks generated 51 alerts and run2 with 76 blocks generated 61 alerts for this rule. So more blocks -> more alerts, this is fine. But, looking deeper into the script blocks and generated alerts, we noticed something at the end of script block 38 of 57 of run 3.

Add-Member Noteproperty 'Comment' $Info.lgrpi1_comment\n
$LocalGroup.PSObject.TypeNames.Insert(0, 'PowerVi

And the beginning of script block 39 of 57:

ew.LocalGroup.API')\n

So, in this case the PowerView script was fragmented in such a way, that a string that should have been detected was no longer detected, i.e., “PowerView” was split into “PowerVi” and “ew”. (To be fair, script block 38 still raised an alarm because the string “PowerView” occures in it multiple times, but still this example illustrates the problem at hand.)

Losing alerts

This shows, that depending on the fragmentation of script blocks, we can indeed lose alerts and miss contents of scripts that should be detected, e.g., by strings split into two parts in two different blocks. But there are other cases: Rules like Execute Invoke-command on Remote Host detect multiple strings in a single script block (ScriptBlockText|contains|all). Now, when one of those strings is randomly put into a different block, the rule no longer triggers. Although this case should be more likely than the case of “search strings split in two”, the 10 simulations did not result in such a case since the number of alerts for this specific rule is much smaller (only 5-7 alarms compared to 35-61 for “Malicious PowerShell Commandlets - ScriptBlock”).

Conclusion

We learned that loading the PowerView script multiple times results in fragmentations of it ranging from 39 to 76 blocks. The alerts raised on these script blocks showed the trend of increasing number of alerts with increasing number of script blocks. Although this behavior adds uncertainty to the generation of alerts, it is of no critical nature. But, another example showed, that the fragmentation of scripts into blocks might result in suspicious/malicious strings being split into two blocks, resulting in a case where the search strings could not be found and the detection is completely missed. Furthermore, when searching for multiple strings in a single block, the fragmentation of scripts might result in these strings being split into two different blocks - where detection is also no longer possible.

Is there a remedy? Maybe re-combining script fragments (like this ) to run detection mechanisms on the reconstructed scripts?

Sidenote: To add, this behavior might also be leveraged by malicious actors to avoid detection…

The described findings were observed on a Windows 10 host with PowerShell Version 5.1 and PowerShell logging configurations according to the recommendations by the Australian Cyber Security Centre (ACSC) which include PowerShell Module and PowerShell Script Block Logging.