Aug 13, 2010

Playing in the Chrome Sandbox

by Nephi Johnson

Used 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:

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.

Tags:
blog comments powered by Disqus