Playing in the Chrome Sandbox
by Nephi JohnsonUsed in a security context, the term sandbox refers to a mechanism used to segregate untrusted processes in such a way that renders the process harmless. There has been a lot of talk about sandboxes recently and it seems like more and more of the big applications are starting to use them. For example, Google Chrome runs every tab in a sandbox, Firefox runs its plugins within a sandbox, Microsoft Office 2010 uses a sandbox in its "Protected View," and the next version of Adobe Reader will also have a sandbox. I'm sure there are many more applications out there that either already run untrusted code within a sandbox, or are planning on doing something similar.
Sandboxes sound great. In fact, they are great. If an attacker is able to compromise an application that is running in a sandbox, the sandbox can make it very difficult for him to be able to do anything useful, even though he can execute arbitrary code. People are not perfect, however, and thus the sandboxes people create can't purposefully be perfect. That being said, existing sandbox implementations are quite robust and have excellent ideas. One of the most popular sandboxes is the one found in Google Chrome, which I have learned a lot about over the past few months.
Two months ago, I had studied up on the Google Chrome sandbox implementation in preparation for a short talk at a local infosec meeting (AHA!). Shortly thereafter, Mark Dowd posted the first of a three-part series on Google Chrome's sandbox. And most recently, I had the opportunity to go to REcon in Montreal with the rest of the BreakingPoint security team. During REcon one of my favorite talks was by Stephen Ridley, who talked about the Google Chrome sandbox and how he used a tool he created, Sandkit, to inject a python interpreter into a Chrome target process. Since Chrome's sandbox is the one I'm the most familiar with, it is the one I will use as an example.
NOTE: If you would like to download the Google Chrome source code via subversion, run this: "svn co http://src.chromium.org/svn/trunk/src chrome_src". I pulled all of the examples from r43477. If you would like to download that specific revision so all of the line numbers match up, run this instead: "svn co -r 43477 http://src.chromium.org/svn/trunk/src chrome_src".
A Quick Chrome Overview
Google Chrome has a multi-process architecture that is made up of two main parts: a broker and target(s). Simply put, the broker manages each of the targets, performing system calls and functions that it deems are safe on their behalf. In more day-to-day terms, the broker is the browser, and each of the targets is a tab (aka Renderer) in the browser. In a general sense, this diagram shows how the broker and targets interact:
You should notice that the targets communicate with the broker via IPC channels. This is necessary for a few reasons. First, hooks are installed into every target process, intercepting most system calls and re-routing each call through the broker, who ultimately says whether the call should be allowed. Chromium makes it very clear in their design document about the sandbox that this is not meant to provide security:
"The interception + IPC mechanism does not provide security; it is designed to provide compatibility when code inside the sandbox cannot be modified to cope with sandbox restrictions."
Secondly, the targets also need the broker in order to communicate with other services, such as plugins. The broker is able to accept messages from a target and forward them on to the correct service. A few other restrictions the broker places on newly created targets is that it creates them with a very restrictive token and job object. It also places new renderer processes into an alternate desktop, which prevents attacks such as the "shatter" attack, which I will talk about towards the end of this post. The broker is always the first process started and is the one that creates the target processes. Before a new tab is added to the tab-bar in the UI, the process for the new tab is created by making a new instance of a ChildProcessLauncher [line 339]:
// chrome/browser/renderer_host/browser_render_process_host.cc, Chrome r43477
245 bool BrowserRenderProcessHost::Init(bool is_extensions_process,
246 URLRequestContextGetter* request_context) {
...
305 if (run_renderer_in_process()) {
...
326 } else {
327 // Build command line for renderer. We call AppendRendererCommandLine()
328 // first so the process type argument will appear first.
329 CommandLine* cmd_line = new CommandLine(renderer_path);
330 if (!renderer_prefix.empty())
331 cmd_line->PrependWrapper(renderer_prefix);
332 AppendRendererCommandLine(cmd_line);
333 cmd_line->AppendSwitchWithValue(switches::kProcessChannelID,
334 ASCIIToWide(channel_id));
335
336 // Spawn the child process asynchronously to avoid blocking the UI thread.
337 // As long as there's no renderer prefix, we can use the zygote process
338 // at this stage.
339 child_process_.reset(new ChildProcessLauncher(
340 #if defined(OS_WIN)
341 FilePath(),
342 #elif defined(POSIX)
343 renderer_prefix.empty(),
344 base::environment_vector(),
345 channel_->GetClientFileDescriptor(),
346 #endif
347 cmd_line,
348 this));
349
350 fast_shutdown_started_ = false;
351 }
352
353 return true;
354 }
On a side note, notice the `if` statement on line 305; the command-line switch `--single-process` would make the result of `run_renderer_in_process()` true, making all tabs run as separate threads in one process instead of as separate processes. In the constructor for the `ChildProcessLauncher` class, the Launch() function is called on a newly created ChildProcessLancher::Context instance [line 273]:
// chrome/browser/child_process_launcher.cc, Chrome r43477
262 ChildProcessLauncher::ChildProcessLauncher(
263 #if defined(OS_WIN)
264 const FilePath& exposed_dir,
265 #elif defined(OS_POSIX)
266 bool use_zygote,
267 const base::environment_vector& environ,
268 int ipcfd,
269 #endif
270 CommandLine* cmd_line,
271 Client* client) {
272 context_ = new Context();
273 context_->Launch(
274 #if defined(OS_WIN)
275 exposed_dir,
276 #elif defined(OS_POSIX)
277 use_zygote,
278 environ,
279 ipcfd,
280 #endif
281 cmd_line,
282 client);
283 }
The `Launch` function in turn queues up a call to the `LaunchInternal` function [line 62]...
// chrome/browser/child_process_launcher.cc, Chrome r43477
48 void Launch(
49 #if defined(OS_WIN)
50 const FilePath& exposed_dir,
51 #elif defined(OS_POSIX)
52 bool use_zygote,
53 const base::environment_vector& environ,
54 int ipcfd,
55 #endif
56 CommandLine* cmd_line,
57 Client* client) {
58 client_ = client;
59
60 CHECK(ChromeThread::GetCurrentThreadIdentifier(&client_thread_id_));
61
62 ChromeThread::PostTask(
63 ChromeThread::PROCESS_LAUNCHER, FROM_HERE,
64 NewRunnableMethod(
65 this,
66 &Context::LaunchInternal,
67 #if defined(OS_WIN)
68 exposed_dir,
69 #elif defined(POSIX)
70 use_zygote,
71 environ,
72 ipcfd,
73 #endif
74 cmd_line));
75 }
... which when called, calls the `StartProcessWithAccess()` function [line 104]...
// chrome/browser/child_process_launcher.cc, Chrome r43477
92 void LaunchInternal(
93 #if defined(OS_WIN)
94 const FilePath& exposed_dir,
95 #elif defined(OS_POSIX)
96 bool use_zygote,
97 const base::environment_vector& env,
98 int ipcfd,
99 #endif
100 CommandLine* cmd_line) {
...
103 #if defined(OS_WIN)
104 handle = sandbox::StartProcessWithAccess(cmd_line, exposed_dir);
105 #elif defined(OS_POSIX)
106
107 #if defined(OS_LINUX)
...
120 #endif
... which finally gets us to the code where all the action is! The first thing StartProcessWithAccess() does is determine which type of process should be created:
// chrome/common/sandbox_policy.cc, Chrome r43477
392 base::ProcessHandle StartProcessWithAccess(CommandLine* cmd_line,
393 const FilePath& exposed_dir) {
394 base::ProcessHandle process = 0;
395 const CommandLine& browser_command_line = *CommandLine::ForCurrentProcess();
396 ChildProcessInfo::ProcessType type;
397 std::string type_str = cmd_line->GetSwitchValueASCII(switches::kProcessType);
398 if (type_str == switches::kRendererProcess) {
399 type = ChildProcessInfo::RENDER_PROCESS;
400 } else if (type_str == switches::kExtensionProcess) {
401 // Extensions are just renderers with another name.
402 type = ChildProcessInfo::RENDER_PROCESS;
403 } else if (type_str == switches::kPluginProcess) {
404 type = ChildProcessInfo::PLUGIN_PROCESS;
405 } else if (type_str == switches::kWorkerProcess) {
406 type = ChildProcessInfo::WORKER_PROCESS;
407 } else if (type_str == switches::kNaClLoaderProcess) {
408 type = ChildProcessInfo::NACL_LOADER_PROCESS;
409 } else if (type_str == switches::kUtilityProcess) {
410 type = ChildProcessInfo::UTILITY_PROCESS;
411 } else if (type_str == switches::kNaClBrokerProcess) {
412 type = ChildProcessInfo::NACL_BROKER_PROCESS;
413 } else if (type_str == switches::kGpuProcess) {
414 type = ChildProcessInfo::GPU_PROCESS;
415 } else {
416 NOTREACHED();
417 return 0;
418 }
419
420 bool in_sandbox =
421 (type != ChildProcessInfo::NACL_BROKER_PROCESS) &&
422 !browser_command_line.HasSwitch(switches::kNoSandbox) &&
423 (type != ChildProcessInfo::PLUGIN_PROCESS ||
424 browser_command_line.HasSwitch(switches::kSafePlugins)) &&
425 (type != ChildProcessInfo::GPU_PROCESS);
418 }
...
508 }
Notice that processes are only put into a sandbox if it is not the broker, if the command-line doesn't have the switch '--no-sandbox', if it is an untrusted plugin, and if it is not a GPU process. If the process is deemed as not needing the sandbox, the process is launched as-is:
// chrome/common/sandbox_policy.cc, Chrome r43477
443 if (!in_sandbox) {
444 base::LaunchApp(*cmd_line, false, false, &process);
445 return process;
446 }
However, if the process does need to be sandboxed, extra security measures are added:
// chrome/common/sandbox_policy.cc, Chrome r43477
450 sandbox::TargetPolicy* policy = g_broker_services->CreatePolicy();
451
452 bool on_sandbox_desktop = false;
453 // TODO(gregoryd): try locked-down policy for sel_ldr after we fix IMC.
454 // TODO(gregoryd): do we need a new desktop for sel_ldr?
455 if (type == ChildProcessInfo::PLUGIN_PROCESS) {
456 if (!AddPolicyForPlugin(cmd_line, policy))
457 return 0;
458 } else {
459 AddPolicyForRenderer(policy, &on_sandbox_desktop);
460
461 if (type_str != switches::kRendererProcess) {
462 // Hack for Google Desktop crash. Trick GD into not injecting its DLL into
463 // this subprocess. See
464 // http://code.google.com/p/chromium/issues/detail?id=25580
465 cmd_line->AppendSwitchWithValue("ignored", " --type=renderer ");
466 }
467 }
468
469 if (!exposed_dir.empty()) {
470 result = policy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
471 sandbox::TargetPolicy::FILES_ALLOW_ANY,
472 exposed_dir.ToWStringHack().c_str());
473 if (result != sandbox::SBOX_ALL_OK)
474 return 0;
475
476 FilePath exposed_files = exposed_dir.AppendASCII("*");
477 result = policy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
478 sandbox::TargetPolicy::FILES_ALLOW_ANY,
479 exposed_files.ToWStringHack().c_str());
480 if (result != sandbox::SBOX_ALL_OK)
481 return 0;
482 }
483
484 if (!AddGenericPolicy(policy)) {
485 NOTREACHED();
486 return 0;
487 }
The first thing done to restrict the target process is to create an empty sandbox::TargetPolicy [line 450]. Then, depending on the type of process, extra policies are added to the target policy. Plugin policies are added depending on whether the plugin is trusted or untrusted. Trusted plugins have the following policies added (essentially unrestricted):
// chrome/common/sandbox_policy.cc, Chrome r43477
230 // Creates a sandbox without any restriction.
231 bool ApplyPolicyForTrustedPlugin(sandbox::TargetPolicy* policy) {
232 policy->SetJobLevel(sandbox::JOB_UNPROTECTED, 0);
233 policy->SetTokenLevel(sandbox::USER_UNPROTECTED, sandbox::USER_UNPROTECTED);
234 return true;
235 }
Untrusted plugins are given specific permissions for specific directories and registry keys, such as the temp directory, Application Data directories, and registry keys for Adobe and Macromedia:
SetJobLevel(sandbox::JOB_UNPROTECTED, 0);
242
243 sandbox::TokenLevel initial_token = sandbox::USER_UNPROTECTED;
244 if (win_util::GetWinVersion() > win_util::WINVERSION_XP) {
245 // On 2003/Vista the initial token has to be restricted if the main token
246 // is restricted.
247 initial_token = sandbox::USER_RESTRICTED_SAME_ACCESS;
248 }
249 policy->SetTokenLevel(initial_token, sandbox::USER_LIMITED);
250 policy->SetDelayedIntegrityLevel(sandbox::INTEGRITY_LEVEL_LOW);
251
252 if (!AddDirectory(base::DIR_TEMP, NULL, true,
253 sandbox::TargetPolicy::FILES_ALLOW_ANY, policy))
254 return false;
...
275 if (!AddDirectory(base::DIR_APP_DATA, L"Macromedia", true,
276 sandbox::TargetPolicy::FILES_ALLOW_ANY,
277 policy))
278 return false;
...
285 if (!AddKeyAndSubkeys(L"HKEY_CURRENT_USER\\SOFTWARE\\ADOBE",
286 sandbox::TargetPolicy::REG_ALLOW_ANY,
287 policy))
288 return false;
...
315 return true;
316 }
If the target process is a renderer, many more restrictions are applied to the target_policy:
// chrome/common/sandbox_policy.cc, Chrome r43477
352 void AddPolicyForRenderer(sandbox::TargetPolicy* policy,
353 bool* on_sandbox_desktop) {
354 policy->SetJobLevel(sandbox::JOB_LOCKDOWN, 0);
355
356 sandbox::TokenLevel initial_token = sandbox::USER_UNPROTECTED;
357 if (win_util::GetWinVersion() > win_util::WINVERSION_XP) {
358 // On 2003/Vista the initial token has to be restricted if the main
359 // token is restricted.
360 initial_token = sandbox::USER_RESTRICTED_SAME_ACCESS;
361 }
362
363 policy->SetTokenLevel(initial_token, sandbox::USER_LOCKDOWN);
364 policy->SetDelayedIntegrityLevel(sandbox::INTEGRITY_LEVEL_LOW);
365
366 bool use_winsta = !CommandLine::ForCurrentProcess()->HasSwitch(
367 switches::kDisableAltWinstation);
368
369 if (sandbox::SBOX_ALL_OK == policy->SetAlternateDesktop(use_winsta)) {
370 *on_sandbox_desktop = true;
371 } else {
372 *on_sandbox_desktop = false;
373 DLOG(WARNING) << "Failed to apply desktop security to the renderer";
374 }
375
376 AddDllEvictionPolicy(policy);
377 }
You might notice on line 363, that an initial token is passed in, as well as a lockdown token. The reasoning behind this is described on Chromiums's design document for the Chrome sandbox:
"Targets do not start executing with the restrictions specified by policy. They start executing with a token that is very close to the token the regular user processes have. The reason is that during process bootstrapping the OS loader access a lot of resources, most of them are actually undocumented and can change at any time. Also, most applications use the standard CRT provided with the standard development tools; after the process is bootstrapped the CRT needs to initialize as well and there again the internals of the CRT initialization are undocumented."
On line 369, the alternate desktop is set for the renderer. As I had mentioned before, placing target processes in a separate desktop prevents attacks such as the shatter-attack, which utilizes message passing between applications to achieve privilege escalation. This is succesful at blocking such attacks because processes can only send messages to other processes in the same desktop. The last function call, AddDllEvictionPolicy(), loops over a list of hard-coded dlls that are suspected of causing Chrome renderer processes to crash, queueing up any it finds to be unloaded at a later time. While this doesn't directly contribute to making the sandbox more secure, it does help the renderers run without crashing and is amusing to look over:
// chrome/common/sandbox_policy.cc, Chrome r43477
29 // The DLLs listed here are known (or under strong suspicion) of causing crashes
30 // when they are loaded in the renderer.
31 const wchar_t* const kTroublesomeDlls[] = {
32 L"adialhk.dll", // Kaspersky Internet Security.
33 L"acpiz.dll", // Unknown.
34 L"avgrsstx.dll", // AVG 8.
35 L"btkeyind.dll", // Widcomm Bluetooth.
36 L"cmcsyshk.dll", // CMC Internet Security.
37 L"dockshellhook.dll", // Stardock Objectdock.
38 L"GoogleDesktopNetwork3.DLL", // Google Desktop Search v5.
39 L"fwhook.dll", // PC Tools Firewall Plus.
40 L"hookprocesscreation.dll", // Blumentals Program protector.
41 L"hookterminateapis.dll", // Blumentals and Cyberprinter.
42 L"hookprintapis.dll", // Cyberprinter.
43 L"imon.dll", // NOD32 Antivirus.
44 L"ioloHL.dll", // Iolo (System Mechanic).
45 L"kloehk.dll", // Kaspersky Internet Security.
46 L"lawenforcer.dll", // Spyware-Browser AntiSpyware (Spybro).
47 L"libdivx.dll", // DivX.
48 L"lvprcinj01.dll", // Logitech QuickCam.
49 L"madchook.dll", // Madshi (generic hooking library).
50 L"mdnsnsp.dll", // Bonjour.
51 L"moonsysh.dll", // Moon Secure Antivirus.
52 L"npdivx32.dll", // DivX.
53 L"npggNT.des", // GameGuard 2008.
54 L"npggNT.dll", // GameGuard (older).
55 L"oawatch.dll", // Online Armor.
56 L"pavhook.dll", // Panda Internet Security.
57 L"pavshook.dll", // Panda Antivirus.
58 L"pctavhook.dll", // PC Tools Antivirus.
59 L"pctgmhk.dll", // PC Tools Spyware Doctor.
60 L"prntrack.dll", // Pharos Systems.
61 L"radhslib.dll", // Radiant Naomi Internet Filter.
62 L"radprlib.dll", // Radiant Naomi Internet Filter.
63 L"rlhook.dll", // Trustware Bufferzone.
64 L"r3hook.dll", // Kaspersky Internet Security.
65 L"sahook.dll", // McAfee Site Advisor.
66 L"sbrige.dll", // Unknown.
67 L"sc2hook.dll", // Supercopier 2.
68 L"sguard.dll", // Iolo (System Guard).
69 L"smum32.dll", // Spyware Doctor version 6.
70 L"smumhook.dll", // Spyware Doctor version 5.
71 L"ssldivx.dll", // DivX.
72 L"syncor11.dll", // SynthCore Midi interface.
73 L"systools.dll", // Panda Antivirus.
74 L"tfwah.dll", // Threatfire (PC tools).
75 L"wblind.dll", // Stardock Object desktop.
76 L"wbhelp.dll", // Stardock Object desktop.
77 L"winstylerthemehelper.dll" // Tuneup utilities 2006.
78 };
Going back up to the StartProcessWithAccess() function, the next thing that happens is a check for the exposed_dir parameter to the function. If this parameter is not an empty string, it gives the process full access to all files in that directory:
// chrome/common/sandbox_policy.cc, Chrome r43477
469 if (!exposed_dir.empty()) {
470 result = policy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
471 sandbox::TargetPolicy::FILES_ALLOW_ANY,
472 exposed_dir.ToWStringHack().c_str());
473 if (result != sandbox::SBOX_ALL_OK)
474 return 0;
475
476 FilePath exposed_files = exposed_dir.AppendASCII("*");
477 result = policy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
478 sandbox::TargetPolicy::FILES_ALLOW_ANY,
479 exposed_files.ToWStringHack().c_str());
480 if (result != sandbox::SBOX_ALL_OK)
481 return 0;
482 }
After all of the process-specific policies have been added, the generic policies for sandbox processes are added with a call to AddGenericPolicy():
// chrome/common/sandbox_policy.cc, Chrome r43477
193 // Adds the generic policy rules to a sandbox TargetPolicy.
194 bool AddGenericPolicy(sandbox::TargetPolicy* policy) {
195 sandbox::ResultCode result;
196
197 // Add the policy for the pipes
198 result = policy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
199 sandbox::TargetPolicy::FILES_ALLOW_ANY,
200 L"\\??\\pipe\\chrome.*");
201 if (result != sandbox::SBOX_ALL_OK)
202 return false;
203
204 if (Is64BitWindows()) {
205 result = policy->AddRule(sandbox::TargetPolicy::SUBSYS_NAMED_PIPES,
206 sandbox::TargetPolicy::NAMEDPIPES_ALLOW_ANY,
207 L"\\\\.\\pipe\\chrome.nacl.*");
208 if (result != sandbox::SBOX_ALL_OK)
209 return false;
210 }
211
212 // Add the policy for debug message only in debug
213 #ifndef NDEBUG
214 std::wstring debug_message;
215 if (!PathService::Get(chrome::DIR_APP, &debug_message))
216 return false;
217 if (!win_util::ConvertToLongPath(debug_message, &debug_message))
218 return false;
219 file_util::AppendToPath(&debug_message, L"debug_message.exe");
220 result = policy->AddRule(sandbox::TargetPolicy::SUBSYS_PROCESS,
221 sandbox::TargetPolicy::PROCESS_MIN_EXEC,
222 debug_message.c_str());
223 if (result != sandbox::SBOX_ALL_OK)
224 return false;
225 #endif // NDEBUG
226
227 return true;
228 }
Notice on lines 198-200 that permission is explicitly given to access some named pipes. These pipes are used for IPC communication with the broker. You can view all of the named pipes being used by Chrome by running Process Explorer by Sysinternals (if you'd like to look into this more, look into the ipc source directory - ipc/ipc_channel_win.cc contains the code that creates and interacts with the named pipes):
After the above call, the SpawnTarget() function is called, which actually creates the Windows token [line 246] and job object [line 251] and uses those to finally create the process:
// sandbox/src/broker_services.cc, Chrome r43477
220 ResultCode BrokerServicesBase::SpawnTarget(const wchar_t* exe_path,
221 const wchar_t* command_line,
222 TargetPolicy* policy,
223 PROCESS_INFORMATION* target_info) {
...
242 // Construct the tokens and the job object that we are going to associate
243 // with the soon to be created target process.
244 HANDLE lockdown_token = NULL;
245 HANDLE initial_token = NULL;
246 DWORD win_result = policy_base->MakeTokens(&initial_token, &lockdown_token);
247 if (ERROR_SUCCESS != win_result)
248 return SBOX_ERROR_GENERIC;
249
250 HANDLE job = NULL;
251 win_result = policy_base->MakeJobObject(&job);
252 if (ERROR_SUCCESS != win_result)
253 return SBOX_ERROR_GENERIC;
254
255 if (ERROR_ALREADY_EXISTS == ::GetLastError())
256 return SBOX_ERROR_GENERIC;
...
263 // Create the TargetProces object and spawn the target suspended. Note that
264 // Brokerservices does not own the target object. It is owned by the Policy.
265 PROCESS_INFORMATION process_info = {0};
266 TargetProcess* target = new TargetProcess(initial_token, lockdown_token,
267 job, thread_pool_);
268
269 std::wstring desktop = policy_base->GetAlternateDesktop();
270
271 win_result = target->Create(exe_path, command_line,
272 desktop.empty() ? NULL : desktop.c_str(),
273 &process_info);
...
307 }
In the call to MakeTokens(), another function (CreateRestrictedToken) is called that uses a case statement to determine which SIDs to set on the token:
// sandbox/src/restricted_token_utils.cc, Chrome r43477
21 DWORD CreateRestrictedToken(HANDLE *token_handle,
22 TokenLevel security_level,
23 IntegrityLevel integrity_level,
24 TokenType token_type) {
...
37 switch (security_level) {
...
53 case USER_NON_ADMIN: {
54 sid_exceptions.push_back(WinBuiltinUsersSid);
55 sid_exceptions.push_back(WinWorldSid);
56 sid_exceptions.push_back(WinInteractiveSid);
57 sid_exceptions.push_back(WinAuthenticatedUserSid);
58 privilege_exceptions.push_back(SE_CHANGE_NOTIFY_NAME);
59 break;
60 }
61 case USER_INTERACTIVE: {
62 sid_exceptions.push_back(WinBuiltinUsersSid);
63 sid_exceptions.push_back(WinWorldSid);
64 sid_exceptions.push_back(WinInteractiveSid);
65 sid_exceptions.push_back(WinAuthenticatedUserSid);
66 privilege_exceptions.push_back(SE_CHANGE_NOTIFY_NAME);
67 restricted_token.AddRestrictingSid(WinBuiltinUsersSid);
68 restricted_token.AddRestrictingSid(WinWorldSid);
69 restricted_token.AddRestrictingSid(WinRestrictedCodeSid);
70 restricted_token.AddRestrictingSidCurrentUser();
71 restricted_token.AddRestrictingSidLogonSession();
72 break;
73 }
74 case USER_LIMITED: {
75 sid_exceptions.push_back(WinBuiltinUsersSid);
76 sid_exceptions.push_back(WinWorldSid);
77 sid_exceptions.push_back(WinInteractiveSid);
78 privilege_exceptions.push_back(SE_CHANGE_NOTIFY_NAME);
79 restricted_token.AddRestrictingSid(WinBuiltinUsersSid);
80 restricted_token.AddRestrictingSid(WinWorldSid);
81 restricted_token.AddRestrictingSid(WinRestrictedCodeSid);
82
83 // This token has to be able to create objects in BNO.
84 // Unfortunately, on vista, it needs the current logon sid
85 // in the token to achieve this. You should also set the process to be
86 // low integrity level so it can't access object created by other
87 // processes.
88 if (win_util::GetWinVersion() >= win_util::WINVERSION_VISTA) {
89 restricted_token.AddRestrictingSidLogonSession();
90 }
91 break;
92 }
...
141 }
Back in the SpawnTarget() function, the call to MakeJobObject() is rather simple:
// sandbox/src/restricted_token_utils.cc, Chrome r43477
117 DWORD PolicyBase::MakeJobObject(HANDLE* job) {
118 // Create the windows job object.
119 Job job_obj;
120 DWORD result = job_obj.Init(job_level_, NULL, ui_exceptions_);
121 if (ERROR_SUCCESS != result) {
122 return result;
123 }
124 *job = job_obj.Detach();
125 return ERROR_SUCCESS;
126 }
After the token and job object are created, a new TargetProcess instance is made, which is then used to create the actual process:
// sandbox/src/broker_services.cc, Chrome r43477
263 // Create the TargetProces object and spawn the target suspended. Note that
264 // Brokerservices does not own the target object. It is owned by the Policy.
265 PROCESS_INFORMATION process_info = {0};
266 TargetProcess* target = new TargetProcess(initial_token, lockdown_token,
267 job, thread_pool_);
268
269 std::wstring desktop = policy_base->GetAlternateDesktop();
270
271 win_result = target->Create(exe_path, command_line,
272 desktop.empty() ? NULL : desktop.c_str(),
273 &process_info);
Notice that the target is spawned in a suspended state. The target is resumed back in the StartProcessWithAccess() function, after SpawnTarget() returns:
// chrome/common/sandbox_policy.cc, Chrome r43477 489 result = g_broker_services->SpawnTarget( 490 cmd_line->program().c_str(), 491 cmd_line->command_line_string().c_str(), 492 policy, &target); 493 policy->Release(); 494 495 if (sandbox::SBOX_ALL_OK != result) 496 return 0; 497 498 ResumeThread(target.hThread);
After the return from SpawnTarget(), the target process should be up and running in the sandbox. Another point I did not mention was how the broker process will debug each child process. This is how it knows to display the "sad-tab" page when a tab crashes. The code for this begins in the StartProcessWithAction() function:
// chrome/common/sandbox_policy.cc, Chrome r43477 440 bool child_needs_help = 441 DebugFlags::ProcessDebugFlags(cmd_line, type, in_sandbox); ... 502 // Help the process a little. It can't start the debugger by itself if 503 // the process is in a sandbox. 504 if (child_needs_help) 505 DebugUtil::SpawnDebuggerOnProcess(target.dwProcessId);
This is what SpawnDebuggerOnProcess() looks like:
// base/debug_util_win.cc, Chrome r43477
187 // Note: Does not use the CRT.
188 bool DebugUtil::SpawnDebuggerOnProcess(unsigned process_id) {
189 wchar_t reg_value[1026];
190 int len = arraysize(reg_value);
191 if (RegReadString(HKEY_LOCAL_MACHINE,
192 L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug",
193 L"Debugger", reg_value, &len)) {
194 wchar_t command_line[1026];
195 if (StringReplace(reg_value, process_id, command_line,
196 arraysize(command_line))) {
197 // We don't mind if the debugger is present because it will simply fail
198 // to attach to this process.
199 STARTUPINFO startup_info = {0};
200 startup_info.cb = sizeof(startup_info);
201 PROCESS_INFORMATION process_info = {0};
202
203 if (CreateProcess(NULL, command_line, NULL, NULL, FALSE, 0, NULL, NULL,
204 &startup_info, &process_info)) {
205 CloseHandle(process_info.hThread);
206 WaitForInputIdle(process_info.hProcess, 10000);
207 CloseHandle(process_info.hProcess);
208 return true;
209 }
210 }
211 }
212 return false;
213 }
There is one other part of the Chrome sandbox that I haven't yet talked about: how and where Chrome installs all of its hooks. Stephen Ridley also touched on this topic at REcon. He mentioned that you won't find the code that installs the hooks, but that some of the tests included with the Chrome source have examples of how this is done using the Interception Manager:
// sandbox/src/interception_unittest.cc, Chrome r43477 91 interceptions.AddToPatchedFunctions(L"ntdll.dll", "NtCreateFile", 92 INTERCEPTION_SERVICE_CALL, function, 93 OPEN_KEY_ID); 94 interceptions.AddToPatchedFunctions(L"kernel32.dll", "CreateFileEx", 95 INTERCEPTION_EAT, function, OPEN_KEY_ID); 96 interceptions.AddToPatchedFunctions(L"kernel32.dll", "SomeFileEx", 97 INTERCEPTION_SMART_SIDESTEP, function, 98 OPEN_KEY_ID); 99 interceptions.AddToPatchedFunctions(L"user32.dll", "FindWindow", 100 INTERCEPTION_EAT, function, OPEN_KEY_ID); 101 interceptions.AddToPatchedFunctions(L"kernel32.dll", "CreateMutex", 102 INTERCEPTION_EAT, function, OPEN_KEY_ID); 103 interceptions.AddToPatchedFunctions(L"user32.dll", "PostMsg", 104 INTERCEPTION_EAT, function, OPEN_KEY_ID);
As I mentioned at the beginning of this post, developers aren't perfect, and so their sandboxes are also likely to not be perfect. Here are some examples of recent security problems in the chrome sandbox:
- Command Buffer Service Int Overflow
- Clipboard IPC FileSystemDispatcher
- Insufficient validation of rename_info and length, read access violation Raw Pointer from renderer
- Logic problem Int overflow with vectors
- BitmapDeserialization
- Logic problem OOB array access forwarding messages from renderer to worker process
One of the more notable things that I liked about the Chrome sandbox is that the first design principle of the developers is to not re-invent the wheel. They made as much use out of pre-existing Windows security mechanisms as they could. As the security of Windows gets better, so should their sandbox. I think they've done a great job with it.

