I discovered a vulnerability in Sandboxie Classic <= 5.55.13 (and Plus <= 1.0.13 with default settings) that allowed an attacker to escape the sandbox and execute arbitrary code.
The vulnerability has been assigned CNVD-2022-33480 and CVE-2022-28067.
Discovery and Root Cause
This is a rather simple (yet powerful) vulnerability, to be honest. But since it’s my first real-world vuln, I think it merits a blog post.
If you don’t want to go through the trivialities, here’s the tldr: Sandboxie didn’t hook
NtGetNextThread(), which made it possible for a sandboxed program to obtain a writable handle to the thread(s) of an unsandboxed program. And then we used that handle to manipulate an unsandboxed thread and inject data or code by inserting APCs, and finally hijacked the thread to make it run arbitrary code.
The discovery was purely by chance. One day I was wandering around on the internet, and perhaps picking up some useful information about process-related NT APIs, when bang – this page showed up in a google search. It’s the changelog of Sandboxie. One line in particular drew my attention -
Released on 25 June 2015.
- Fixed SBIE2205 Service not implemented: CloseClipboard C0000058 error caused by Windows update KB3057839
- NtGetNextProcess can be used to alter processes outside the sandbox and will now be blocked.
- A DDE change in 4.18 broke Excel running as a forced program.
- Clipboard formats that were restricted in 4.18 are supported again.
- MS Office applications are again able to print to file inside the sandbox without errors
One cannot help but exclaim, “whoa, such an ancient hole closed up so late!” (considering this API existed even on Vista)
But wait, isn’t there another API responsible for doing the same thing with threads, if my memory serves me? Turns out yes, and that twin is
NtGetNextThread(). Give it a readable handle to a process, and it will return a handle to its threads with access rights of your choice. A quick find on page for the name of this API returned nothing, which suggested to me that it could be one of those obscure undocumented NT APIs that Sandboxie didn’t take care of.
So I went to the source code of Sandboxie and searched for “
NtGetNextThread”. Still nothing. This was a good indication that Sandboxie did not hook it, and we might have a chance to take advantage of that.
Write a few lines of code, and you’ll be able to prove that a sandboxed program can call
NtGetNextThread() to get a writable (even
THREAD_ALL_ACCESS) handle to threads outside the sandbox. Now what?
We can terminate threads as we want, of course, but is that all? Can we manipulate those threads and make them run malicious code?
First Attempt: Textbook Code Injection w/ Thread Hijacking
As per usual, I tried to inject code into unsandboxed programs and hijack their threads to run the injected code by using
SetThreadContext(). But it turned out that because we had no handle to the remote process, we could not write code into its memory. Of course, Operating Systems 101 tells us virtual memory is allocated on a per-process basis, not per-thread (i.e., all the threads in a process share the same virtual memory space), so having access only to threads, and not their process, probably doesn’t give us access to their virtual memory.
Second Attempt: Code Gadgets
If we can hijack the control flow of threads, why not just make it run snippets of existing code (i.e. code gadgets) so we won’t need to inject code? Let’s try
WinExec()! It takes exactly two parameters which are the path to an executable you want to run, and a flag usually set to zero. By running on 64-bit Windows, we’ll only need to call
SetThreadContext() to set the
rdx of the remote thread (for parameter passing), and then hijack its
However, things don’t always go as expected. It turned out that for some unknown reason,
SetThreadContext() will change some volatile registers’ contents (in the target thread), including
rdx, so the parameter we set will be overwritten. Darn!
We could have turned to other code gadgets and saw if we had any luck, but that was too much trouble, and besides, when searching for the reason why
SetThreadContext() didn’t work, I found a paper which led me to my third attempt.
Third Attempt: APC Injection
Here’s the paper, published in BlackHat USA 19. It’s an analysis of process injection techniques as of 2019. It turned out to be immensely helpful, especially because it mentioned an APC injection technique that allowed you to write arbitrary data/code to remote processes without a handle to the process (but a writable handle to one of its threads is needed).
The technique is called “memset/memmove write primitive”, and was invented by the authors of the paper. The code is pretty simple so I’ll just reproduce it here:
Basically, it is just inserting APCs into the remote thread. These APCs’ entry point is
memset(), and the parameters are passed accordingly. By
memset()ing one byte at a time, we can write any data we want into the target process’s memory. The idea is: if we can’t write to the memory of the remote process, why not manipulate one of its threads and let it do the thing for us? We have control on the thread, and the thread has control on its own process’s memory!
Once we have this write primitive, code execution is not far away: write shellcode, and hijack the thread to run it. In my case, I just wanted to demonstrate the vulnerability, so I wrote a path to
calc.exe into the remote process, and then queued another APC to pass the path as a parameter and call
The full PoC is as follows. Also available on GitHub.
The vulnerability has been fixed in Sandboxie Classic 5.55.14 (Plus 1.0.14) by intercepting the
NtGetNextThread() call and filtering out requests to access unsandboxed threads. Prior to this version, an extra option “Enable Object Filtering” which is turned off by default could serve as a workaround. This option works by registering callbacks for handle creation, and can block a sandboxed program from obtaining handles to unsandboxed threads.
Thanks to David Xanatos, who is the current developer and maintainer of Sandboxie, for their quick response to my report and quick fix of the problem. Also thanks to Amit Klein and Itzik Kotler, who wrote the amazing paper and proposed the write primitive I used in my PoC.