Saturday 28 October 2017

CTF Writeup - Flare-On 2017 - 07: zsud.exe


  • Name - zsud.exe
  • Category - Reverse Engineering
  • Points - 1
  • Binary - Download here

The 7th Flare-On challenge is an x86 single-user dungeon game:


The game allows you to move around, interact with objects, pick them up, equip them, drop them and even talk to a guy called Kevin.

Loading the binary in IDA and looking at the strings tab we notice a few interesting ones:
  • M:\\whiskey_tango_flareon.dll
  • flareon.dll
  • !This program cannot be run in DOS mode.
  • .text
  • .rsrc
  • .reloc

All of these strings point towards an embedded DLL that starts at location 0x148AB0:


Following the xrefs for this memory region we are directed to function sub_F7170() from which we can identify the size of the embedded DLL:


Extract 0x1200 bytes starting from 0x148AB0, dump them into a file, rename to flareon.dll and open it using dnSpy:

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Management.Automation;
using System.Security.Cryptography;
using System.Text;

namespace flareon
{
    public class four
    {
        private static string Decrypt2(byte[] cipherText, string key)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(key);
            byte[] array = new byte[16];
            byte[] iV = array;
            string result = null;
            using (RijndaelManaged rijndaelManaged = new RijndaelManaged())
            {
                rijndaelManaged.Key = bytes;
                rijndaelManaged.IV = iV;
                ICryptoTransform transform = rijndaelManaged.CreateDecryptor(rijndaelManaged.Key, rijndaelManaged.IV);
                using (MemoryStream memoryStream = new MemoryStream(cipherText))
                {
                    using (CryptoStream cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Read))
                    {
                        using (StreamReader streamReader = new StreamReader(cryptoStream))
                        {
                            result = streamReader.ReadToEnd();
                        }
                    }
                }
            }
            return result;
        }

        public static int Smth(string arg)
        {
            using (PowerShell powerShell = PowerShell.Create())
            {
                try
                {
                    byte[] cipherText = Convert.FromBase64String(arg);
                    string script = four.Decrypt2(cipherText, "soooooo_sorry_zis_is_not_ze_flag");
                    powerShell.AddScript(script);
                    Collection collection = powerShell.Invoke();
                    foreach (PSObject current in collection)
                    {
                        Console.WriteLine(current);
                    }
                }
                catch (Exception var_5_70)
                {
                    Console.WriteLine("Exception received");
                }
            }
            return 0;
        }
    }
}

The DLL accepts a Base64 string, decodes it, AES decrypts it using the key 'soooooo_sorry_zis_is_not_ze_flag' and runs the resultant PowerShell script. Let's try and extract the PS1 script from the binary.

At the beginning of function sub_12523D0(), a region in memory is Base64'd and then passed to flareon.dll after this has been loaded into memory. The following image shows the resultant base64 string pointed at by EAX right after it has been encoded:


Extract the base64 string and create a C# program, based on the .NET flareon.dll, to recover the PS1 script:


[ ... snip ... ]

public static int Smth(string arg)
{
    byte[] cipherText = Convert.FromBase64String(arg);
    string script = Program.Decrypt2(cipherText, "soooooo_sorry_zis_is_not_ze_flag");
    System.IO.StreamWriter file = new System.IO.StreamWriter("zsud.ps1");
    file.WriteLine(script);
    file.Close();
    return 0;
}

public static void Main()
{
    string base64encoded = "Sbogppc38m/yviiq2 … [snip] … AyyAEQ2IwZPNoPEzE=";
    Smth(base64encoded);
    Console.ReadKey();
}

[ ... snip ... ]

The decrypted script can be viewed/downloaded here. If we run the powershell script on its own, we get the same UI depicted in the first image above.

First thing to notice in the script is the string 'http://127.0.0.1:9999/some/thing.asp' on line 814 which hints that the script communicates with a local web server embedded in the binary. This is confirmed by the following netstat result when zsud.exe is open:

Command Prompt
C:\>netstat -antp tcp | findstr 9999 TCP 127.0.0.1:9999 0.0.0.0:0 LISTENING InHost C:\>


Let's go back to the PS1 script and try to uncover what it's doing. The following is an excerpt of the important sections:


[ ... snip ... ]

$key = New-Thing "a key" "You BANKbEPxukZfP2EikF8jN04 ... [snip] ... PtEVBhQ==" @("key")

[ ... snip ... ]

function Invoke-XformKey([String]$keytext, [String]$desc) {
    $newdesc = $desc 

    Try {
        $split = $desc.Split()
        $text = $split[0..($split.Length-2)]
        $encoded = $split[-1]
        $encoded_urlsafe = $encoded.Replace('+', '-').Replace('/', '_').Replace('=', '%3D')
        $uri = "${script:baseurl}?k=${keytext}&e=${encoded_urlsafe}"
        $r = Invoke-WebRequest -UseBasicParsing "$uri"
        $decoded = $r.Content
        if ($decoded.ToLower() -NotContains "whale") {
            $newdesc = "$text $decoded"
        }
    } Catch {
        Add-ConsoleText "..."
    }
    
    return $newdesc
}

function Invoke-MoveDirection($char, $room, $direction, $trailing) {
    $nextroom = $null
    $movetext = "You can't go $direction."
    $statechange_tristate = $null

    $nextroom = Get-RoomAdjoining $room $direction
    if ($nextroom -ne $null) {
        $key = Get-ThingByKeyword $char 'key'
        if (($key -ne $null) -and ($script:okaystopnow -eq $false)) {
            $dir_short = ([String]$direction[0]).ToLower()

            ${N} = ${sC`Ri`Pt:MS`VcRt}::("{1}{0}" -f'nd','ra').Invoke() % 6

            if ($directions_enum[$dir_short] -eq ($n)) {
                $script:key_directions += $dir_short
                $newdesc = Invoke-XformKey $script:key_directions $key.Desc
                $key.Desc = $newdesc
                if ($newdesc.Contains("@")) {
                    $nextroom = $script:map.StartingRoom
                    $script:okaystopnow = $true
                }
                $statechange_tristate = $true
            } else {
                $statechange_tristate = $false
            }
        }

        $script:room = $nextroom
        $movetext = "You go $($directions_short[$direction.ToLower()])"

        if ($statechange_tristate -eq $true) {
            $movetext += "`nThe key emanates some warmth..."
        } elseif ($statechange_tristate -eq $false) {
            $movetext += "`nHmm..."
        }

        if ($script:autolook -eq $true) {
            $movetext += "`n$(Get-LookText $char $script:room $trailing)"
        }
    } else {
        $movetext = "You can't go that way."
    }

    return "$movetext"
}

[ ... snip ... ]

$baseurl = 'http://127.0.0.1:9999/some/thing.asp'
$directions_enum = @{'n' = 0; 's' = 1; 'e' = 2; 'w' = 3; 'u' = 4; 'd' = 5}

[ ... snip ... ]



The script does the following for each movement we perform in the game:
  1. Calls Invoke-MoveDirection
  2. Generates a random number between 0 and 5 (line 39)
  3. Translates our move to a number (lines 27 & 77)
  4. If these 2 numbers coincide, call Invoke-XformKey (line 43)
  5. Sends the list of directions (k param) and url-encoded key description (e param) to local web server (lines 15 & 16)
  6. If response does not contain 'whale', set it as the new key description (lines 18 & 19)
  7. If response contains '@', warp to starting room and stop game (lines 45 - 47)
The last bullet point hints that if we send the correct GET request to the local web server, it should spit the e-mail address. Using parts of the PS1 script above, we create another PS1 script to try and bruteforce the first 25 directions expected by the server:

iex ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("U0VULWl0RW
[ ... snip ... ]
"Xl9LCAke2ZpRWxkVmFgTGB1YGVzfSAgKTsgIHJldHVybiAke2FgVFRyfTsgICB9")))

.("{0}{1}{2}"-f 'SEt-IT','E','M') VariaBLE:q21s 
[ ... snip ...]
 ${T`ype`BU`IldEr}.("{2}{0}{1}"-f 'teTy','pe','Crea').Invoke(); }


[STring]::joIN( '', ('35h88w112_119}81r74r77h100J94<5
[ ... snip ...]
|%{[CHAR]($_ -BXoR  0x03  ) } ) )|.( $ShelliD[1]+$SheLLID[13]+'X')


$key = New-Thing "a key" "You BANKbEPxukZfP2EikF8jN04 ... [snip] ... PtEVBhQ==" @("key")

$baseurl = 'http://127.0.0.1:9999/some/thing.asp'

function Invoke-XformKey([String]$keytext, [String]$desc) {
    $newdesc = $desc 

    Try {
        $split = $desc.Split()
        $text = $split[0..($split.Length-2)]
        $encoded = $split[-1]
        $encoded_urlsafe = $encoded.Replace('+', '-').Replace('/', '_').Replace('=', '%3D')
        $uri = "${script:baseurl}?k=${keytext}&e=${encoded_urlsafe}"
        $r = Invoke-WebRequest -UseBasicParsing "$uri"
        $decoded = $r.Content

        # If response is different than previous one, set as new description
        if ($decoded.ToLower() -ne $encoded.ToLower()) {
            $newdesc = "$text $decoded"
            return $newdesc
        }
    } Catch {}
}

$directions_enum = @{0 = "n"; 1 = "s"; 2 = "e"; 3 = "w"; 4="u"; 5 = "d"}
$cumulativekey = ""

for ($i=0; $i -lt 25; $i++){
    for ($j=0; $j -lt 6; $j++){
        $testingchar = $directions_enum[$j]
        if ($newkey = Invoke-XformKey $cumulativekey$testingchar $key){
            $key = $newkey
            $cumulativekey += $testingchar
            break
        }
    }
}

echo "Directions: $cumulativekey"
echo "Key: $key"

Running the script:

Command Prompt
C:\>powershell -file bruteforcer.ps1 Directions: wnneesssnewne Key: You can start to make out some words but you need to follow the ZipRg2+UxcDPJ8T iemKk7Z9bUOfPf7VOOalFAepISztHQNEpU4kza+IMPAh84PlNxwYEQ1IODlkrwNXbGXcx/Q== C:\>


We notice 3 important things when we run the script:
  1. The key is incompletely decoded
  2. Only the first 13 direction attempts out of the 25 were executed
  3. Multiple runs produce the same output even though the expected input is decided by rand()
The first 2 happen because the server is bruteforce resistant and is expecting the right directions to spit out the answer. The 3rd bullet point is answered by looking at lines 431 - 433 of the extracted PS1 script:

if (($thing.Keywords -Contains "key") -and ($container_new -eq $script:char)){
    ${Msv`c`RT}::("{1}{0}"-f 'rand','s').Invoke(42)
}

When we pick up the 'key' in the game, srand(42) is executed, which explains why the rand() function always produces the same result and hence the server always expects the same directions. If we factor in the srand(42) though we notice that the output of 'rand() % 6' still does not match 'wnneesssnewne', which has been proven to be right else we would've never decoded parts of the key.

During the challenge it took me ages to realise the trick: both the rand() and srand() function are hooked in the binary and replaced with a custom implementation! This happens in function sub_1336530():


In the image we can see that the pointer to the real rand() is being hooked by new_rand() whilst srand() is being hooked by new_srand(). Let's take a look at both of these new functions. The following is the new_srand():


Recall that srand() is called from the PS1 when we collect the key. When this happens, it sets the global variable dword_138BD28 to 1, as displayed in the image above.

Let us take a look at the new implementation of rand(), i.e. new_rand():


If the global variable dword_138BD28 has been set to 1, i.e. new_srand() has been called, then it will end up in the green basic block which takes the expected direction from the array dword_1389CB8. This is NOT random at all!

Let's create a python script to extract all the expected directions:

import sys

directions_enum = {0:"n", 1:"s", 2 : "e", 3 : "w", 4:"u", 5 : "d"}

dword_1389CB8 = [0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00]

dword_138BD24 = 0

for x in range(200):
    result = dword_1389CB8[dword_138BD24 * 4]
    dword_138BD24 = (dword_138BD24 + 1) % 0x35
    sys.stdout.write(directions_enum[result])

Running the script:

Command Prompt
C:\>python direction_extractor.py wnneesssnewneewwwdundundunsuneunsewdunsewsewsewsewdunwnneesssnewneewwwdundundun suneunsewdunsewsewsewsewdunwnneesssnewneewwwdundundunsuneunsewdunsewsewsewsewdu nwnneesssnewneewwwdundundunsuneunsewdunsew C:\>


Sw33t! Let's modify the PS1 script now to specifically send these rather than trying to bruteforce our way:


[ ... snip ... ]

$solutions = "wnneesssnewneewwwdundundunsuneunsewdunsewsewsewsewdunwnneesssnewneewwwdundundunsuneunsewdunsewsewse"

for ($j=1; $j -lt 54; $j++){
    $tempSolution = $solutions.substring(0,$j)
 if ($newkey = Invoke-XformKey $tempSolution $key){
        $key = $newkey
    }
}

echo "Directions: $cumulativekey"
echo "Key: $key"

And this time:

Command Prompt
C:\>powershell -file send_right_directions.ps1 Directions: wnneesssnewneewwwdundundunsuneunsewdunsewsewsewsewdun Key: You can start to make out some words but you need to follow the RIGHT_PATH!@66696e646b6576696e6d616e6469610d0a C:\>


Not exactly what we want by close enough. The long hex string at the end translated to 'findkevinmandia'. So let's go and find Kev ... nahh, let's extract the logic directly from the script.

The decrypter to this output can be found in the original PS1 file on lines 463 - 477 which are invoked when we talk to Kevin Mandia whilst wearing a helmet and the key is dropped in the room. Extracting the logic from the script and putting our response from the server we end up with the following script:

$key = "You can start to make out some words but you need to follow the RIGHT_PATH!@66696e646b6576696e6d616e6469610d0a"

$md5 = New-Object System.Security.Cryptography.MD5CryptoServiceProvider
$utf8 = New-Object System.Text.UTF8Encoding
$hash = [System.BitConverter]::ToString($md5.ComputeHash($utf8.GetBytes($key)))


$Data = [System.Convert]::FromBase64String("EQ/Mv3f/1XzW4FO8N55+DIOkeWuM70Bzln7Knumospan")
$Key = [System.Text.Encoding]::ASCII.GetBytes($hash)

# Adapated from the gist by harmj0y et al
$R={$D,$K=$Args;$H=$I=$J=0;$S=0..255;0..255|%{$J=($J+$S[$_]+$K[$_%$K.Length])%256;$S[$_],$S[$J]=$S[$J],$S[$_]};$D|%{$I=($I+1)%256;$H=($H+$S[$I])%256;$S[$I],$S[$H]=$S[$H],$S[$I];$_-bxor$S[($S[$I]+$S[$H])%256]}}
$x = (& $r $data $key | ForEach-Object { "{0:X2}" -f $_ }) -join ' '
$resp = "`nKevin says, with a nod and a wink: '$x'."
$resp += "`n`nBet you didn't know he could speak hexadecimal! :-)"

echo $resp

The output:

Command Prompt
C:\>powershell -file kevin_mandia.ps1 Kevin says, with a nod and a wink: '6D 75 64 64 31 6E 67 5F 62 79 5F 79 30 75 72 35 33 6C 70 68 40 66 6C 61 72 65 2D 6F 6E 2E 63 6F 6D'. Bet you didn't know he could speak hexadecimal! :-) C:\>


Converting the hex to ascii: mudd1ng_by_y0ur53lph@flare-on.com

No comments:

Post a Comment