Breaking Open the dir-LIVE Script

Thu, Apr 20, 2006 5-minute read

I mentioned earlier that I was going to dissect the dir-LIVE script – it’s taken a bit longer to get to it than I had planned, but I tried to make even this joke a learning opportunity.

When you open the script, the first thing you’ll notice is that it’s mostly composed of junk characters.  “Wow,” you might say – “how does that turn into a directory listing?”

 It’s really just a slight-of-hand – not a super cool obfuscation technique.

This encoding is entirely enabled by Monad’s ability to execute the content of a string as though it were script.  For example:

MSH C:\temp> invoke-command “2+2”
4

The $contents variable isn’t just a script in a string, though.  It’s encoded.  The .Net framework natively supports a method of encoding called “Base64 encoding,” one of the most common ways to transport binary data over connections that support only plain ASCII text.  Email is one example of a technology that often uses Base64 encoding.  However, I just did it to make the script more difficult to read :-) 

So, I took my original script, Base64-encoded it, and put the results in a string.  The joke script then decodes the string, and invokes it.  This was not a security measure though (and should never be considered one,) as it is trivial to modify the script to print out the content rather than execute it.  Admit it – you had some fun hacking my script to read it :-)

Executing the content of a string is normally a one-step operation – you use the “invoke-command” cmdlet.  However, we’ve renamed the cmdlet in our internal builds.  This missing cmdlet would have broken the script for any of the many internal Microsoft folks running our next drop of code.

To get around this, the script checks to see if the “invoke-command” cmdlet exists.  If it does, it remembers to use that command.  Otherwise, it knows to use the new name, “invoke-expression.”  The script then uses Monad’s “&” syntax to execute the appropriate “invoke-*” command, with the decoded string as its argument.

Here is the script I wrote to do this obfuscation automatically:

## Obfuscates a script
param([string] $inFile)

$content = [String]::Join("`n", @(get-content $inFile))
$bytes = (new-object System.Text.UnicodeEncoding).GetBytes($content)
$encoded = [Convert]::ToBase64String($bytes)
$destinationContent = @'
$contents = @"

'@

$destinationContent += $encoded

$destinationContent += @'

"@

$contentBytes = [Convert]::FromBase64String(($contents -replace "``n",""))
$contentString = (new-object System.Text.UnicodeEncoding).GetString($contentBytes)

$invoker = get-command -ea SilentlyContinue invoke-command
if(-not $invoker) { $invoker = get-command invoke-expression }

& $invoker $contentString
'@

$destinationContent

So, here is the script in its original form:

## dir-LIVE.msh
## Participate in the Web 2.0 revolution
$generator = new-object Random
$oldForeground = $host.UI.RawUI.ForegroundColor

$sponsored = @(
   ,("System", "$($env:WINDIR)", "Meet fun young DLLs in your area")
   ,("Temp", "$($env:TEMP)", "Stash your stuff.  No credit card required.")
   ,("Home", "$([Environment]::GetFolderPath(`"Personal`"))", "Secure a lower mortgage today.  0% refinancing available!")
   ,("Autorun", "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run", "90% of all PCs have spyware.  DO YOU?")
   ,("Certificates", "Cert:\CurrentUser\My", "Locate $($env:USERNAME).  Perform a background check on ANYBODY!")
   ,("Aliases", "Alias:\ ", "New season, new time.  Only on ABC.")
   ,("Recycle Bin", "C:\RECYCLER\S-1-5-21-823518204-813497703-1708537768", "Looking for junk?  Find exactly what you want - only on EBAY.")
   ,("Current Location", "$(get-location)", "You are broadcasting your CWD!  Protect your system from hackers.")
)

$newCommand = $myInvocation.Line.Replace($myInvocation.InvocationName, "get-childitem")
foreach($alias in (get-alias | where { $_.Definition -eq $myInvocation.InvocationName }))
{
   $newCommand = $newCommand -replace $alias.Name,"get-childitem"
}

$invoker = get-command -ea SilentlyContinue invoke-command
if(-not $invoker) { $invoker = get-command invoke-expression }

$directoryContent = ((& $invoker $newCommand) | out-string).Split("`n")

function Main
{

   $lineCounter = 0
   foreach($line in $directoryContent)
   {
      $output = $line.TrimEnd()

      if(($lineCounter -eq 7) -or 
         (($lineCounter -gt 7) -and (($lineCounter % 21) -eq 0)))
      {
         $index = $generator.Next(0, $sponsored.Count)
         $host.UI.RawUI.ForegroundColor = "Yellow"
         GetSponsorLink $index
         $host.UI.RawUI.ForegroundColor = $oldForeground
      }

      $output
      $lineCounter++
   }
}

function GetSponsorLink
{
   param([int] $index)

   ""
   "+---------------------------------------------------------------------+"
   "|                                                                     |"
   "| SPONSORED CHILDITEMS:                                               |"
   "|                                                                     |"

   $currentAd = $sponsored[$index]
   $trimmedDescription = $($currentAd[1]).SubString(0,[Math]::Min($currentAd[1].Length,48))
   $adText = "| $($currentAd[0]) - $trimmedDescription".PadRight(70) + "|`n"
   $adText += "| $($currentAd[2])".PadRight(70) + "|`n"
   $adText += "|".PadRight(70) + "|"
   $adText

   "+---------------------------------------------------------------------+"
   ""
}

. Main

The script is fairly straight-forward, except for one part.  At a high-level, it does the following:

  1. Defines the 8 random advertisements that could be placed in a listing
  2. <gloss over> Complex magic to get a directory listing </gloss over>
  3. Injects advertisements approximately every 21 child items

#2 is the part that is not straight forward.  Get-ChildItem is a complex cmdlet – it supports filters, targets, recursive descents, and even dynamic parameters like “-codesign” on the certificate provider.  I had no intention of making this a production-ready replacement for Get-ChildItem, but I wanted the –LIVE version to proxy the parameters as seamlessly as possible.

To do this, we first get the command as typed by the user:

$newCommand = $myInvocation.Line(…

And replace the current script’s name with “get-childitem”:

(…).Replace($myInvocation.InvocationName, “get-childitem”)

So, for example:

When we run the script like this:

C:\temp\dir-LIVE.msh . *.cs -rec -codesign

That becomes $myInvocation.Line.  $myInvocation.InvocationName becomes “c:\temp\dir-LIVE.msh”.  So we get:

Get-childitem . *.cs –rec –codesign

If you found buggy behaviour (ie: “dir-LIVE dir-LIVE.msh”), this string replacement is almost certainly at fault – as Monad does not yet support transparent proxying to other commands.

I also over-engineered this one a bit in the next stage.  If you aliased anything to “dir-LIVE.msh,” then the script does the same replacement technique for all aliases that are defined to replace dir-LIVE.msh.

And there you have it … obfuscation, a way to cope with breaking changes, and a fragile way to proxy other cmdlets.

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]