Practical Windows Sandboxing – Part 3
The third tool we need in order to create a sandboxed app is a desktop. We've said in many places that the desktop is a security boundary. Unfortunately, there's little real security within a desktop – and this isn't something unique to Windows – the X Window System has some of the same class of issues, though the details vary quite a bit. Prior to the 'discovery' of what's come to be known as "Shatter" attacks, people have recognized problems inherent in having windows on the same desktop hosted by apps with different privilege levels. In fact, KB article 115825, dated from 1994, calls out this issue. I find the fact that window handles are not securable kernel objects a very unfortunate omission back when Windows NT first shipped.
If we read the documentation for CreateRestrictedToken, it tells us to place our new application on another desktop in order to prevent attacks around window messages. If you look at some of the APIs available, we find that PostMessage won't work across desktops, and neither does PostThreadMessage (fixed because of a bug I filed a few years ago). The job object can be used to prevent SwitchDesktop, which actually only keeps something from switching the desktop you see to that app's desktop, so that's not quite as useful as it first seems. So you create a new desktop, pass the name of the desktop on the lpDesktop parameter of the STARTUP_INFO struct for CreateProcessAsUser, and now we ought to be secure, right?
If only this were so – that's what I'd thought, but we've got some amazingly good security testers in the Office TWC Security test team, led by Tom Gallagher. In particular, there's one I call "evil tester" who sorted out an especially nasty attack. As it turns out, SetThreadDesktop can be used for a thread to switch to any desktop that the process can obtain a handle for, with the only restriction that the desktop has to be in the current window station for the process. Just making another window station won't help, either – the process could call SetProcessWindowStation. It would seem at first glance that the bits in the job object that control desktops ought to prevent this, but currently that's not the case. So how can a desktop be a security boundary if there's no way to prevent a process from coming back? And once it does come back, SetWindowsHook means that the restricted process has just become very non-restricted.
Initially, I thought I was completely foiled. My first thought was that if SetThreadDesktop needs a handle, maybe the handle requires some sort of permission that I can deny. No such luck. Any handle will do, no matter how few permissions are available. Next, what's the ACL on the desktop and window station? This was even more annoying – the ACL is:
Logon ID SID:F
This is just ridiculous – as it turns out, an app really only needs about 3 bits of the access mask to make a window on a desktop and run, so granting Restricted:F is excess privilege. What's even worse is that I (or someone in the security group) should have caught this in the original Windows security push. To make things even more annoying, you can't disable the logon ID SID and have a viable application, and you _have_ to have Restricted in the SidsToRestrict. Now what? "Desktop is a security boundary" – yeah, right. SetThreadDesktop, SetWindowsHook, badda-bing, crash, thud. Feh. Under normal conditions, it isn't a huge hole until someone tries to make a sandboxed app.
John Lambert of SWI came to the rescue with a particularly interesting trick. John knows ACLs about as well as anyone here, and came up with a brilliant solution. You may recall that I pointed out that you can put any valid SID in SidsToRestrict, even one that doesn't actually map to any real user or group. Now we make up a completely random SID and add it to our list of SidsToRestrict. You then grab the ACL for the current desktop that we don't want the restricted app to get back onto, and insert a deny:F ACE for the SID we just made up. The restricted process now can't get a handle at all, no other processes are affected, and without a handle SetThreadDesktop fails.
The moral of the story is that excepting dirty tricks like John thought up, the desktop isn't truly a security boundary – what's the boundary is the window station, and to be precise, it's the logon ID. If a process has a logon ID that isn't associated with any other processes, you create a window station and desktop to stick that process on, then it's completely locked down with respect to window message type attacks. Under normal conditions, there's one window station per logon ID, and one desktop per window station, so to be charitable, it's typically true that the desktop is a security boundary, and if you do something silly to put a highly privileged window on a user's desktop, you are creating security holes. However, if we're dealing with a highly restricted application, it isn't enough to just place it on a different desktop – you have to keep it from coming back.
With Vista, there's also some level of restrictions on window messages between integrity levels, and that may allow you to run a restricted app on the same desktop without too many opportunities for mayhem – SetWindowHook is blocked, along with a bunch of other things.
This concludes the long version of the talk I'll be giving Thursday – as you can see, there's no way all this will fit in 20 minutes of turbo talk.