// Used to set a cookie. The cookie is set asynchronously, but will be
// available to a subsequent ViewHostMsg_GetCookies request.
IPC_MESSAGE_ROUTED3(ViewHostMsg_SetCookie,
GURL /* url */,
GURL /* first_party_for_cookies */,
std::string /* cookie */)
// Used to get raw cookie information for the given URL. This may be blocked
// by a user prompt to validate a previous SetCookie message.
IPC_SYNC_MESSAGE_ROUTED2_1(ViewHostMsg_GetRawCookies,
GURL /* url */,
GURL /* first_party_for_cookies */,
std::vector<webkit_glue::webcookie></webkit_glue::webcookie>
/* raw_cookies */)
// Used to delete cookie for the given URL and name
IPC_SYNC_MESSAGE_CONTROL2_0(ViewHostMsg_DeleteCookie,
GURL /* url */,
std::string /* cookie_name */)
We will revisit these macros a bit later on, but for now we can see that cross-referencing all the defined message type classes from the above-mentioned files with where they are associated with a callback function (ie. where the message class is used as a parameter to the IPC_MESSAGE_HANDLER() macro), we can find the implementation of each routine exposed over IPC, what data types they take, and what data they return. (We will discuss the marshalling of such parameters shortly.) Therefore, we are able to enumerate the exposed function attack surface and review each callback for possible flaws. For each of these functions, we must be mindful of typical vulnerabilities related to integer manipulation, dangerous memory management patterns (use after free etc), and standard buffer or pointer-related errors.
Example 1 - Worker Process Message Forwarding
Here is an example of a memory manipulation error that can be triggered in the browser process by a sandboxed renderer process. Chrome has the notion of 'Worker Processes' (which are also sandboxed) which renderers may start by sending the browser a ViewHostMsg_CreateWorker message. It may then subsequently send arbitrary messages destined for the worker process that are routed through the privileged browser process. The code to perform this forwarding is implemented in the WorkerProcessHost::OnMessageReceived() function (from chrome/browser/worker_host/worker_process_host.cc):
for (Instances::iterator i = instances_.begin(); i != instances_.end(); ++i) {
if (i->worker_route_id() == message.routing_id()) {
if (!i->shared()) {
// Don't relay messages from shared workers (all communication is via
// the message port).
WorkerInstance::SenderInfo info = i->GetSender();
CallbackWithReturnValue<int>::Type* next_route_id =
GetNextRouteIdCallback(info.first);
RelayMessage(message, info.first, info.second, next_route_id);
}
if (message.type() == WorkerHostMsg_WorkerContextDestroyed::ID) {
instances_.erase(i);
UpdateTitle();
}
break;
}
}
Assuming an appropriate worker process has been found, the RelayMessage() function is called:
void WorkerProcessHost::RelayMessage(const IPC::Message& message,
IPC::Message::Sender* sender,
int route_id,
CallbackWithReturnValue<int>::Type* next_route_id)
<int>{
if (message.type() == WorkerMsg_PostMessage::ID) {
// We want to send the receiver a routing id for the new channel, so
// crack the message first.
string16 msg;
std::vector<int> sent_message_port_ids;
std::vector<int> new_routing_ids;
if (!WorkerMsg_PostMessage::Read(
&message, &msg, &sent_message_port_ids, &new_routing_ids)) {
return;
}
DCHECK(sent_message_port_ids.size() == new_routing_ids.size());
for (size_t i = 0; i < sent_message_port_ids.size(); ++i) {
new_routing_ids[i] = next_route_id->Run();
MessagePortDispatcher::GetInstance()->UpdateMessagePort(
sent_message_port_ids[i], sender, new_routing_ids[i], next_route_id);
}
The code here reads two vectors from the input message and proceeds to fill one of them (
new_routing_ids) out. The loop featured above will write elements in to
new_routing_ids according to the size of
send_message_port_ids. Therefore, if
new_routing_ids is smaller than
sent_message_port_ids, then this loop will end up writing data to an out of bounds memory location, resulting in a buffer overflow. This bug of course hinges on the fact that the vector operator[] function does not do validation on the index passed to it, which is the case for g++, but not for Microsoft STL implementations. This bug was fixed in February of this year (
http://src.chromium.org/viewvc/chrome?view=rev&revision=38209).
Note
The code above appears to do a sanity check with the DCHECK() line preceding the loop, however closer inspection will reveal that DCHECK() assertions are only present in debug builds, and are not compiled in to release builds.
In addition to memory corruption, we must also keep in mind the higher-level consequences of being able to randomly call any of these functions:
- Is there a way to call certain functions in an unexpected way due to an expected sequence of IPC calls?
- Does the function perform any operation that would represent a potential security problem? For example, will it let us write arbitrary files to locations that we couldn't normally write to due to sandbox restrictions?
Example 2 - Clipboard Type Confusion Vulnerability
This bug is actually a type confusion vulnerability but stems from an unexpected invocation of one of the standard clipboard messages that a renderer may send a browser. The browser exposes two functions for writing to the clipboard - ViewHostMsg_ClipboardWriteObjectsSync and ViewHostMsg_ClipboardWriteObjectsAsync. They are both exposed to IPC via the ResourceMessageFilter object (implemented in chrome/browser/renderer_host/resource_message_filter.cc). As their respective names imply, they are used to write objects to the clipboard - with the former function performing a synchronous write and the latter performing an asynchronous one. In each case, the parameter passed by the user is a std::map < int, vector <> >. For each pair in the map, the integer value denotes the type of object to be written to the clipboard, and the vector <> is essentially a buffer containing the contents. The function that stores the data in the clipboard is the Clipboard::DispatchObject() function, which performs sanity checking on the data for various object types before storing them. The object type of interest to us is the CBF_SMBITMAP type:
case CBF_SMBITMAP: {
using base::SharedMemory;
using base::SharedMemoryHandle;
if (params[0].size() != sizeof(SharedMemory*))
return;
// It's OK to cast away constness here since we map the handle as
// read-only.
const char* raw_bitmap_data_const =
reinterpret_cast<const char*>(&(params[0].front()));
char* raw_bitmap_data = const_cast<char*>(raw_bitmap_data_const);
scoped_ptr<SharedMemory> bitmap_data(
*reinterpret_cast<SharedMemory**>(raw_bitmap_data));
if (!ValidateAndMapSharedBitmap(params, bitmap_data.get()))
return;
WriteBitmap(static_cast<const char*>(bitmap_data->memory()),
&(params[1].front()));
break;
}
As can be seen, the buffer contents supplied by the user is actually interpreted as a pointer to a SharedMemory object. Thus, it would seem that the user is able to specify an arbitrary pointer that is interpreted as a pointer to an object. However, this is not entirely correct. We see that in ResourceMessageFilter::OnClipboardWriteObjectsSync() (the function exposed to IPC For ViewHostMsg_ClipboardWriteObjectsSync), the buffer supplied by the user for CBF_SMBITMAP where the pointer is taken from is actually replaced with a valid pointer by the browser process.
void ResourceMessageFilter::OnClipboardWriteObjectsSync(
const Clipboard::ObjectMap& objects,
base::SharedMemoryHandle bitmap_handle)
{
DCHECK(base::SharedMemory::IsHandleValid(bitmap_handle))
<< "Bad bitmap handle";
// We cannot write directly from the IO thread, and cannot service the IPC
// on the UI thread. We'll copy the relevant data and get a handle to any
// shared memory so it doesn't go away when we resume the renderer, and post
// a task to perform the write on the UI thread.
Clipboard::ObjectMap* long_living_objects = new Clipboard::ObjectMap(objects);
// Splice the shared memory handle into the clipboard data.
Clipboard::ReplaceSharedMemHandle(long_living_objects, bitmap_handle,
handle());
ChromeThread::PostTask(
ChromeThread::UI,
FROM_HERE,
new WriteClipboardTask(long_living_objects));
}
The actual replacement is performed by Clipboard::ReplaceSharedMemHandle(). The problem is that objects of type CBF_SMBITMAP are only ever expected to appear in ViewHostMsg_ClipboardWriterObjectsSyncmessages, and not expected in ViewHostMsg_ClipboardWriteObjectsAsync messages. Therefore, the Clipboard::ReplaceSharedMemHandle() function is never called for the latter:
void ResourceMessageFilter::OnClipboardWriteObjectsAsync(
const Clipboard::ObjectMap& objects)
{
// We cannot write directly from the IO thread, and cannot service the IPC
// on the UI thread. We'll copy the relevant data and post a task to preform
// the write on the UI thread.
Clipboard::ObjectMap* long_living_objects = new Clipboard::ObjectMap(objects);
ChromeThread::PostTask( ChromeThread::UI, FROM_HERE,
new WriteClipboardTask(long_living_objects));
}
You should keep in mind that the attack surface might also include IPC functions exposed by the local unprivileged process. If the local process exposes methods that return data to a more privileged process, they might be able to supply malformed or unexpected responses that will result in vulnerabilities when being processed by the client.
Messages
So far we have looked at the functions exposed over IPC, how to enumerate them, and how to determine what parameters may be passed to them. But we skipped some important details - how exactly are these functions invoked? And how are parameters transported from one process to the other? The answer to these questions requires us to look at some of the lower level transmission details a little more closely, which we will do here.
The basic unit for transmission across IPC channels is the Message object (defined in ipc/ipc_message.h). Message objects are structured as follows.
Messages are processed in the order they are received from the communications channel by the ChannelImpl::ProcessIncomingMessage() function (implemented within ipc/ipc_channel_win.cc or ipc/ipc_channel_posix.cc depending on the target platform). This is perhaps the lowest-level input vector (save for OS-level problems) for targeting higher-privileged processes via the IPC framework. Here, we can examine how messages are received, buffered, and decapsulated. We might expect to find integer-related errors due to message lengths, out-of-state errors problems due to unexpected flag fields and such (which we didn't cover here - this is an exercise left to the reader), or poor descriptor manipulation for Unix implementations.
As we already know, once messages have been successfully received, they are routed to an appropriate callback function through the use of a routing ID and message type, both of which are present in the Message header. The IPC_MESSAGE_HANDLER() macros actually expand to a case statement based on the message type. The remainder of the message data is interpreted as a set of parameters for the called function, or for results of a called function in the case of reply messages.
Parameter Deserialization
Previously, we looked at how callback classes were associated with callback routines exposed over IPC. We also introduced the IPC_MESSAGE_ROUTED() families of macros that indicate the parameters that each function will take. These macros actually build definitions of the named callback classes based on the parameters provided to the macro. Callback classes indicated by the first parameter are all derived from IPC::Message or IPC::MessageWithTuple < > and contain key elements needed to invoke the callback function correctly. Firstly, each object contains the aforementioned static ID member that is used to match messages type IDs to desired functions, and secondly, a Dispatch() method is implemented for each class that performs the necessary parameter deserialization from the message blob and passes the resultant parameters to the destination callback function. Note that in the case of callback functions with no parameters, the IPC::Message class is used, which does not need to do any parameter deserialization. Otherwise, IPC::MessageWithTuple < > is derived from, with the data types of the expected parameters indicated as template parameters to the class.
So, the MessageWithTuple < >::Dispatch() function must unpack parameters intended for the callback function from the message data. This is achieved with the MessageWithTuple < >::Read() function, which uses several layers of templating indirection to call the correct deserialization routine. Likewise, MessageWithTuple < >::Write() will cause relevant output parameters to be written to a message for transmitting a result. The Message class derives from the Pickle class (implemented in base/pickle.cc), which contains functions for reading basic data types such integers, strings, and bools. Callbacks that take basic types as parameters essentially use these functions directly.
For more complicated data structures, unpacking and packing is achieved by calling another templatized method: ParamTraits <> ::Read() and ParamTraits <> ::Write(), where 'type' is the expected data structure being read or written. So, for every possible data structure received by a callback function, there is a matching ParamTraits <> implementation. An example for the gfx::Point object is shown:
bool ParamTraits<gfx::Point>::Read(const Message* m, void** iter,
gfx::Point* r) {
int x, y;
if (!m->ReadInt(iter, &x) ||
!m->ReadInt(iter, &y))
return false;
r->set_x(x);
r->set_y(y);
return true;
}
void ParamTraits<gfx::Point>::Write(Message* m, const gfx::Point& p) {
m->WriteInt(p.x());
m->WriteInt(p.y());
}
Most of the ParamTraits < > implementations are located within the following files:
chrome/common/common_param_traits.h
chrome/common/common_param_traits.cc
chrome/common/gpu_messages.h
chrome/common/plugin_messages.h
chrome/common/render_messages.h
chrome/common/utility_messages.h
chrome/common/webkit_param_traits.h
ipc/ipc_message_utils.cc
ipc/ipc_message_utils.h
The type deserialization routines are another broad attack surface that can be targeted by untrusted processes looking to perform privilege escalation attacks. There are a large number of data structures that contain relatively complex data structures, and they make likely candidates for memory corruption-style vulnerabilities, especially due to integer manipulation problems.
Example 3 - SkBitmap Deserialization
SkBitmap structures are used to transmit bitmap data between processes over IPC. The SkBitmap structure looks like this:
class SkBitmap {
...
uint32_t fRowBytes;
uint32_t fWidth;
uint32_t fHeight;
uint8_t fConfig;
uint8_t fFlags;
uint8_t fBytesPerPixel; // based on config
};
ParamTraits < SkBitmap > ::Read() allows a largely unchecked SkBitmap_Data structure to be used to create an SkBitmap:
bool ParamTraits<SkBitmap>::Read(const Message* m, void** iter, SkBitmap* r) {
const char* fixed_data;
int fixed_data_size = 0;
if (!m->ReadData(iter, &fixed_data, &fixed_data_size) ||
(fixed_data_size <= 0)) {
NOTREACHED();
return false;
}
if (fixed_data_size != sizeof(SkBitmap_Data))
return false; // Message is malformed.
const char* variable_data;
int variable_data_size = 0;
if (!m->ReadData(iter, &variable_data, &variable_data_size) ||
(variable_data_size < 0)) {
NOTREACHED();
return false;
}
const SkBitmap_Data* bmp_data =
reinterpret_cast<const SkBitmap_Data*>(fixed_data);
return bmp_data->InitSkBitmapFromData(r, variable_data, variable_data_size);
}
While the
allocPixels() function (called from
InitSkBitmapFromData()) protects from integer overflows, it does so by checking that
fHeight * fRowBytes do not overflow. However,
fWidth can be desynchronized from
fRowBytes (that is,
fWidth is not checked to be valid in relation to the
fHeight and
fRowBytes members), which in turn can create problems in the privileged process. For example, when the
SkBitmap is stored in a Thumbnail store (via the
ViewHostMsg_Thumbnail message), the
JPEGCodec encodes the data from the
SkBitmap, which does calculations based on the images'
fWidth value, not
fRowBytes. This bug was fixed in December of last year (
http://src.chromium.org/viewvc/chrome?view=rev&revision=35371).
In addition to memory corruption-style vulnerabilities, it is important to keep an eye out for data structures that maintain some sort of internal state information. In this case, it might be possible to alter that state in an unexpected way and cause processing vulnerabilities due to the desynchronized structure.
Example 4 - Plugin Messages
Quite a large number of messages exchanged between the renderer process and the plugin process were found to contain data structures that had internal pointer fields. These pointers were never used by the renderer (as they are only valid in the context of the plugin process), but were transmitted back and forth as part of a way to maintain state of persistent data structures. One such example is NPVariant_Params structure defined in chrome/common/plugin_messages.h. This data structure was defined as:
struct NPVariant_Param {
NPVariant_ParamEnum type;
bool bool_value;
int int_value;
double double_value;
std::string string_value;
int npobject_routing_id;
intptr_t npobject_pointer;
};
Essentially, it is used to transmit an NPVariant object between processes as part of invocation or property getting/setting of plugin scriptable objects. The NPVariant data structure is read using ParamTraits < NPVariant_Param > ::Read():
static bool Read(const Message* m, void** iter, param_type* r) {
... code ...
} else if (r->type == NPVARIANT_PARAM_OBJECT_ROUTING_ID) {
result =
ReadParam(m, iter, &r->npobject_routing_id) &&
ReadParam(m, iter, &r->npobject_pointer);
} else if (r->type == NPVARIANT_PARAM_OBJECT_POINTER) {
result = ReadParam(m, iter, &r->npobject_pointer);
} else if ((r->type == NPVARIANT_PARAM_VOID) ||
(r->type == NPVARIANT_PARAM_NULL)) {
result = true;
} else {
NOTREACHED();
}
return result;
}
As can be seen, the NPVariant_Params passed as arguments to Invoke() can contain arbitrary pointers if they are of type NPVARIANT_PARAM_OBJECT_ROUTING_ID or NPVARIANT_PARAM_OBJECT_POINTER. These pointers are subsequently treated as valid NPObject pointers when passed to the destination plugin function. This can easily lead to arbitrary execution.
Handle Sharing
You will notice that there are numerous instances where handles to objects are shared across processes. For Windows versions of Chrome, passing handles to other processes is typically achieved using the DuplicateHandle() function, and then passing the resultant HANDLE value to the remote process. For Unix, socket descriptors are passed over the local Unix socket using SCM_RIGHTS ancillary messages. The descriptors passed over the sockets are then referenced by index from the array of socket descriptors received by the same message. Most of the socket descriptor sanitation takes place in the ChannelImpl::ProcessIncomingMessage() function that was mentioned earlier. Passing resources between processes needs to be done quite carefully, as potentially sensitive resources are being handed to privileges of lesser privilege. I found an example of this type of problem, however the Google Chrome security team had already found it and patched it by the time I reported it. Still, it makes a good case study, so we will briefly mention it here.
Example 5 - Database File Manipulation
Chrome provides a number of methods to manipulate database files, exposed through the DatabaseDispatcherHost object(implemented in chrome/browser/renderer_host/database_dispatcher_host.cc). Several of these messages resulted in the opening of a database file of the callers choosing, although the database file given would be pre-pended with a fixed database path. The work for generating the filename is implemented within DatabaseUtil::GetFullFilePathForVfsFile() and DatabaseUtil::CrackVfsFilename (both implemented in webkit/database/database_util.cc).
Conclusion
The IPC framework is a large and ripe attack surface. Hopefully this article has helped to demystify some of its inner workings and provided a rough guide to how one might go about auditing it. Tune in for my final post where I will discuss the sandbox implementation itself, how it works, and also provide some examples of vulnerabilities that were uncovered there.
Will part 3 be published any soon?
Was part 3 ever published? I'm curious to read that together with Dino Blazakis's paper on OS X sandboxing (http://www.semantiscope.com/research/BHDC2011/BHDC2011-Paper.pdf).
Hi Mark,
how are you, man? first of all, congratulations by the awesome article! So, I'm trying to study the Chrome sandbox architecture. Have you some reference for help me? Maybe you already have something for the third part of these posts about Chrome. i'm looking for everything, before go to directly the code. : ]
[]'s
Marcos Álvares
Still hanging out for part 3 ;)
I'm really looking forward to part 3!
+1 I'm waiting for part3.
I'm looking forward to part 3 as well. I am not Mark Dowd though.