Several weeks ago, during one of the investigations, I needed to triage a few potentially malicious Windows executables. One of them caught my attention - a .NET binary located in a seemingly legitimate subdirectory under Program Files. At the same time the file was obfuscated (based on a quick look at FLOSS output) and according to VirusTotal it was detected as “potentially malicious” by several antivirus products. Well, I thought, even if the file turns out to be non-malicious, there must be a reason for it to be obfuscated. Oh boy, how little did I know…
I opened the file in dnSpy and immediately encountered first obstacle - code was obfuscated with SmartAssembly. Fortunately, de4dot did all the dirty work for me and within seconds I was left with a compact code consisting of several classes. I quickly located main part of the program and realized that I am likely dealing with some kind of loader – part of the code was responsible for reading, decrypting and parsing data from two
After poking around a little bit more I found a method that was responsible for creating a new PowerShell runspace and executing PowerShell code retrieved from a previously decrypted resource. I was aware of the tools like p0wnedShell making use of exactly same method to “execute PowerShell code without running powershell.exe” so I thought that I am finally onto something.
At this stage, I just wanted to get my hands on a decrypted PowerShell code as fast as possible. Using CFF Explorer I exported
RT_RCDATA resource content to a file. Then I copied C# code responsible for decryption from dnSpy window and pasted it to LINQPad. I also needed to make a few small adjustments to the original code to read the content of the resource from a file and pass it to decryption function.
The code worked well but what I got back was not exactly what I expected. It was still a PowerShell code but it did not look like Invoke-Mimikatz or other offensive module that I knew of. Instead, I was looking at a rather ordinary script written by someone to manage software and patch installation on the workstations. All that effort for nothing? What a disappointment!
One last thing I wanted to figure out was the reason why someone made an effort to package a simple PowerShell script in this way. Was it a custom made packer? Or maybe the file was generated by some tool that I was not aware of?
I took a bunch of unique strings from the analyzed binary and fired up my favorite search engine. I quickly realized that analyzed binary was likely generated using SAPIEN Script Packager - available in products like PowerShell Studio or PrimalScript. Following this path I also found out that I was not the only one to encounter scripts packaged this way:
- Back in 2012 @mattifestation presented how to dynamically extract script content from binaries generated by SAPIEN PrimalScript
- Last year @RemkoWeijnen released ExeToPosh - a tool to statically extract script content from binaries created by SAPIEN PowerShell Studio
That’s all sorted out then, my investigation was over – or maybe not?
Nothing is lost…
Judging by the fair amount of posts on the SAPIEN Forums their products seem to be quite popular among developers and administrators. Following my “failed” investigation I started wondering if they are also popular among malware creators? Let’s take a brief look at some of the SAPIEN Script Packager capabilities:
- It allows PowerShell script to be encrypted, embedded into executable as a resource and later executed within PowerShell runspace (do you recall p0wnedShell?)
- Generated executable can enforce execution restrictions and allow embedded script to be executed only by defined users or on hosts with specified hostnames, assigned MAC addresses or AD domains
- Packages generated with newer versions of PowerShell Studio or PrimalScript can stop execution when PowerShell Script Block logging is enabled or disable it for the time of execution. Since version 5.4.144 of PowerShell Studio also Transcription logging can be disabled
- Packaged script can be also executed within the security context of another user (by using “Alternate credentials” option). “When selecting RunAs or Impersonation, the specified credentials are stored inside the packaged executable. They are of course stored encrypted.” - we’ll get back to this later
On top of that, the SAPIEN PrimalScript script packager supports many more “engines”, allowing users to package not only PowerShell scripts but also:
- VBScript and JScript (via WScript/CScript)
- HTA (via MSHTA)
- CMD and Batch (via CMD)
- Windows Script Host scripts
After learning about all these features I was almost sure that there must be some malicious PowerShell scripts packaged this way (spoiler: I was not wrong). I came up with this simple plan:
- Collect as many samples as possible
- Use ExeToPosh to extract scripts
- Analyze extracted scripts
First step went smoothly thanks to Malware Hunting capabilities offered by VirusTotal. I initially created a really basic YARA rule for Retrohunt which resulted in approximately 230 samples and additionally gave me steady influx of 1-3 samples per day when applied to newly uploaded files.
1 2 3 4 5 6 7 8
The second step was when things started going south. While ExeToPosh worked really well for some of the collected samples it failed to extract data from the rest of the files. I did not want to reverse SAPIEN’s products (the license explicitly prohibits it anyway) so I ended up analyzing 20 or so samples. After several long evenings I knew exactly what was wrong. The files were generated by a different versions of Script Packager - and while mechanics did not change much between versions it was the small things that made a difference.
Here is what I learned about collected executables generated by SAPIEN’s Script Packager:
- Samples were both native PE executables and .NET managed assemblies
- Older samples (based on compile timestamp) were not obfuscated. With few exceptions all .NET samples were obfuscated with SmartAssembly
Up to four
RT_RCDATAbinary resources were included in generated executables:
- Resource ID 1 - “configuration”: stored a binary structure defining runtime settings for the script and the associated files. This resource seemed to be mandatory and was present in every analyzed sample. If “Alternate credentials” option was used credentials were stored in two separate fields: username as clear text string and password blob encrypted with the “configuration key”
- Resource ID 2 - “library files”: stored a structure containing WSC/COM objects. The objects were sometimes compressed (ZIP) before being placed in the structure, finally the whole structure was encrypted with the “data key”. This resource seemed to be optional
- Resource ID 3 - “data files”: stored arbitrary files needed by the script. Data in this resource was organized the same way as “library files”. This resource seemed to be optional
- Resource ID 4 - “script”: stored packaged script. Script was encrypted with the “data key”. Some scripts were also compressed (ZIP) before being encrypted. This resource seemed to be mandatory and was present in every analyzed sample
Majority of analyzed samples implemented two decryption schemes: AES and what was internally called “simple decode”. AES, however, seems to be available only in the “high encryption pack” and I have not seen any sample making any use of it
- Decryption key pairs were constant among samples and were included as clear text strings in generated executables. Older samples used following static key pair:
hsdiafwiuera(“configuration key” and “data key” respectively). Newer samples (starting around release of PowerShell Studio 5.4.138) used the following static key pair:
- There were at least six different versions of configuration structures - starting with oldest and shortest (800 bytes) and ending with newest and longest (5236 bytes). Each new version introduced new fields or changes in the field format (e.g. character encoding)
- Some fields present in the configuration structure were not always used by the loader at runtime. Taking this into account it is not always possible to tell if a given option is used without fully reverse engineering specific sample
- Based on additional strings included in extracted scripts, collected samples were created by a range of products: starting with quite dated PrimalScript 2009 and ending with most recent PowerShell Studio 2018 5.5.150 and PrimalScript 2018 7.4.112
In order to facilitate all of the above options and to speed up the analysis of several hundred samples I decided to create my own tool to statically analyze and extract embedded data. You can find the script on Github. It works well for samples that I have collected but taking into account the variety of Script Packager versions and packaging options it may fail miserably in certain situations.
After running all collected samples through my script I ended up with a log file containing more than one million lines of scripts (mostly PowerShell) and metadata.
…Everything is forgotten (Unless you upload it to VirusTotal)
When I started going through extracted data I was a little bit baffled - I expected to find relatively large number of malicious scripts (after all I sourced all samples from VirusTotal). Out of 250 analyzed samples only approximately 20% turned out to be malicious:
- 45 instances of
fontdrvhost.ps1- a PowerShell downloader and management script for cryptocurrency miners (e.g. Hybrid Analysis, VirusTotal, extracted script). Interestingly majority of samples contained configuration (e.g. proxy servers) for specific AD domains. All collected samples used
- Test PowerShell Empire stager
TrashPayloadMVEC.ps1(VirusTotal, extracted script)
- Test PowerShell Empire stager
LabTestHttp.ps1(VirusTotal, extracted script)
- Cobalt Strike Beacon loader
amazon_64.ps1- set to communicate with
22.214.171.124:443(Censys). It was one of the few observed samples that made use of execution restrictions - in this case only SYSTEM user was allowed to run the script (VirusTotal, extracted script)
- PowerShell loader for mimikatz
GardeRat.ps1(VirusTotal, extracted script)
- Shellcode loader - likely Cobalt Strike stager set to communicate with
126.96.36.199:443(VirusTotal, extracted script)
At the time of writing only packaged versions of
fontdrvhost.ps1 had a decent amount of AV detections. Rest of the files listed above were ignored by majority of AV engines.
That would be it for malicious scripts. So what made remaining 80% of the extracted files - or rather what interesting data was included there? This part turned out to be a real treasure trove of this small research project. Let’s start with some statistics (based on approximately 210 non-malicious samples):
- Only 4 of them were signed
- Approximately 40 samples had more than five AV engine detections according to VirusTotal. As far as I can tell this is a threshold where VirusTotal will remove a sample on your request only if you make (false) detections disappear by contacting each AV vendor. This point may be important because:
- 10 samples made use of the “Alternate credentials” where credentials for RunAs/Impersonate option are placed in the configuration resource. The list of collected credentials included such accounts as:
Another 30 extracted scripts included one or more secrets, for example:
- Numerous invocations of
Set-ADAccountPasswordincluding clear text passwords
- Several sets of credentials for FTP accounts
- Several sets of credentials for SQL databases
- Several sets of credentials for SCCM service accounts
- Malwarebytes Premium license keys
- Username and password for
- TeamViewer API tokens
- Numerous invocations of
The same amount of scripts exposed potentially sensitive data such as internal hostnames, URLs or usernames
- About 60 extracted scripts included a header exposing organization name and (user)name of script developer. List of companies included known network security vendors, health care, hospitality and entertainment providers
- Plenty of extracted scripts fell into “IT management” category exposing how different organizations perform endpoint, user and email accounts management, software deployment or IT monitoring
Looking at above points it is clear that majority of these scripts were meant to remain internal to organizations in which they were developed. Unfortunately, the way how scripts are packaged does not seem to make things any better. I can see how a .NET binary obfuscated with SmartAssembly, containing
IsDebuggerPresent string and decrypting data from its resource section during runtime can end up with a generic detections by multiple AV engines. Then it is just a short path to situation when someone uploads such ‘flagged’ binary to VirusTotal or one of the many online sandboxes.
It seems to make perfect sense to start monitoring networks and endpoints for presence of executables generated by SAPIEN Script Packager - both for malware detection and prevention of leakage of potentially sensitive data. It is worth noting that there are also other tools that can be used to package PowerShell scripts in a similar way: ISESteroids, Posh2Exe, PowerShell Pro Tools PSPack.exe.