In the last blog post I outlined many of the lesser-known components of Invoke-Obfuscation, many of which I added in the weeks and months following the initial release. Several of these additions introduced more randomization and obfuscation, but more importantly provided more visibility into the obfuscation syntax itself. This visibility fundamentally changed the way that I use Invoke-Obfuscation, which is the subject of this post.
As a defender I spend a lot of time removing "noise" from large data sets to find suspicious activity. When I think about using obfuscation to evade assumed defenses I typically decide between insanely obfuscated versus just enough obfuscation to break whatever detection rules I think might be in effect, typically trying to blend in with the noise with which I and likely the target's defensive resources, both human and product, are all too familiar.
When stealth is top priority then I often use Invoke-Obfuscation to take my payload to a certain point, but then obfuscate by hand the rest of the way. The con to this is that it takes a lot of time and precision. The pro (depending on how you look at it) is that many organizations are still running PowerShell version 2 or 3 and do not have script block logging enabled, so often this manual obfuscation is not necessary. However, when an organization has their ducks in a row then it forces me to think a lot more intentionally about what I am changing and what defense I am attempting to evade with each change.
So here are the main points that I consider when using Invoke-Obfuscation for both commands and scripts:
- If you want obfuscation to persist into PowerShell script block logs (EID 4104) then token-layer obfuscation is a must. Token obfuscation (
TOKEN\ALL\1) is almost always the first option that I apply to any command or script. For smaller commands I typically obfuscate one token type at a time until it produces the obfuscation syntax that I like (like
Set-Variablefor TYPE obfuscation). I will also run
TOKEN\MEMBER\2after MEMBER obfuscation since for options
4a .Invoke() is added to maintain compatibility with PowerShell version 2.0 as this is not necessary in PowerShell version 3.0+. You can see this .Invoke() syntax for these two options in the MEMBER obfuscation menu:
- In version 1.6 (2017-01-24) I removed WHITESPACE token obfuscation from being applied for
TOKEN\ALL\1since it adds to the already significant time overhead when obfuscating large scripts. However, I usually apply whitespace obfuscation to large scripts anyways just because I'd hate to see all the work for the function go to waste :). But in all seriousness, I do tend to apply 2-3 rounds of
TOKEN\WHITESPACE\1to almost every command the I obfuscate. Too much whitespace, however, becomes a good indicator for defenders so that is something to keep in mind, and as defenders we should not rely on randomized whitespace being applied. As I stated in the beginning of this post, if I am not going full-blown obfuscation then I almost never add any unnecessary whitespace so as to avoid standing out.
- After applying token-layer obfuscation I typically either choose a custom LAUNCHER not in the public release that avoids the PowerShell command ever hitting any command line arguments (typically via environment variables, standard input, WMI, or some disk-based options), or I add STRING and or ENCODING obfuscation to clean up all the crazy special characters that TOKEN obfuscation introduces. If my payload is a command under 2000 characters then I usually opt for an ENCODING option since these cause argument bloat more than the STRING options. Otherwise I opt for STRING obfuscation for scripts since usually script length is not a concern in regards to cmd.exe's command length limit. This is definitely APT32's favorite combination -- TOKEN plus a lot of STRING obfuscation.
- Going back to ENCODING, it is helpful to know which ENCODING options produce the most and least command bloat. The below table shows the average length over 1000 iterations of a 100-character command run through each ENCODING option:
So for the least command bloat I typically use
ENCODING\. Using this as a starting point, remember that you can repeatedly run
UNDO,ENCODING\*,SHOW OPTIONSuntil you get get an encoding payload that is in the size range that you are targeting (
SHOW OPTIONSshows the size of the obfuscated command).
- In several recent scenarios I found myself battling command length limits as well as the need to convert a multi-line script into a one-liner command. STRING obfuscation does not help in this situation as it maintains new-lines. So despite its command bloat ENCODING obfuscation has remained the best option for converting a multi-line script into a one-liner command...until now! In conjunction with this blog post I am releasing a COMPRESS option to Invoke-Obfuscation via a new function Out-CompressedCommand.ps1.
The core of this function is a heavy copy/paste from Matt Graeber's (@mattifestation) Out-EncodedCommand.ps1 minus the -EncodedCommand syntax and adding a few dashes of obfuscation to the decoding and decompression components. This addition is quite helpful in reducing the command size after TOKEN obfuscation and before adding a final layer of STRING or ENCODING obfuscation, like this recipe:
TOKEN\ALL\1,BACK,MEMBER\1,BACK,WHITESPACE\1,1,1,COMPRESS\1,HOME,(ENCODING|STRING)\*. An example compressed command is shown below:
- The last piece of the obfuscation puzzle is how to launch the obfuscated command. If I am launching the command from a macro, HTA, SCT, scriptlet file, etc. then I almost never use any LAUNCHER found in the public version of Invoke-Obfuscation. Instead, as I said earlier, I craft a launching solution that will keep the PowerShell command off of any process command line arguments (usually with process-level environment variables or standard input via WScript.Shell's .StdIn.WriteLine as in this sample tweeted from @JohnLaTwC). However, where Invoke-Obfuscation's LAUNCHER options are helpful are in creating payloads for persistence (registry, WMI, etc.) or lateral movement (scheduled task, service, WMI again, etc.). Some Red Teamers have found
LAUNCHER\WMICuseful for lateral movement when using Cobalt Strike's wmic command as they can simply add in the /node: and /user: fields to the result from Invoke-Obfuscation.
- The final artifact that I use when choosing a LAUNCHER is the Process Argument Tree displayed after the application of a LAUNCHER. I primarily added this view for defenders, but as a Red Teamer it is extremely helpful in choosing a LAUNCHER that abstracts the PowerShell command into a different process or even breaks the parent/child process relationship altogether (
WMIC). More importantly, I often apply, undo and re-apply LAUNCHERS 5-10 times until it produces a satisfactory obfuscated syntax for the final powershell.exe invocation. Since there are sooooo many randomizations in Invoke-Obfuscation some of my favorite syntaxes only surface every 10+ invocations, thus my repeated use of command chaining paired with
UNDOuntil I get just the right syntax. For example, after applying
LAUNCHER\STDIN++\234... ...I might keep running
UNDO,LAUNCHER\STDIN++\234until it produces a less common invocation syntax for the final powershell.exe process as well as no space between the parent cmd.exe's /C argument and the rest of the command stored in the process-level environment variable %jvh% like this:
That wraps up this usage guide for the current iteration of Invoke-Obfuscation. As always, please obfuscate legally, responsibly and only with express written consent from the owner of the system(s) on which you are running code.
In addition, I encourage Red Teamers using this tool to develop detection recommendations for their clients. Developing detections helps clients and can improve the Red Team's TTPs as it often leads to more elusive and creative attack and obfuscation ideas going forward. Red Teamers can be successful while also helping improve the target's defensive understanding and posture, and these iterative steps will naturally raise the bar for Red and Blue alike. That has been and remains the objective of this research and framework, and I hope that users will support that objective.