Microsoft didn’t sandbox Windows Defender, so I did

Microsoft exposed their users to a lot of risks when they released Windows Defender without a sandbox. This surprised me. Sandboxing is one of the most effective security-hardening techniques. Why did Microsoft sandbox other high-value attack surfaces such as the JIT code in Microsoft Edge, but leave Windows Defender undefended?

As a proof of concept, I sandboxed Windows Defender for them and, am now open sourcing my code as the Flying Sandbox Monster. The core of Flying Sandbox Monster is AppJailLauncher-rs, a Rust-based framework to contain untrustworthy apps in AppContainers. It also allows you to wrap the I/O of an application behind a TCP server, allowing the sandboxed application to run on a completely different machine, for an additional layer of isolation.

In this blog post, I describe the process and results of creating this tool, as well as thoughts about Rust on Windows.

Flying Sandbox Monster running Defender in a sandbox to scan a WannaCry binary.

The Plan

Windows Defender’s unencumbered access to its host machine and wide-scale acceptance of hazardous file formats make it an ideal target for malicious hackers. The core Windows Defender process, MsMpEng, runs as a service with SYSTEM privileges. The scanning component, MpEngine, supports parsing an astronomical number of file formats. It also bundles full-system emulators for various architectures and interpreters for various languages. All of this, performed with the highest level of privilege on a Windows system. Yikes.

This got me thinking. How difficult would it be to sandbox MpEngine with the same set of tools that I had used to sandbox challenges for the CTF community two years ago?

The first step towards a sandboxed Windows Defender is the ability to launch AppContainers. I wanted to re-use AppJailLauncher, but there was a problem. The original AppJailLauncher was written as a proof-of-concept example. If I had any sense back then, I would’ve written it in C++ Core rather than deal with the pains of memory management. Over the past two years, I’ve attempted rewriting it in C++ but ended up with false starts (why are dependencies always such a pain?).

But then inspiration struck. Why not rewrite the AppContainer launching code in Rust?

Building The Sandbox

A few months later, after crash coursing through Rust tutorials and writing a novel of example Rust code, I had the three pillars of support for launching AppContainers in Rust: SimpleDacl, Profile, and WinFFI.

  • SimpleDacl is a generalized class that handles adding and removing simple discretionary access control entries (ACE) on Windows. While SimpleDacl can target both files and directories, it has a few setbacks. First, it completely overwrites the existing ACL with a new ACL and converts inherited ACEs to “normal” ACEs. Also, it disregards any ACEs that it cannot parse (i.e. anything other than AccessAllowedAce and AccessDeniedAce. Note: we don’t support mandatory and audit access control entries.).
  • Profile implements creation of AppContainer profiles and processes. From the profile, we can obtain a SID that can be used to create ACE on resources the AppContainer needs to access.
  • WinFFI contains the brunt of the functions and structures winapi-rs didn’t implement as well as useful utility classes/functions. I made a strong effort to wrap every raw HANDLE and pointer in Rust objects to manage their lifetimes.

Next, I needed to understand how to interface with the scanning component of Windows Defender. Tavis Ormandy’s loadlibrary repository already offered an example C implementation and instructions for starting an MsMpEng scan. Porting the structures and function prototypes to Rust was a simple affair to automate, though I initially forgot about array fields and function pointers, which caused all sorts of issues; however, with Rust’s built-in testing functionality, I quickly resolved all my porting errors and had a minimum test case that would scan an EICAR test file.

The basic architecture of Flying Sandbox Monster.

Our proof-of-concept, Flying Sandbox Monster, consists of a sandbox wrapper and the Malware Protection Engine (MpEngine). The single executable has two modes: parent process and child process. The mode is determined by the presence of an environment variable that contains the HANDLEs for the file to be scanned and child/parent communication. The parent process populates these two HANDLE values prior to creating an AppContainer’d child process. The now-sandboxed child process loads the malware protection engine library and scans the input file for malicious software.

This was not enough to get the proof-of-concept working. The Malware Protection Engine refused to initialize inside an AppContainer. Initially, I thought this was an access control issue. After extensive differential debugging in ProcMon (comparing AppContainer vs non-AppContainer execution), I realized the issue might actually be with the detected Windows version. Tavis’s code always self-reported the Windows version as Windows XP. My code was reporting the real underlying operating system; Windows 10 in my case. Verification via WinDbg proved that this was indeed the one and only issue causing the initialization failures. I needed to lie to MpEngine about the underlying Windows version. When using C/C++, I would whip up a bit of function hooking code with Detours. Unfortunately, there was no equivalent function hooking library for Rust on Windows (the few hooking libraries available seemed a lot more “heavyweight” than what I needed). Naturally, I implemented a simple IAT hooking library in Rust (32-bit Windows PE only).

Introducing AppJailLauncher-rs

Since I had already implemented the core components of AppJailLauncher in Rust, why not just finish the job and wrap it all in a Rust TCP server? I did, and now I’m happy to announce “version 2” of AppJailLauncher, AppJailLauncher-rs.

AppJailLauncher was a TCP server that listened on a specified port and launched an AppContainer process for every accepted TCP connection. I tried not to reinvent the wheel, but mio, the lightweight IO library for Rust, just didn’t work out. First, mio’s TcpClient did not provide access to raw “socket HANDLEs” on Windows. Second, these raw “socket HANDLEs” were not inheritable by the child AppContainer process. Because of these issues, I had to introduce another “pillar” to support appjaillauncher-rs: TcpServer.

TcpServer is responsible for instantiating an asynchronous TCP server with a client socket that is compatible with STDIN/STDOUT/STDERR redirection. Sockets created by the socket call cannot redirect a process’s standard input/output streams. Properly working standard input/output redirection requires “native” sockets (as constructed via WSASocket). To allow the redirection, TcpServer creates these “native” sockets and does not explicitly disable inheritance on them.

My Experience with Rust

My overall experience with Rust was very positive, despite the minor setbacks. Let me describe some key features that really stood out during AppJailLauncher’s development.

Cargo. Dependency management with C++ on Windows is tedious and complex, especially when linking against third-party libraries. Rust neatly solves dependency management with the cargo package management system. Cargo has a wide breadth of packages that solve many common-place problems such as argument parsing (clap-rs), Windows FFI (winapi-rs et. al.), and handling wide strings (widestring).

Built-in Testing. Unit tests for C++ applications require a third-party library and laborious, manual effort. That’s why unit test are rarely written for smaller projects, like the original AppJailLauncher. In Rust, unit test capability is built into the cargo system and unit tests co-exist with core functionality.

The Macro System. Rust’s macro system works at the abstract syntax tree (AST) level, unlike the simple text substitution engine in C/C++. While there is a bit of a learning curve, Rust macros completely eliminate annoyances of C/C++ macros like naming and scope collisions.

Debugging. Debugging Rust on Windows just works. Rust generates WinDbg compatible debugging symbols (PDB files) that provide seamless source-level debugging.

Foreign Function Interface. The Windows API is written in, and meant to be called from, C/C++ code. Other languages, like Rust, must use a foreign function interface (FFI) to invoke Windows APIs. Rust’s FFI to Windows (the winapi-rs crate) is mostly complete. It has the core APIs, but it is missing some lesser used subsystems like access control list modification APIs.

Attributes. Setting attributes is very cumbersome because they only apply to the next line. Squashing specific code format warnings necessitates a sprinkling of attributes throughout the program code.

The Borrow Checker. The concept of ownership is how Rust achieves memory safety. Understanding how the borrow checker works was fraught with cryptic, unique errors and took hours of reading documentation and tutorials. In the end it was worth it: once it “clicked,” my Rust programming dramatically improved.

Vectors. In C++, std::vector can expose its backing buffer to other code. The original vector is still valid, even if the backing buffer is modified. This is not the case for Rust’s Vec. Rust’s Vec requires the formation of a new Vec object from the “raw parts” of the old Vec.

Option and Result types. Native option and result types should make error checking easier, but instead error checking just seems more verbose. It’s possible to pretend errors will never exist and just call unwrap, but that will lead to runtime failure when an Error (or None) is inevitably returned.

Owned Types and Slices. Owned types and their complementary slices (e.g. String/str, PathBuf/Path) took a bit of getting used to. They come in pairs, have similar names, but behave differently. In Rust, an owned type represents a growable, mutable object (typically a string). A slice is a view of an immutable character buffer (also typically a string).

The Future

The Rust ecosystem for Windows is still maturing. There is plenty of room for new Rust libraries to simplify development of secure software on Windows. I’ve implemented initial versions of a few Rust libraries for Windows sandboxing, PE parsing, and IAT hooking. It is my hope that these are useful to the nascent Rust on Windows community.

I used Rust and AppJailLauncher to sandbox Windows Defender, Microsoft’s flagship anti-virus product. My accomplishment is both great and a bit shameful: it’s great that Windows’ robust sandboxing mechanism is exposed to third-party software. It’s shameful that Microsoft hasn’t sandboxed Defender on its own accord. Microsoft bought what eventually became Windows Defender in 2004. Back in 2004 these bugs and design decisions would be unacceptable, but understandable. During the past 13 years Microsoft has developed a great security engineering organization, advanced fuzzing and program testing, and sandboxed critical parts of Internet Explorer. Somehow Windows Defender got stuck back in 2004. Rather than taking Project Zero’s approach to the problem by continually pointing out the symptoms of this inherent flaw, let’s bring Windows Defender back to the future.

17 thoughts on “Microsoft didn’t sandbox Windows Defender, so I did

  1. Awesome work! Great to see the community stepping up to fill the gaps MS and others leave. How long did this effort take, seems like it wouldn’t have been trivial. Have you tested the battery of functionality on Defender and confirmed it doesn’t break? (Said another way, is this a proof of concept implementation or could individuals/organizations actually go and use what you developed as is?)

    • The git commit logs are a semi-accurate measure of the time required to develop both codebases. It took 2-3 weekends for a single person.

      Yes, we have tested it and it works. You can see a demo in the gif at the top of this post where it detects WannaCry.

  2. I’ve implemented initial versions of a few Rust libraries for Windows sandboxing, PE parsing, and IAT hooking

    You have my interest. Is there anything about your PE-parsing code that would prevent it from running on a Linux machine? …and, if not, have you published it as a crate I could use?

    I’m working on a hybrid Rust+Python3 (Py3 for the Qt5 QWidget bindings) game launcher as a platform for experimenting with pushing the limits of autodetection and I welcome anything which would reduce the amount of code I have to reinvent to do things like telling apart the various flavours of EXE (MZ-only/NE/PE/CLR), extracting the VERSIONINFO resource, extracting icon resources, etc.

    (eg. So I can do things like using the flavour of EXE to pre-fill the “Run in DOSBox/Wine/etc.” selector and automatically guessing the launcher title from the “ProductName” VERSIONINFO field, assuming that proves to be worthwhile once I actually run said code against my test corpus.)

      • The PE parsing code in Flying Sandbox Monster is very minimal, it only includes enough code for our proof of concept to work.

        I see what you mean and, given all those uses of unsafe, I think I’d rather just reinvent PE parsing myself instead of extending that (probably using Nom, though I have written partial parsers for formats like GIF and RAR manually in the past).

        You may be interested in pe-parse, our lightweight PE parsing library written in C++. It has Python bindings.

        Unfortunately, pe-parse is unsuitable, both because it doesn’t advertise support for building on Windows and because I don’t want to complicate my build process.

        (Currently, building a self-contained redistributable bundle should be as simple as using setuptools-rust to integrate cargo with a tool like cx_Freeze or py2exe which runs build and then gathers the tree of imports without caring whether bits like Qt and PyQt came precompiled.)

      • > Unfortunately, pe-parse is unsuitable, both because it doesn’t advertise support for building on Windows and because I don’t want to complicate my build process.

        pe-parse has zero dependencies and it builds on Windows.

      • pe-parse has zero dependencies

        pe-parse depends on cmake and, even if cmake were guaranteed to already be present, integrating cmake into cargo build is non-trivial and something more that could go wrong on configurations I neglect to test.

        (And, given that some of my machines are esoteric combinations like “non-Android ARMv7 with softfloat ABI for legacy compatibility”, I have a strict policy of not allowing precompiled libraries as a way to cheat my “must be easy for others to build” requirements.)

        Also, if you omit the Python binding layer, the backend code is currently pure Rust. As soon as I add a single native component, it becomes more complicated to ensure builds complete successfully. (Rust has much more forgiving build requirements if the only thing it’s using the C compiler for is the final link against libc. For example, rustup target add i686-unknown-linux-musl doesn’t require sudo apt-get install musl* for pure-Rust projects. )

        …plus, I forgot to mention that it’d be something I want to use in the GUI-agnostic backend, which means I’d need to write Rust bindings to pe-parse. (When I’m done, the Python will just be a shim to work around the immature and incomplete Rust bindings for Qt’s QWidget API.)

        and it builds on Windows

        I was referring to the fact that the README doesn’t give instructions for Windows when Windows is the platform I know the least about coding on.

        As a Python/PHP/JS/Bourne/etc. programmer with only the bare minimum “took a couple courses for my Bachelor’s degree” experience in C and C++ (and I did that under Linux), I’m not willing to take responsibility for integrating a project that considers it acceptable to hand-hold the least on the platform where I’d need it the most.

        (This project began as a pure Python+PyQt thing all and I turned to rust-cpython because it’s allowing me to make my code safer by migrating an increasing amount of the project to Rust’s stricter type system without having to think about the Python C API.)

      • I appreciate the effort, but, after whipping up some quick test code to run them against my “basic/redistributable” corpus of test EXEs, none of them will be suitable.

        goblin is the closest to what I need, but panics on unwrap() in some of my test EXEs, which means I’d find it more worry-inducing than just writing a new parser with Nom. (I’m not going to go through the hassle of auditing a non-trivial parser crate, which I have no familiarity with, for panics.)

        pe appears either barely maintained or abandoned (last commit just under a year ago), the single example is next to useless, the only API docs are the automatic ones that you can ask for even if the developer doesn’t know they exist (the bare minimum that rustdoc generates from the type signatures), and the code implements a lot of things I don’t think I’ll need and doesn’t even bother to do things like deriving/implementing Display or Debug.

        pelite expects you to already know whether you’re dealing with 32-bit PE or 64-bit PE and, by this time, I was so demoralized that I decided to put off testing whether any of them would be useful as a second stage after a separate “identify EXE type” parser.

        Given that none of them report enough information to tell apart MZ, NE, or something else in their failure messages, I’ll need to write the code to parse enough headers to identify MZ vs. NE vs. PE anyway, so I suspect it’ll be easier to just extend my parser to also extract the relevant fields from NE (Windows 3.1) and PE files, rather than pulling in a whole new crate just for PE.

  3. You rock! I hope that in sime future update Microsoft will implement your idea. All the best to you, have a nice day!

  4. Pingback: 2017 in review | Trail of Bits Blog

  5. Pingback: Introducing windows-acl: working with ACLs in Rust | Trail of Bits Blog

Leave a Reply