How to Peel a PowerShell Onion: A Bloodhound Case Study

Published May 2, 2022


Recently the GuidePoint Security DFIR team was called in to conduct an investigation for a Managed Service Provider (MSP). The response effort included multiple environments, including their own, and involved multiple detections of Cobalt Strike and Mimikatz sprinkled throughout the environment with a side of Active Directory reconnaissance tools. Fortunately enough, the DFIR team was able to intervene and prevent the threat actor from conducting further post-exploitation efforts and there was no deployment of ransomware (queue up the low bones). 

During the investigation, as is often the case when Cobalt Strike is in play, one system was identified as having a large PowerShell script executed on it. The GuidePoint Research and Intelligence Team (GRIT) was called in to assist with malware analysis for this investigation and took a look at this beefy PowerShell script (amongst many other malware samples). There wasn’t anything cutting edge or novel about the malicious script–in fact, we had seen similar scripts many times before–however, it can be challenging for analysts to analyze. In this case, we used it as a learning opportunity to pass on some malware analysis skills to a few DFIR analysts. 

Being effective at triaging malicious PowerShell scripts can be a huge time saver during an incident response (especially when doing IR at scale), and that being the case, we figured it might be beneficial to pass these skills along to other analysts outside of our team as well. 

In this how-to tutorial we will use the following five-step methodology to peel back the layers on this PowerShell Onion:

  • Step One: Ensure you Have a Workable Script
  • Step Two: Get the Lay of the Land
  • Step Three: Take the Lazy Route and Let the Script Deobfuscate Itself
  • Step Four: Repeat Steps 1-3 to Prepare for the Final Payload
  • Step Five: Verify the Final Payload

PowerShell Scripts Can Be Like Onions (They Sometimes Have Layers)

Once upon a time in a kingdom far far away, a wise Ogre once said: “Ogres are like onions… they have layers.” The same adage is sometimes true for PowerShell scripts, they also can have layers. In the case of this malicious PowerShell script, there were six layers to be exact. 

In many circumstances, we encounter one or two levels of obfuscation of a PowerShell script, but that’s it. In this case, we had to deal with a few more. The positive note is that most of the layers employed the same type of obfuscation which made it quite a bit easier to deal with. 

Let’s take a look at what we were dealing with and start working through this malicious script.

Step One: Ensure you Have a Workable Script

Using PowerShell script block logs from the Windows Event logs on a system is a great way to find evidence of malicious PowerShell executions during an incident response investigation. The DFIR team utilizes Velociraptor to pull PowerShell logs at scale, and while reviewing these logs in Velociraptor the DFIR analyst encountered the following message:

Creating Scriptblock text (1 of 65):

Figure 1: PowerShell Script Block Event Log

If you’ve ever done PowerShell log review and encountered something similar to this, then you know just how “fun” consolidating this into a workable script can be. Alas, this is the first step in being able to analyze this malicious PowerShell script: we have to make this mess into something we can work with.

My favorite tool for reworking scripts is Sublime Text. It has really nice syntax highlighting for multiple languages, RegEx-based find-and-replace functionality, and much more that makes working with difficult scripts easy to manage. When working with obfuscated or “ugly” PowerShell scripts, you can use some of these tricks to make the script a bit easier to work with:

  • For PowerShell scripts that come as convoluted one-liners with multiple commands combined into one ugly script, replace all semicolons with a newline. This makes it much easier to read and work through.
    • Caveat: If there are for loops included in the script, you will have to add those semicolons back in manually to ensure the script is runnable if needed.
  • Use RegEx find-and-replace functionality to remove unnecessary text and new line characters
  • Find and remove all instances of obfuscation characters such as ^, , , @, and other characters that do nothing other than make your eyes want to bleed.
    • Caveat: Make sure that removing these characters does not impact the functionality of the script.

Step Two: Get the Lay of the Land

Now that we have a prettified script (hopefully all of those syntax colors are really popping at this point), we can start to take stock of what this script’s intentions might be. Specifically, we want to see what kinds of quick wins we can get from a first glance at what the code looks like. 

In our case, our prettified script looked like this:

Figure 2: Prettified PowerShell Script

Although our pretty script only has 45 lines, don’t let that fool you. Line one had over a million characters in it! By quickly reviewing this script we notice a couple of things that are going to help us along the way.

What we noticed:

  • Historically when we see a single line with over a million characters, that sets off some bells that it’s most likely a payload, more specifically an obfuscated Windows PE.
  • Many of the strings start with “==” and include the base64 character set, which is the hallmark of base64 encoded contents in reverse. This means that there is probably going to be some code that is meant to reverse the string before using it.
    • We can confirm this hypothesis quickly by observing multiple method calls to FromBase64String, ToCharArray, and Reverse.
  • We see several properties on the variable $kxuitqqsyx including Padding, BlockSize, KeySize, Key, IV, and a call to the method CreateDecryptor. These properties and this method are all indicative of encryption being used within the script.
  • At the very end of the script, we see the use of Invoke-Expression. This means that whatever contents are stored in the variable $oguhpmjnjvhh will be executed next. This means there is likely more to the story than we see here.

Just by taking a quick look at the still obfuscated script, we have an idea of what to expect. Now we can move forward with some intentional and controlled dynamic deobfuscation and confirmation of functionality.

Step Three: Take the Lazy Route and Let the Script Deobfuscate Itself

Before we start doing anything past static analysis, now is a good time to remind everyone that a lab environment that follows proper malware analysis and operations security standard operating procedures is a must. Make sure that you are doing analysis like this in a safe and controlled environment. A virtual environment with controlled networking and snapshot capabilities is the way to go if you have that available. Now that all the administrative notes are out of the way, let’s get ready to run this thing.

The primary goal when starting to dynamically work with samples is to control the flow of execution. There are a couple of ways to do this:

  1. Run the script in the PowerShell ISE and set breakpoints to stop executing at strategic points where you can learn the values of variables and interact with the current state.
  2. Go old school. Replace any “execution” calls to cmdlets like Invoke-Expression or Invoke-Command, and use strategically placed print statements to learn what the script is going to do. 

For this tutorial, we are going to select the tried and true option number two (it tends to be faster).

If we replace Invoke-Expression($oguhpmjnjvhh) with Write-Host $oguhpmjnjvhh, that will immediately tell us what the next actions of this malicious script are within the console. To make it even more useful, we can replace Invoke-Expression($oguhpmjnjvhh) with $oguhpmjnjvhh | Out-File .\Next_Stage.ps1 which will add the contents to another file for us to review. 

We opted for writing the contents of the script to another file and this is what it looked like.

Figure 3: Second Level Prettified Script

At this point, you should be having deja vu. This second script probably has you second-guessing whether we are showing you the same file as before. The flow and formatting of this stage are nearly identical to the stage before it. All the same flow and functionality are present.

Now that we know our methodology works to peel back the layers of this malicious PowerShell script, let’s keep the peeling going. At this stage, we simply continued to replace the Invoke-Expression statement to allow us to keep dumping subsequent stages out to files.

Rinse and repeat until you finally get to something different and more interesting.

Step Four: Repeat Steps 1-3 to Prepare for the Final Payload

At this point I think I can probably guess what you are thinking: “but why didn’t you use some cool code and RegEx to just automatically peel back this PowerShell onion and give you the final script in one shot?” And that’s a fair question. The honest answer is that we typically recommend giving the deobfuscation routine a handful of manual iterations before thinking of moving to an automated methodology, especially when first learning the process. With debugging, screwing up some code, monkeying with RegEx, and getting a final product it sometimes takes longer than just doing the manual analysis. That being said, if I knew I was going to encounter this same script 10 more times next month, I would invest the time to make a really cool decoding script. In this case, our “handful of attempts” rule turned out to be just enough iterations to get us what we were looking for.

The resultant PowerShell contents were quite ugly. Below are two screenshots, one from the beginning of the script content and one from the end.

Figure 4: Final Level Definition of PSHound Function

Figure 5: Final Level Obfuscated Contents

Getting a lay of the land once again, three things immediately stood out while looking at this hot mess. 

  • The function defined was called PSHound. (I think all of us are on the same page with what this script is going to attempt to do). 
  • $bjgrs was yet another huge string that is likely the final obfuscated payload we are interested in.
  • We had more obfuscated content to deal with. This leads us back to step one, making this into a workable script.

Once we replaced ; with new-line characters and removed the unnecessary ` and @ characters, we ended up with something a little bit more pleasant to work with.

Figure 6: Final Level Prettified Contents

In this final section of the script, there are two parts to be aware of:

  • The contents stored in $hdpr appear to be responsible for decoding contents, loading code in memory, and executing it
  • An alias, gshpp, will be created and used by the script 

We will start with looking at the purpose of gshpp.
Having a strong knowledge of PowerShell will give you an edge with quickly realizing what a malicious script is attempting to accomplish. In this case, the malicious script iterates through a loop until it is able to successfully set a new alias, gshpp, for iex (which is itself an alias for Invoke-Expression).

Figure 7: Loop to Create New Alias for IEX

This was a somewhat clever method for executing malicious script components without explicitly calling Invoke-Expression. Although this is interesting for verifying how the script is attempting to execute the final payload on the system, it still doesn’t get us our juicy payload.

On to the final step!

Step Five: Verify the Final Payload

If we focus on the contents of $hdpr and yet again go back to step one to make this a bit easier to work with, we can remove the extra quotation marks and make these contents even more clear than before.

Figure 8: Deobfuscation and Loading Routine

If we once again get a lay of the land, we can quickly identify that much of this routine is meant to deobfuscate our final payload before loading what we expect to be a .NET assembly into memory before executing a function called [Diagnostics]::Time. The biggest clue that we were dealing with a .NET assembly is due to how the functions were being called. When there are functions invoked with the “::” notation as you see above, that’s a clear indication that .NET is being utilized. This is also very common with dynamically loading executables in PowerShell since there are many .NET integrations built in.

Now that we know our final deobfuscated payload is most likely a .NET assembly based on the method and function calls, we can walk through the deobfuscation process and let the script do nearly all of the work for us. You’ll notice that $input is highlighted in blue within Sublime Text, this means that $input is a reserved term within PowerShell. For us to let PowerShell execute this on our behalf, we must change this to something new. Additionally, we can replace lines 7-8 with [io.file]::WriteAllBytes to write all of the deobfuscated byte contents to a file for us to interact with. When we fix up this final script, it looks something like this.

Figure 9: Modified Deobfuscation and Loading Routine

And once this execution is complete, we are now the proud owners of Bloodhound!

Figure 10: Bloodhound Executable

And because we are the extra mile kind of malware analysts, we can open this file up in a .NET disassembly tool like dnSpy and see all the sweet Active Directory reconnaissance features this binary has to offer.

Figure 11: Viewing the Executable in dnSpy


This PowerShell script was by no means the most difficult example of an obfuscated malicious script, but it does highlight the same process that we use when encountering almost any malicious script whether it is PowerShell, Javascript, VBScript, etc. Sometimes malware analysts have the luxury to take more time to examine all facets of a malicious script and admire the tricks that malware authors employ (this is personally my favorite part). But during an incident response, especially when doing incident response at scale with a tool like Velociraptor, time is of the essence. The faster an analyst can deobfuscate a PowerShell script and determine the intent, impact, and indicators of compromise, the more beneficial they will be to the effectiveness of the investigation. 

Until next time, happy hunting and happy reversing!

*** This is a Security Bloggers Network syndicated blog from The Guiding Point | GuidePoint Security authored by Drew Schmitt. Read the original post at: