Job Control with CreateProcessWithLogonW

2019-05-21 14:38发布

问题:

An application I'm writing requires the execution of potentially malicious code to be executed on a host system. The code only interacts with stdin, stdout, and stderr, and should not attempt to interact with the filesystem or network.

I've restricted network access through a firewall rule, and filesystem access through running the process as an unprivileged user created through NetUserAdd with CreateProcessWithLogonW. Finally, I assign the process to a job object that limits memory and active processes.

This works fine on Windows 8, but when I tested it on a Windows 7 machine (the deployment platform), I found that AssignProcessToJobObject failed with an access denied, despite running as administrator. From this answer, I found that

CreateProcessWithLogonW executes the new process as a child of the Secondary Logon service, which has the outcome of making the process escape any Job Object membership/restrictions even if the Job Object did not allow breakaway.

Furthermore, the Secondary Logon service automatically creates its own new Job Object and assigns the new process into it.

So while this works on Windows 8 which allows nested job objects, it fails on Windows 7 and under.

The same answer suggests spawning an agent process under the Secondary Logon service and using it to spawn the process with the CREATE_BREAKAWAY_FROM_JOB flag. However, when attempting this, the agent's CreateProcess call fails with 5 ERROR_ACCESS_DENIED, because the job Secondary Logon puts the agent in does not allow breakaways.

How can I assign a process created under another user to a job object on Windows 7?

回答1:

OK, I've done some experimentation, and can confirm that both CreateProcessWithLogonW() and CreateProcessWithTokenW() put the newly created process into a job object.

However, CreateProcessAsUser() does not. So this is probably the best workaround, though it does require the "Replace a process level token" privilege. You can either run the code from a context that already has this privilege (i.e., a service configured to run as local service or network service) or you can grant the privilege to the user account that will be used to run the code.

See also Shattering the myths of Windows security at the Invisible Things Lab's blog (and in particular the linked whitepaper) which describes various issues with running potentially malicious code in a limited user account.



回答2:

In my scenario, I only needed to assign some limits on the process (memory limit & active process limit), and not make any further use of the spawned process' job object. Since all CreateProcess variants (CreateProcessWithLogon, CreateProcessWithToken, etc) seem to end up spawning the process under the Secondary Logon Service which assigns the process to a job, I reasoned that as long as I can get a handle to the service's job, I could impose whatever limits I wanted.

It would be convenient to call OpenJobObject to fetch the Logon Service's job object, but the latter creates the job with a null name. Therefore, I resorted to enumerating the Logon Service's handles, and checking which job handle is the right one through IsProcessInJob. It sounds dirty, sure, and I'm not certain its all too reliable, but following this post on sysinternals (as recommended per this answer) finding the right handle did not turn out to be too hard. Note that SeDebugPrivilege is required for this step to work.

After that, it was a simple matter of imposing a JobObjectExtendedLimitInformation on the job through SetInformationJobObject. For good measure, I also added a JobObjectBasicUIRestrictions to at least make it bit harder for an attacker.

And that's all!

However, as @Harry Johnston made clear, this form of sandboxing is not impervious. But it's good enough in my case.