Practical Windows Sandboxing, Part 2
Once you have a process in a restricted token, the next tool you can use to limit what it can do is a job object. Like restricted tokens, these shipped in Windows 2000. A job object is similar to how ulimits work on UNIX(ish) OS's, but don't do some of the things ulimits do (like a limit on the number of open handles), and does a bunch of stuff that ulimits won't do (at least last time I looked – things could have changed), as well as some very Windows-specific limitations. Basically, these are fairly simple – you create a job object by calling CreateJobObject, and then call AssignProcessToJobObject to stick a process into the job object.
Your first problem is that if the process is already running, what good is it to stick into a job object? What keeps it from doing bad stuff before we put it into a job object? Not much, but there are 2 ways to handle the problem. First is that up until the point we process user input, if we wrote the code, we ought to be able to trust the process. We could let it initialize normally, put it in a job object, and then allow it to process user input. A second approach is to create the process suspended, put it into the job object, and then call ResumeThread on the thread handle for the main thread we got back from CreateProcess (or for a restricted token, CreateProcessAsUser).
Once you have a job object, typically you'll want to set some limits. One of the most useful things to do is to associate an IO completion port with the job object, and set up a watcher thread that reacts to events in the object. For example, I could set up a CPU time limit, but since you can never be quite sure what's just taking a long time and what's a denial of service attack, just setting a hard limit will kill the process when it hits the limit. With an IO completion port, we might be able to make a more intelligent decision.
Some of the more important limits are in the JOBOBJECT_BASIC_LIMIT_INFORMATION struct. Using this, we can limit the number of active processes, memory and CPU consumption, and scheduling priority. An important flag determines whether a process in a job object can create processes outside the job object, and if we're using this for a sandbox, this certainly needs to be disallowed.
The UI restrictions are interesting – we can prevent a process from creating and switching desktops (though this won't cover quite everything), changing display settings (I'm not sure if this would work anyway, but it doesn't hurt), calling ExitWindows, which can log you off (and which could reboot the system if you hadn't already dropped that privilege), reading and writing the clipboard, and gives the job object its own global atom table. I haven't seen many exploits around messing with the global atoms, but that's probably just an accident – the global atoms represent a string table, and consistent with its old 16-bit Windows heritage, there doesn't seem to be any security associated with it. Those with long memories may recall that the kernel mode call that the global atoms used was responsible for the "getadmin" exploit, which came out July 4, 1997. Russ Cooper and I were the first ones to report that to Microsoft, which was a fun story, but I digress…
There are a couple of problems with job objects – the first is that they do not nest. Once a process is in a job object, it can't get out, and it can't be assigned to another job object. If your parent process is somehow put in a job object (which can happen on Vista), and you don't create the new process as a breakaway process, when you go to assign the process to the job object, you get ERROR_ACCESS_DENIED. Not the error I'd expect, but apparently whoever wrote AssignProcessToJobObject thought that was the only possible way for this to fail. There are a couple of actual uses for job objects in the OS, so maybe one day this will change. Another thing I see as a problem is that there isn't a way to restrict the process from using ordinary sockets to get on the network, other than the firewall APIs (depending on which OS you have).
A third problem is that one might think that the UI limits might prevent an application from getting back to the default desktop – after all, SwitchDesktop is blocked, but as we shall see, there's a problem lurking, which will be the topic of the next post.