








Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
Rutgers University Rutgers University University of Wisconsin-Madison Rutgers University. Abstract. Device drivers on commodity operating systems execute.
Typology: Lecture notes
1 / 14
This page cannot be seen from the preview
Don't miss anything!









This is an expanded version of an identically-titled paper published in ACSAC’ Proceedings of the 25th^ Annual Computer Security Applications Conference, Honolulu, Hawaii, December 2009.
Device drivers on commodity operating systems execute with kernel privilege and have unfettered access to kernel data structures. Several recent attacks demonstrate that such poor isolation exposes kernel data to exploits against vulner- able device drivers, for example through buffer overruns in packet processing code. Prior architectures to isolate kernel data from driver code either sacrifice performance, execute too much driver code with kernel privilege, or are incompat- ible with commodity operating systems. In this paper, we present the design, implementation and evaluation of a novel security architecture that bet- ter isolates kernel data from device drivers without sac- rificing performance or compatibility. In this architec- ture, a device driver is partitioned into a small, trusted kernel-mode component and an untrusted user-mode com- ponent. The kernel-mode component contains privileged and performance-critical code. It communicates via RPC with the user-mode component which contains the rest of the driver code. A RPC monitor mediates all control and data transfers between the kernel- and user-mode components. In particular, it verifies that all data transfers from the un- trusted user-mode component to the kernel-mode component preserve kernel data structure integrity. We also present a runtime technique to automatically infer such integrity spec- ifications. Our experiments with a Linux implementation of this architecture show that it can prevent compromised de- vice drivers from affecting the integrity of kernel data and do so without impacting common-case performance.
Device drivers execute with kernel privilege in most com- modity operating systems and have unrestricted access to kernel data structures. Because the kernel is part of the Trusted Computing Base (TCB) of the system, vulnerabili- ties in driver code can jeopardize the entire system. Several studies indicate that device drivers are rife with exploitable security holes. A recent study of user/kernel bugs in the Linux kernel found that 9 out of 11 of these bugs were in device drivers [23]. An audit of the Linux kernel by Coverity also found that over 50% of bugs were in device drivers [12]. Our own analysis of vulnerability databases revealed several device drivers that are vulnerable to mal-
∗Supported by NSF awards 0831268, 0915394 and 0931992.
formed input from untrusted user-space applications, allow- ing an attacker to execute arbitrary code with kernel privi- lege [4, 30]. Similarly, device drivers by their very nature copy untrusted data from devices to kernel memory. Because the kernel does not restrict the memory locations accessible to devices, a compromised driver can write arbitrary values to sensitive kernel data structures. For example, a compro- mised driver could overwrite the table of interrupt handlers in the operating system with pointers to attacker-defined code. As demonstrated by recently published exploits against wire- less device drivers in Windows XP [7, 8] and Mac OS X [27], vulnerabilities in drivers are an increasingly attractive target for attackers. Microkernels [26, 40, 42] offer one way to isolate ker- nel data from vulnerable device drivers. They execute de- vice drivers as user-mode processes and can prevent ma- licious modifications to kernel data by enforcing domain- specific rules, e.g., as done in Nexus [40]. However, mi- crokernels restructure the operating system, and the protec- tion mechanisms that they offer are not applicable to com- modity operating systems, which are structured as macro- kernels. Moreover, enforcing security policies on device drivers may impose significant performance overhead. For example, Nexus reports CPU overheads of 2.5× on a CPU- intensive media streaming workload. User-mode driver frameworks [3, 10, 15, 24, 28, 38] allow commodity oper- ating systems to execute device drivers in user mode. How- ever, porting drivers to these frameworks often requires com- plete rewrites of device drivers and the resulting performance overheads are often significant [3, 38]. This paper extends prior work on Microdrivers [20] and proposes a security architecture that offers commodity op- erating systems the benefits of executing device drivers in user mode without affecting common-case performance. In this architecture, each device driver is composed of a trusted kernel-level component, called a k-driver, and an untrusted user-level component, called a u-driver. The k-driver con- tains code that requires kernel privilege (e.g., interrupt pro- cessing functions) and performance-critical code (e.g., func- tions on the I/O path). The rest of the code, which contains functions to initialize, shutdown, and configure the device, neither requires kernel privilege nor is on the critical path and executes as a user mode process. The combination of the u- driver and the k-driver is called a microdriver. A prior study with 297 Linux device drivers comprising network, sound and SCSI drivers showed that as much as 65% of driver code can execute in user mode without requiring kernel privilege
or affecting common-case performance [20]. A u-driver and its corresponding k-driver communicate via an RPC-like interface. When the k-driver receives a re- quest from the kernel to execute functionality implemented in the u-driver, such as initializing or configuring the de- vice, it forwards this request to the u-driver. Similarly, the u-driver may also invoke the k-driver to perform privileged operations or to invoke functions that are implemented in the kernel. However, the u-driver is untrusted and all requests that it sends to the k-driver must be monitored. For example, a u-driver that has been compromised by exploiting a buffer overrun vulnerability may potentially send spurious updates to kernel data structure in its requests to the k-driver. Be- cause the k-driver applies these updates to kernel data struc- tures, the compromised u-driver may affect the security of the entire operating system. We present a RPC monitor to interpose upon all commu- nication between the u-driver and the k-driver, and to ensure that each message conforms to a security policy. The RPC monitor checks both data values and function call targets in these messages. Data values in messages may contain up- dates to data structures that the u-driver shares with the k- driver. The RPC monitor enforces integrity constraints on updates to kernel data structures initiated by the u-driver. In our implementation, these integrity constraints are specified as data structure invariants—constraints that must always be satisfied by the data structure. For example, one such invari- ant may state that the list of network devices must not change during an invocation of a u-driver function to obtain device configuration settings. We present an approach to automati- cally extract such data structure invariants using Daikon [18], a state-of-the-art invariant inference tool. Similarly, the RPC monitor also ensures that k-driver function calls that are in- voked by the u-driver via RPC are allowed by a control trans- fer policy that is extracted using static analysis of the driver. This paper makes two key contributions over prior work on Microdrivers [20]. First, it presents the design and imple- mentation of the RPC monitor to mediate u-driver/k-driver communication. In prior work on Microdrivers, all commu- nication between a u-driver and a k-driver was unchecked, thereby poorly isolating kernel data from untrusted u-drivers. Second, it presents a technique to automatically infer data structure integrity constraints to be enforced by the RPC monitor. The key property of these constraints is that they ex- press invariants over heap data structures, thereby constrict- ing the updates that a compromised u-driver can apply to ker- nel data structures. The security architecture proposed in this paper offers several benefits over prior isolation architectures.
drivers) and monitors kernel data structure updates initiated by u-drivers.
Device drivers for commodity operating systems execute in the same protection domain as the rest of the kernel to achieve good performance and easy access to hardware. This architecture does not isolate kernel data from vulnerabilities in device drivers, which are written in C by third-party ven- dors. Such vulnerabilities, especially in packet-processing code and ioctl handlers, can be exploited by malicious user- space applications. For example, recent work [7, 8] shows that a remote attacker can hijack control of Windows ma- chine by exploiting a buffer overflow in beacon and probe response processing code in an 802.11 device driver. In- deed, our study of vulnerability databases revealed several exploitable buffer overrun and memory allocation vulnera- bilities in driver code [4, 30]. The threats posed to kernel data by compromised device drivers can broadly be classified into two categories.
ments marshaling/unmarshaling protocols to transfer data. (2) Object tracking. Splitting a device driver into a k-driver and u-driver results in data structures being copied between address spaces. The runtime tracks and synchronizes the k- driver’s and the u-driver’s versions of driver data structures. Specifically, it is responsible for propagating the k-driver’s changes to a driver data structure to a u-driver upon an upcall, and for propagating the u-driver’s changes to the k-driver when the upcall returns or when the u-driver makes a down- call into the k-driver. There are several key challenges that must be addressed by the runtime. For example, it must ensure that the u-driver and the k-driver can never simultaneously lock a data struc- ture, and that when the lock is released, the copies of the data structure in the u-driver and the k-driver are synchronized. It must also correctly allocate and deallocate memory in user and kernel space in response to allocation/deallocation re- quests by the u-driver and the k-driver. We refer the inter- ested reader to the Microdrivers paper [20], which describes mechanisms to deal with these challenges in detail.
3.1. RPC monitor
As discussed above, the runtime ensures that driver data structure changes made by the u-driver are propagated to the k-driver, either when an upcall returns or when the u- driver issues a downcall. Because the u-driver is untrusted, all data and control transfers initiated by the u-driver must be checked against a security policy. This is the task of the RPC monitor, shown in Figure 1, which mediates all RPC messages from the u-driver to the k-driver. Note that control and data transfers from the k-driver to the u-driver need not be mediated because the k-driver is trusted. Because our ar- chitecture seeks to protect the integrity of kernel data (rather than its secrecy), the RPC monitor need only monitor writes to kernel data structures. The RPC monitor is implemented as a kernel module that enforces security policies before con- trol and data are transferred to the k-driver. Monitoring data transfer. A compromised u-driver can ma- liciously modify kernel data structures by passing corrupt data. The RPC monitor must therefore detect and prevent malicious data transfers. When a u-driver returns control to its k-driver follow- ing an upcall, or when the u-driver invokes functionality implemented in the kernel or the k-driver via a downcall, data structures in the k-driver are synchronized with their u-driver counterparts using the marshaling protocol. The RPC monitor ensures that each such update conforms to a driver-specific security policy. Intuitively, the goal of the se- curity policy is to ensure that kernel data structures are not updated maliciously, i.e., each update must preserve kernel data structure integrity. For instance, an update must not allow a compromised u-driver access to kernel/device mem- ory regions that a benign u-driver does not normally access. Similarly, an update must not allow a compromised u-driver to execute arbitrary code with kernel privilege. Specifying such integrity constraints is challenging be- cause of the quantity and heterogeneity of kernel data struc-
tures updated by device drivers. In addition, our security ar- chitecture splits device drivers to ensure good performance; consequently, several driver-specific data structures may be copied across the user/kernel boundary. For example, Linux represents network devices using a per-driver net device data structure. In a network microdriver, this data structure may be modified by the u-driver, for example, when the de- vice is initialized or configured. It is also important to mon- itor updates to such driver-specific data structures because these updates propagate to the kernel. Specifying integrity constraints for driver data structures often requires domain- specific knowledge, therefore making manual specification of such integrity constraints cumbersome and error-prone. To overcome these challenges, we present an approach that automatically infers integrity constraints by monitoring driver execution. In our architecture, these constraints are expressed as data structure invariants—properties that the data structure must always satisfy. For example, an invari- ant may state that a function pointer to the packet-send func- tion (e.g., the hard start xmit pointer in the net device data structure in Linux) of a network driver must not change after being initialized. Our approach infers such invariants during training; these are checked during enforcement. During the training phase, we execute the u-driver on sev- eral benign workloads, and use Daikon [18] to infer data structure invariants automatically. Daikon does so by ob- serving the values of data structures that cross the user/kernel boundary and hypothesizing invariants. During the enforce- ment phase, the RPC monitor enforces these invariants on data structures received from a u-driver; it first copies these data structures to a vault area in the kernel, and checks that the invariants hold. If they do, it updates kernel data struc- tures with values from the vault. The kernel itself never uses data structures directly from the vault before they are checked by the RPC monitor. By monitoring data transfers from the u-driver to the k-driver, the RPC monitor prevents compromised u-drivers from affecting kernel data integrity. Monitoring control transfer. The RPC monitor checks u- driver to k-driver control transfers to prevent the u-driver from making unauthorized calls to kernel functions. As the u-driver services an upcall, it may invoke the k- driver via a downcall, either to call a k-driver function or to execute a function implemented in the kernel. Downcalls are implemented using ioctl system calls that are handled in the k-driver. Because the u-driver is untrusted, these down- calls must be verified to be legitimate, e.g., that a downcall is not initiated by a code injection attack on a compromised u- driver. Such unauthorized downcalls can be maliciously used by the u-driver, e.g., to cause denial of service by invoking the kernel function to unregister a device. To avoid such at- tacks, we statically analyze the u-driver and extract the set of downcalls that a u-driver can issue in response to an upcall (static analysis is performed before the driver is loaded). The RPC monitor enforces this statically extracted policy when it receives a downcall from the u-driver. Having checked both data and control integrity, the RPC monitor transfers control to the k-driver, which can now re-
sume execution on newly-updated kernel data structures.
We extended the implementation of the Microdrivers ar- chitecture on the Linux-2.6.18.1 kernel with support to moni- tor data and control transfers from the u-driver to the k-driver. In this implementation, the k-driver, the kernel runtime and the RPC monitor are implemented as a kernel module while the u-driver and the user runtime execute as a multi-threaded user-space process.
4.1. Background on Microdrivers
A microdriver begins operation when its kernel module is loaded and the user-space process is started. The main thread of the user-space process makes an ioctl call into the kernel module and blocks. The kernel module unblocks this thread when it needs to invoke functions in the u-driver. The u-driver and k-driver exchange data and control us- ing an RPC-like mechanism, shown in Figure 2. To invoke the u-driver using an an upcall (Figure 2(a)), the k-driver ( 1 ) registers the k-driver function that initiates the upcall with the RPC monitor; ( 2 ) marshals data structures that will be read/modified by the u-driver; and ( 3 ) unblocks the thread of the u-driver’s user-space process. This transfers control to the u-driver, which in turn ( 4 ) consults the object tracker and unmarshals the data structures into its address space; and ( 5 ) invokes the appropriate u-driver function on the unmar- shaled data structure. The object tracker is a bi-directional ta- ble responsible for maintaining the correspondence between kernel- and user-mode pointers of data structures shared be- tween the k-driver and the u-driver. As the u-driver runtime unmarshals data received from the k-driver into its address space, it uses the object tracker to identify u-driver objects that correspond to kernel-mode pointers received from the k- driver. If the runtime is unable to find such an object, e.g., be- cause the k-driver or the kernel created a new object that the u-driver is unaware of, the u-driver can allocate a new object and enter a new mapping into the object tracker. When an upcall returns, or when the u-driver invokes functions in the k-driver via an ioctl system call (i.e., a downcall), data is marshaled by the u-driver and unmarshaled in the kernel, as shown in Figure 2(b). The main difference in this case is that a RPC monitor interposes on these requests before they are forwarded to the k-driver. The RPC monitor has two key responsibilities—(i) to check control transfers; and (ii) to check data structure integrity. The RPC moni- tor uses a statically-extracted control flow policy to check control transfers—this policy statically determines the set of allowed downcalls for each upcall. For each downcall, the RPC monitor uses the k-driver function registered with it (in step (1) of Figure 2(a)) to ensure that the downcalls are al- lowed. If this downcall is allowed, the RPC monitor checks the integrity of data structures received from the u-driver. To do so, it unmarshals the data received from the u-driver into a vault area. This area is not accessed by the k-driver and is only used by the RPC monitor to check data structure in- tegrity. The RPC monitor checks that each variable that was unmarshaled satisfies a set of invariants; if so, it uses the data
from the vault area to update kernel data structures and frees any data structures the vault. DriverSlicer. To allow existing device drivers on com- modity operating systems to benefit from our architec- ture, we extended DriverSlicer, a device driver partitioning tool [20], to generate security enforcement code. Driver- Slicer is implemented as a plugin to CIL [31], a source code transformation tool, and consists of about 11,000 lines of Ocaml code. Given a small number of annotations, Driver- Slicer automatically partitions a device driver into a k-driver and a u-driver. It also generates code for the k-driver and u-driver runtimes, and the RPC monitor, including code to check control transfers from the u-driver to the k-driver and code to monitor data structure integrity. DriverSlicer consists of two parts: a splitter and a code generator. The splitter analyzes a device driver and identi- fies functions that must execute in the kernel, i.e., those that require kernel privilege to access the device or those that are performance critical. To do so, it uses programmer-supplied specifications in the form of type signatures, to identify such functions. For example, it identifies interrupt handlers based upon their function prototypes; in Linux interrupt handlers always return a value of type irqreturn t. Similarly, func- tions responsible for transmitting network packets typically have two parameters: a pointer to an sk buff structure, and a pointer to a net device structure. Such type signatures need only be supplied once per family of drivers, e.g., one set of type signatures suffices to identify performance critical and privileged functions for most network drivers. The splitter uses a statically-extracted call-graph of the device driver to mark ( 1 ) all functions that match these type signatures; and ( 2 ) all functions potentially called (transitively) by such func- tions as those that must execute in the k-driver; the remaining functions are relegated to the u-driver. DriverSlicer’s code generator uses the output of the split- ter to partition the driver into a k-driver and a u-driver, and generates RPC code to transfer control and data. In doing so, it may require programmer-supplied annotations to clar- ify the semantics of pointers. For example, to generate code to marshal an object referenced by a void * pointer, the code generator must be supplied with an annotation that de- termines the type of the object. Similarly, the code genera- tor also requires annotations to determine whether a pointer refers to one instance of an object or to an array of instances. DriverSlicer currently uses eight kinds of annotations, details of which appear elsewhere [20]. DriverSlicer uses these an- notations to generate RPC code that minimizes the amount of data copied between the u-driver and the k-driver; it does so by using static analysis to determine variables and data struc- ture fields that are read/modified by the u-driver and only generating marshaling code to copy these variables and fields using RPC.
4.2. Monitoring kernel data structure updates
This section describes an anomaly detection-based ap- proach to infer and enforce invariants on kernel data struc- tures. The approach has two phases: a training phase, in
Function Invariant rtl8139 init module (entry) rtl8139 intr mask == C07F, rtl8139 norx intr mask == C02E rtl8139 init module (exit) rlt8139 intr mask == O(rtl8139 intr mask) rtl8139 norx intr mask == O(rtl8139 norx intr mask) rtl8139 rx config == O(rtl8139 rx config) rtl8139 tx config == O(rtl8139 tx config) rtl8139 get ethtool stats (exit) rtl chip info has only one value rtl8139 get link (exit) dev->hard start xmit has only one value rtl8139 open (entry/exit) dev->base addr ∈ {0x0531C468, 0x06520468} rtl8139 get link (exit) L(dev->mc list) == O(L(dev->mc list))
Figure 3. Examples of invariants extracted from the 8139too driver.
ple, it determines that the value of rtl8139 intr mask is un- changed by a call to rtl8139 init module. Enforcing such an invariant constrains the values of rtl8139 intr mask that can otherwise be modified by a compromised u-driver to ini- tiate I/O to unauthorized ports.
( 3 ) Ranges/sets of values. In several cases, a variable may not be a constant, but acquire one of a small set of values. As Figure 3 shows, Daikon infers such invariants as well; for example, it infers that the dev->base addr, which repre- sents the base address of I/O memory, can only acquire one of two values during driver execution. This constraint must be enforced when the k-driver’s copy of dev->base addr is synchronized with the u-driver’s copy; for otherwise, a com- promised u-driver could coerce the k-driver into writing to arbitrary I/O memory addresses belonging to other devices.
( 4 ) Linked list invariants. The Linux kernel uses linked lists extensively to manage several critical data structures. Prior work demonstrates that kernel linked lists can be stealthily modified to achieve malicious goals [33]. Unfor- tunately, Daikon’s C front end does not support inference of invariants on linked lists. We therefore extended Daikon to infer invariants on linked lists. In particular, we augmented the marshaling protocol with code that records the contents of linked lists that cross the user/kernel boundary. Daikon then processes this trace of values and hypothesizes invariants. Our imple- mentation currently supports inference of invariants that in- dicate that the length of a linked list is unmodified by a func- tion call. Figure 3 presents one such invariant, which states that the linked list dev->mc list is unmodified by a call to rtl8139 get link. A key feature of the above invariants is their ability to monitor the integrity of both control and non-control data in the kernel. For example, by inferring the constancy of function pointers, Daikon can detect attacks that hijack con- trol flow by modifying function pointers to attacker-defined code. Similarly, Daikon can detect attacks that modify I/O memory addresses and allow a rogue driver to write to arbi- trary memory locations, thereby preventing this non-control data attack. Daikon’s dynamic analysis approach enables it to infer several kinds of invariants that would be difficult to discover using static analysis of the driver. For example, static analysis is ill-suited to infer invariants on lengths of linked lists. Similarly, in pointer-intensive code (as is com- mon in device drivers), it is hard to statically infer whether a
heap object is unmodified by a function call without access to precise aliasing information. One of the challenges that we faced during development was the sheer quantity of data recorded by Daikon’s front end during the execution of a u-driver. This in turn resulted in two problems. First, Daikon’s inference engine took longer to in- fer invariants, and sometimes even exhausted the memory available on the machine. Second, Daikon inferred several hundred invariants per function, which resulted in increased memory consumption during enforcement. For example, consider the 8139cp network microdriver: Daikon inferred an average of 878 invariants at the exit of each function in the u-driver. Worse, several of these invariants were serendipi- tous, i.e., they were overly specific to the workloads used during inference and were not satisfied by other workloads, thereby resulting in false positives during enforcement. To overcome these problems, we incorporated two key op- timizations. First, we configured Daikon’s front end to only record values transmitted to u-driver functions that commu- nicate directly with the k-driver via upcalls and downcalls, and do not record values for functions internal to the u-driver. Second, we configured the front end so that only the values of variables that are accessed in the u-driver are recorded. For example, if a u-driver function only reads/modifies cer- tain fields of an otherwise large C struct, we only record the values of the fields that are read/modified by that func- tion. To implement this optimization, we employed a con- servative static analysis of the u-driver to determine the fields read/modified by functions in the u-driver. Because Driver- Slicer’s code generator emits marshaling and unmarshaling code only for variables and fields of data structures that are read/modified by the u-driver, as discussed in Section 4.1, malicious modifications by the u-driver on other variables and data structure fields will not be synchronized with the k-driver; hence, they need not be monitored. These optimizations drastically reduce the number of in- variants that Daikon infers, which in turn reduces the mem- ory consumption of the invariant table (described below) dur- ing enforcement. For example, in the 8139cp network mi- crodriver, the average number of invariants at function exits drops over forty-fold. We expect that inferring invariants would be a one-time activity, accomplished either during driver development (if the driver is developed as a microdriver), or when a legacy driver is split with DriverSlicer; these invariants can be dis- tributed by vendors along with drivers. Note, however, that
some invariants inferred by Daikon must be modified to be widely applicable across multiple installations and configu- rations. For example, the invariant for dev->base addr in Figure 3 refers to specific I/O memory addresses, and is not applicable across multiple installations (the other invariants in Figure 3 are portable across multiple installations). To be portable, this invariant would have to be modified to gener- ically state that dev->base addr has only two values, rather than referring to specific I/O memory addresses, e.g., as with the invariant for dev->hard start xmit in Figure 3.
4.2.2 Enforcing data structure integrity constraints
The invariants inferred by Daikon are enforced by the RPC monitor when the k-driver receives marshaled data from the u-driver. The RPC monitor unmarshals this data into a vault area in the kernel’s address space. Data structures in the vault area are only accessed by the RPC monitor and not by the kernel. The RPC monitor itself is implemented as a kernel mod- ule that manages two tables: an invariant table and a vault table. The invariant table stores the set of invariants indexed by the u-driver variable(s) that it is associated with, and is initialized when the microdriver is loaded. The vault table stores pointers to data structures in the vault area and is filled by the RPC monitor when it populates the vault area. The RPC monitor enforces invariants on data received from the u-driver by first unmarshaling this data into the vault area and inserting pointers to these resulting data structures in the vault table. This unmarshaling code is automatically generated by DriverSlicer’s code generator. The marshaling code emitted by the code generator also makes a copy of the original values of variables before an upcall to support in- variants that refer to the O value of a variable. To enforce invariants, the RPC monitor retrieves the invariants associ- ated with each variable in the vault table using the invariant table, and verifies that the invariant is satisfied. For invariants on variables that point to the head of a linked list, the RPC monitor traverses the list and ensures that the invariant is sat- isfied. Any failures raise an alert and can trigger recovery mechanisms, such as restarting the u-driver. If all invariants are satisfied, then the marshaling procotol synchronizes ker- nel data structures by overwriting them with their copies in the vault area.
4.3. Monitoring control transfers
This section describes the techniques used to extract and enforce policies on control transfers from the u-driver to the k-driver. A u-driver may issue downcalls as it serves an upcall from the k-driver. The RPC monitor enforces (Sec- tion 4.3.2) a statically extracted control transfer policy (Sec- tion 4.3.1) to ensure that the downcall is permitted. Extract- ing and enforcing such control transfer policies is necessary to prevent code injection attacks via a compromised u-driver; for example, an attacker with control over a u-driver can is- sue a downcall to a kernel function that unregisters a device, thereby causing denial of service.
4.3.1 Extracting control transfer policies To extract a control transfer policy, we employ static analysis of the u-driver. We first use DriverSlicer to statically extract a call graph of the u-driver. This call graph contains one node for each function in the u-driver; an edge f →g indicates that f can potentially call g (possibly indirectly, via a function pointer). We resolve function pointers using a simple type- based pointer analysis: each function pointer can refer to any function whose address is taken, and whose type signature matches that of the function pointer. DriverSlicer’s splitter identifies potential entrypoints into the u-driver; its code gen- erator also includes an RPC stub in the k-driver for each such entrypoint via which upcalls are issued. For each entrypoint, we use the call graph to identify the set of downcalls that the entrypoint can potentially issue—this set of downcalls associated with each entrypoint serves as the control transfer policy. Associating an upcall with a set of downcalls can result in a permissive policy that can potentially admit mimicry attacks [39]. However, we note that in order to compro- mise kernel data structures, a compromised u-driver issuing a downcall must also send appropriate data with the downcall. As discussed in Section 4.2, the RPC monitor checks the va- lidity of this data in addition to monitoring control transfer, thereby constraining the attacker. Nevertheless, our architec- ture admits the enforcement of more complex control trans- fer policies, such as the sequence of downcalls that can fol- low an upcall. Prior work has developed techniques to extract such control transfer policies (e.g., [21]); we plan to extend our architecture with such support in future work. 4.3.2 Enforcing control transfer policies The RPC monitor enforces the control transfer policy ex- tracted above. When a function in the k-driver makes an upcall into the u-driver, the k-driver registers the entrypoint invoked with the RPC monitor, which in turn pushes this en- trypoint on a stack. When the u-driver issues a downcall, the RPC monitor interposes on this request and ensures that the downcall is allowed by the control transfer policy associated with the entrypoint at the head of the stack. The RPC monitor pops the stack when the upcall returns. It is important to use a stack to track the currently-active entrypoint because an upcall into the u-driver can possibly result in multiple control transfers between the user and the kernel. DriverSlicer’s splitter ensures that there is at most one upcall along any path in the static call-graph of the driver. However, in response to an upcall, the u-driver may need to invoke a function that is implemented in the operating system kernel (e.g., the register netdev function to register a network device; note that this is a kernel function, not a k-driver function). In turn, the kernel function may call back into the driver and the relevant function may be implemented in the u-driver, thus resulting in multiple control transfers.
In this section, we report on experiments conducted on four drivers secured using our architecture. We ported the
Size of K-driver Size of U-driver Number of Annotations Driver SLOC # Functions SLOC # Functions Kernel header Driver specific 8139too 545 (33.7%) 11 (21.6%) 1070 (66.2%) 40 (78.4%) 34 8 8139cp 735 (44.7%) 21 (36.8%) 908 (55.3%) 36 (63.1%) 18 16 ens1371 890 (59.7%) 28 (43.7%) 599 (40.3%) 36 (56.3%) 7 7 uhci-hcd 2060 (81.8%) 60 (87.0%) 457 (18.2%) 9 (13.0%) 27 146
Figure 4. Sizes of the k-driver and the u-driver, and the number of annotations needed by DriverSlicer.
Driver # Funcs. in u-driver # Funcs. covered 8139too 40 35 8139cp 36 33 ens1371 36 14 uhci-hcd 9 7
Figure 5. Function coverage (in the u- driver) obtained by the training workload.
Driver # Invariants Inv. tab. Vault tab. 8139too 2607 247,661 65, 8139cp 212 17,217 14, ens1371 750 70,218 3, uhci-hcd 163 12,888 7,
Figure 6. Memory consumption (in bytes) of the invariant and vault tables.
Original driver Driver in our architecture Driver Workload Throughput CPU (%) Throughput CPU (%) 8139too TCP-send 63.39Mbps 99.76% 61.20Mbps (-3.45%) 99.86% (0%) 8139too TCP-receive 91.96Mbps 34.84% 90.35Mbps (-1.8%) 34.96% (0%) 8139cp TCP-send 64.02Mbps 99.88% 64.51Mbps (+0.7%) 99.94% (0%) 8139cp TCP-receive 90.88Mbps 31.82% 91.66Mbps (+0.8%) 29.94% (-5.9%) uhci-hcd Copy 585.84Kbps 4.92% 578.95Kbps (-1.1%) 7.01% (+42%)
Figure 7. Performance of unmodified network and USB drivers and drivers in our security architecture.
function pointer modifications within the u-driver and pre- vent control hijacking attacks.
whether such invariants result in false positives during en- forcement, we ran the drivers with several benign test work- loads that called functions in the u-driver (the training work- load used to infer invariants was the same as the one in Sec- tion 5.2). We did not observe any false positives during this experiment. While it is unclear whether the same result will hold for other drivers as well, we note that in a real de- ployment, false positives could be eliminated by manually inspecting and refining the invariants. To evaluate false negatives, i.e., cases where invariants fail to detect a compromised u-driver, we conducted fault- injection experiments using the 8139too and 8139cp drivers. (We could not conduct these experiments on the ens1371 and uhci-hcd drivers because of limitations of our prototype in- frastructure.) We used an off-the-shelf fault injector [43] to inject 400 random faults in the u-driver of each microdriver. We measured the number of faults that propagated to the ker- nel (via RPC) and the number of these faults that were de- tected by our invariants. Note that our prototype currently lacks a recovery subsystem. Therefore, faults that propagate to the kernel crash the system, i.e., the RPC monitor can de- tect data corruption, but cannot prevent or recover from a system crash. Our experimental methodology was therefore to inspect system logs following each system crash to deter- mine whether the RPC monitor detected the crash. Figure 8 presents the results of this study. As this figure shows, there were several cases in which the system did not crash and in which the faults were contained within the u- driver (the #NoCrash and #UD columns, respectively). The remaining faults, which constituted the majority, propagated to the kernel, thereby showing the need for an RPC monitor
Driver Faults NoCrash UD Clear InLog Detect 8139too 400 49 26 212 113 95 (84%) 8139cp 400 134 14 147 105 64 (61%)
Figure 8. Results from fault injection.
to inspect kernel data structure updates initiated by the u- driver. As discussed above, we used system logs to determine whether the RPC monitor detected a crash. In several cases (shown in the #Clear column), we observed that the system log had been cleared following the crash. In these cases, we could not determine whether the RPC monitor would have detected the crash. Nevertheless, there were several cases in which we observed a crash for which we could inspect our logs to determine the effectiveness of invariants (shown in the #InLog column). The #Detect column shows the number of #InLog crashes that were detected by the RPC monitor. As these results indicate, the RPC monitor could detect 84% of the injected faults in the 8139too driver and 61% of the faults in the 8139cp driver. These results also show that the RPC monitor can effectively thwart a significant fraction of attacks enabled by a compromised u-driver.
5.3. Performance
We measured both the throughput and CPU utilization of the two network drivers and the USB driver using our QEMU testbed. While QEMU does not provide an accurate repre- sentation of performance on real hardware, it allows us to measure differences in performance. If the driver has lower performance, it will be reflected either as higher CPU utiliza- tion or low throughput. If neither changes, the performance on real hardware should be unchanged. We measured throughput and CPU utilization of the net- work drivers using netperf [14]. We transmitted packets be- tween our QEMU test environment and a client machine. The netperf tests used TCP receive and send buffer sizes of 87KB and 16KB, respectively. To test the USB driver, we copied a 140MB file into a USB disk. All our measurements are averaged over 10 runs, and are presented in Figure 7. As this Figure shows, our security architecture minimally im- pacts common-case performance (the minor speedups that we observed are within the margin of experimental error). This is because the code to transmit packets is in the k-driver; sending a packet does not involve any user/kernel transitions. For the sound driver, we compared the CPU utilization of both the original driver and the split driver as they played a 256-Kbps MP3; CPU utilization in both cases was zero. However, uncommon functionality, such as device ini- tialization, shutdown and configuration, resulted in several user/kernel transitions and took almost thrice as long. During the training phase of the experiments reported in Section 5.2, we used several benign workloads that exercised such func- tionality implemented in the u-driver of each device driver. Figure 9 presents the number of user/kernel transitions and the amount of data transferred in upcalls and downcalls dur- ing this training phase.
Driver KBytes sent/received # upcalls # downcalls 8139too 813 200 160 8139cp 9.5 206 124 ens1371 15.6 395 777 uhci-hcd 25.1 36 126
Figure 9. Data movement in up and downcalls.
Hardware-based isolation techniques, such as Nooks [35] and Mondrix [41], rely on memory protection at the page level (Nooks) or with fine-grained segments (Mondrix) to isolate device driver failures. There are two main differences between Nooks/Mondrix and our work. First, both Nooks and Mondrix execute device drivers in kernel mode. Second, they do not enforce integrity specifications on kernel data structure updates, because doing so is likely to impose sig- nificant performance overheads. The consequence of these differences is that while Nooks and Mondrix can improve re- liability with benign but vulnerable drivers, they cannot pro- tect against compromised drivers that attempt to subvert the kernel. For example, they cannot protect against buffer over- flow exploits that maliciously modify kernel data structures. Virtual machine-based techniques isolate device drivers by running a set of device drivers within their own virtual ma- chine e.g., [16, 19, 25]. In principle, this approach offers all the benefits of our architecture. However, in practice, there are two key difficulties. First, these techniques require the use of a VMM. Although VMMs have seen wide deployment for server-class systems, they are still not in wide use on per- sonal desktops—platforms that support a wide variety of de- vices and hence, drivers. Second, VM-based techniques must provide a front-end driver within the guest VM that commu- nicates requests between the device driver (running on a sep- arate VM) and I/O requests from applications in the guest. Although such front-ends can be developed easily for stan- dard classes of drivers (e.g., network, sound, SCSI), devel- oping front-ends for other one-of-a-kind drivers, e.g., those that support non-standard ioctl interfaces, is cumbersome. Thus, while the VMM-based approach has several benefits, it is not applicable to a wide variety of devices and drivers. SafeDrive [43] and XFI [17] are language-based mech- anisms to isolate device drivers. SafeDrive is an adapta- tion of CCured [32] to protect against type-safety violations in device drivers. While SafeDrive offers low-performance overhead and compatibility, device drivers protected with SafeDrive still execute with kernel privilege. Moreover, SafeDrive only protects against type-safety violations; in contrast, our RPC monitor can protect against violations that transcend type-safety, such as requests by the u-driver to al- locate large amounts of memory, which may lead to memory exhaustion. Similarly XFI ensures control-flow integrity for device drivers. Our security architecture allows the use of any user-space security mechanism to be applied to a large fraction of device driver code without investing the effort needed to adapt these mechanisms to kernel code.
[21] J. T. Giffin, S. Jha, and B. P. Miller. Efficient context- sensitive intrusion detection. In NDSS, 2004.
[22] S. Hangal and M. S. Lam. Tracking down software bugs using automatic anomaly detection. In ICSE, 2002.
[23] Rob Johnson and David Wagner. Finding user/kernel pointer bugs with type inference. In USENIX Security Symposium, 2004.
[24] B. Leslie, P. Chubb, N. Fitzroy-Dale, S. Gotz, C. Gray, L. Macpherson, D. Potts, Y. Shen, K. Elphinstone, and G. Heiser. User-level device drivers: Achieved perfor- mance. Jour. Comp. Sci. and Tech., 20(5), 2005.
[25] J. LeVasseur, V. Uhlig, J. Stoess, and S. Gotz. Unmod- ified device driver reuse and improved system depend- ability via virtual machines. In OSDI, 2004.
[26] J. Liedtke. On μ-kernel construction. In ACM SOSP,
[27] D. Maynor. Os X kernel-mode exploitation in a weekend. http://uninformed.org/index.cgi?v= 8 &a=4.
[28] Microsoft. Architecture of the user-mode driver frame- work, 2006.
[29] Microsoft Inc. Microsoft interface definition language.
[30] Linux device driver vulnerabilities from the MITRE database. CVEs 2007-4571, 2007-05, 2007-4308, 2008-0007, 2005-0504, 2006-2935, 2006-2936, 2005- 3180, 2004-1017, 2007-4997, 2006-1368.
[31] G. C. Necula, S. McPeak, S. P. Rahul, and W. Weimer. CIL: Intermediate languages and tools for analysis and transformation. In Compiler Construction, 2002.
[32] George C. Necula, Scott McPeak, and Westley Weimer. CCured: Type-safe retrofitting of legacy code. In Sym- posium Principles of Programming Languages, 2002.
[33] N. L. Petroni, T. Fraser, A. Walters, and W. Arbaugh. An architecture for specification-based detection of se- mantic integrity violations in kernel dynamic data. In USENIX Security Symposium, 2006.
[34] N. L. Petroni and M. W. Hicks. Automated detection of persistent kernel control-flow attacks. In ACM CCS,
[35] Michael M. Swift, Brian N. Bershad, and Henry M. Levy. Improving the reliability of commodity operat- ing systems. ACM Transactions on Computer Systems, 23(1), 2005.
[36] L. Tan, E. M. Chan, R. Farivar, N. Mallick, J. C. Car- lyle, F. M. David, and R. C. Campbell. iKernel: Isolat- ing buggy and malicious device drivers using hardware virtualization support. In IEEE Intl. Symp. on Depend- able, Autonomic and Secure Computing, 2007.
struct cp private { char * IOMem regs; struct cp desc * Array(64) rx ring; ... } struct net device { void * Opaque(struct cp private) priv; struct net device * Sentinel(next,0) next; ... }
Figure 10. Structure definition from the 8139cp driver, illustrating the IOM , A , O and S annotations.
[37] L. Torvalds. UIO: Linux patch for user-mode I/O, 2007. [38] K. T. Van Maren. The Fluke device driver framework. Master’s thesis, Dept. of Computer Science, Univ. of Utah, 1999.
[39] D. Wagner and P. Soto. Mimicry attacks on host-based intrusion detection systems. In ACM CCS, 2002. [40] Dan Williams, Patrick Reynolds, Kevin Walsh, Emin Gun Sirer, and Fred B. Schneider. Device driver safety through a reference validation mechanism. In OSDI, 2008. [41] E. Witchel, J. Rhee, and K. Asanovic. Mondrix: Mem- ory isolation for linux. In ACM SOSP, 2005.
[42] M. Young, M. Accetta, R. Baron, W. Bolosky, D. Golub, R. Rashid, and A. Tevanian. Mach: A new kernel foundation for UNIX development. In Summer USENIX Conference, 1986. [43] F. Zhou, J. Condit, Z. Anderson, I. Bagrak, R. Ennals, M. Harren, G. Necula, and E. Brewer. SafeDrive: Safe and recoverable extensions using language-based tech- niques. In OSDI, 2006.
Figure 10 presents four kinds of annotations used by DriverSlicer using an example from the 8139cp network driver. These annotations are applied to structure definitions and formal parameters of functions. DriverSlicer supports eight kinds of annotations in total; these are described in de- tail in prior work on Microdrivers [20].
Figure 11. Code snippet from the 8139too mi- crodriver showing marshaling protocol modi- fied to check for data structure invariants.
Figure 11 shows an example of the marshaling pro- tocol augmented to check data structure invariants. As this Figure shows, the marshaling protocol is augmented to record the original values of variables in the vault ta- ble; this is required to enforce invariants of the form var=O(var). The unmarshaling protocol (implemented in checkinv rtl8139 init one) copies values received from the u-driver into the vault area and verifies that invariants are satisfied. If so, kernel data structures are updated with values from the vault using the copy from vault function, which copies the value of a data structure/field from the vault area to the kernel.