Topic Definition Statement

profilebribri201316
Mitigating_Privilege_Escalatio.pdf

Mitigating Privilege Escalation

A Thesis Submitted to the Faculty

in partial fulfillment of the requirements for the degree of

Doctor of Philosophy

by

Scott Brookes

Thayer School of Engineering Dartmouth College

Hanover, New Hampshire

May 2018

Examining Committee:

Chairman Stephen Taylor, Ph.D.

Member Sergey Bratus, Ph.D.

Member George Cybenko, Ph.D.

Member Steve Chapin, Ph.D.

F. Jon Kull, Ph.D. Dean of Graduate and Advanced Studies

ProQuest Number:

All rights reserved

INFORMATION TO ALL USERS The quality of this reproduction is dependent upon the quality of the copy submitted.

In the unlikely event that the author did not send a complete manuscript and there are missing pages, these will be noted. Also, if material had to be removed,

a note will indicate the deletion.

ProQuest

Published by ProQuest LLC ( ). Copyright of the Dissertation is held by the Author.

All rights reserved. This work is protected against unauthorized copying under Title 17, United States Code

Microform Edition © ProQuest LLC.

ProQuest LLC. 789 East Eisenhower Parkway

P.O. Box 1346 Ann Arbor, MI 48106 - 1346

10822683

10822683

2018

Abstract

One particularly difficult challenge in the computer security landscape is preventing

privilege escalation. This type of attack happens when an actor is granted access

to some piece of hardware with limited permissions but manages to circumvent the

security policies meant to contain them. Although a simple bug in the operating

system, or even in user libraries, can be sufficient to enable this type of attack,

such a vulnerability is also relatively easy to fix. Privilege escalation mechanisms

represent a more challenging security risk because they are methods by which generic

vulnerabilities (such as a buffer overflow) can be leveraged to escalate privilege.

This thesis describes a collection of operating system hardening techniques de-

signed to mitigate the risks of common privilege escalation mechanisms. This includes

non-deterministic loading techniques to randomize code, leveraging the virtualization

features of modern hardware to protect operating system code, and a novel operat-

ing system design paradigm. A proof-of-concept prototype was developed for each

of these techniques using the Bear research microkernel. The code for all techniques

described in this thesis is available at https://github.com/SCSLaboratory/BearOS.

Each of the techniques described in this thesis is evaluated in terms of the addi-

tional security it offers alongside the performance cost of the technique. The security

analysis of each technique attempts to describe (and quantify where possible) the

types of privilege escalation mechanisms that the technique interrupts. Meanwhile,

macro- and micro-benchmarks that are compatible with the Bear microkernel illus-

ii

trate the practicality of each of these techniques for deployment on real-world systems.

Synthesizing four different security mechanisms that each address unique types

of privilege escalation threats, the thesis provides a glimpse of a hardened operating

system. Contrary to the standard practice of “patching” the status quo in response to

each new threat, it attempts to visualize a next-generation operating system design

that brings together the best features of non-determinism, virtualization, and hard-

ware resource utilization in order to present a more secure computing system that

can still meet the ever-increasing performance requirements of modern computing

applications.

iii

Contents

Abstract ii

Contents iv

List of Tables viii

List of Figures ix

List of Code Snippets xi

List of Algorithms xii

1 Introduction 1

1.1 Approach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

1.2 Metrics of Success . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

1.3 Publications and Contributions . . . . . . . . . . . . . . . . . . . . . 8

1.4 Thesis Organization . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

2 Background and State of the Art 13

2.1 Virtual Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13

2.1.1 Case Study: Recursive Paging . . . . . . . . . . . . . . . . . . 15

2.1.2 The Virtual Address Space . . . . . . . . . . . . . . . . . . . . 17

2.2 Message Passing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

iv

2.3 Scheduling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

3 Related Work 24

3.1 Privilege Escalation Mitigation . . . . . . . . . . . . . . . . . . . . . 25

3.1.1 Techniques based on Virtualization . . . . . . . . . . . . . . . 29

3.1.2 Other Techniques . . . . . . . . . . . . . . . . . . . . . . . . . 35

3.1.3 Comparison . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

3.1.4 Privilege Escalation Mitigation: Conclusion . . . . . . . . . . 42

3.2 Execute-Only Memory . . . . . . . . . . . . . . . . . . . . . . . . . . 42

3.3 Code Diversification . . . . . . . . . . . . . . . . . . . . . . . . . . . 44

3.4 Asymmetrical Multiprocessing . . . . . . . . . . . . . . . . . . . . . . 46

3.5 Operating System Design Paradigm Shifts . . . . . . . . . . . . . . . 48

3.5.1 Microkernels . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48

3.5.2 ExoKernel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

3.5.3 Unikernels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50

4 ExOShim 51

4.1 ExOShim: Background . . . . . . . . . . . . . . . . . . . . . . . . . . 52

4.1.1 The Kernel and Virtual Memory . . . . . . . . . . . . . . . . 52

4.1.2 Virtualization and the EPT . . . . . . . . . . . . . . . . . . . 53

4.2 ExOShim: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . 54

4.2.1 Assumptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54

4.2.2 ExOShim . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55

4.3 ExOShim: Implementation . . . . . . . . . . . . . . . . . . . . . . . . 58

4.3.1 Providing ExOShim a Context . . . . . . . . . . . . . . . . . . 58

4.3.2 Building the EPT . . . . . . . . . . . . . . . . . . . . . . . . . 60

4.3.3 Starting Virtualization . . . . . . . . . . . . . . . . . . . . . . 65

4.4 ExOShim: Evaluation and Analysis . . . . . . . . . . . . . . . . . . . 69

v

4.4.1 Prototype Complexity . . . . . . . . . . . . . . . . . . . . . . 69

4.4.2 Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69

4.4.3 Security Implications . . . . . . . . . . . . . . . . . . . . . . . 71

4.5 ExOShim: Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . 72

4.5.1 Future Work . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72

4.5.2 Final Thoughts . . . . . . . . . . . . . . . . . . . . . . . . . . 73

5 Diversification: KPLT 75

5.1 KPLT: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76

5.1.1 Using the Virtual Memory Abstraction . . . . . . . . . . . . . 76

5.1.2 The Contents of a KPLT . . . . . . . . . . . . . . . . . . . . . 78

5.2 KPLT: Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . 79

5.2.1 KPLT Creation at Load-Time . . . . . . . . . . . . . . . . . . 80

5.2.2 Run-time KPLT Maintenance and Manipulation . . . . . . . . 82

5.3 KPLT: Evaluation and Analysis . . . . . . . . . . . . . . . . . . . . . 86

5.3.1 Security Implications . . . . . . . . . . . . . . . . . . . . . . . 86

5.3.2 Increase in Diversity . . . . . . . . . . . . . . . . . . . . . . . 87

5.3.3 Performance Cost . . . . . . . . . . . . . . . . . . . . . . . . . 88

5.3.4 Remaining Work and Challenges . . . . . . . . . . . . . . . . 88

5.4 KPLT: Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88

6 Diversification: Überdiversity 89

6.1 Diversification Algorithms . . . . . . . . . . . . . . . . . . . . . . . . 91

6.1.1 Run-time Complexity and Termination Analysis . . . . . . . . 94

6.1.2 Proof of Uniformly Distributed Variants . . . . . . . . . . . . 97

6.2 Überdiversity: Implementation . . . . . . . . . . . . . . . . . . . . . . 100

6.2.1 ELF & the Diversity Loader . . . . . . . . . . . . . . . . . . . 100

6.2.2 The Virtual Memory Abstraction . . . . . . . . . . . . . . . . 101

vi

6.2.3 Diversifying the Entire Software Stack . . . . . . . . . . . . . 103

6.3 Überdiversity: Evaluation and Analysis . . . . . . . . . . . . . . . . . 104

6.3.1 Quantification of Diversity Achieved . . . . . . . . . . . . . . 104

6.3.2 Performance Cost . . . . . . . . . . . . . . . . . . . . . . . . . 110

6.3.3 Security Implications . . . . . . . . . . . . . . . . . . . . . . . 114

6.4 Überdiversity: Conclusion . . . . . . . . . . . . . . . . . . . . . . . . 116

7 KUCS 118

7.1 KUCS: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121

7.1.1 Separating Kernel and User Cores . . . . . . . . . . . . . . . . 123

7.1.2 Fine-Grained Virtualization . . . . . . . . . . . . . . . . . . . 126

7.1.3 Increasing Performance . . . . . . . . . . . . . . . . . . . . . . 128

7.2 KUCS: Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . 130

7.2.1 KUCSBear . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130

7.2.2 Interrupts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132

7.2.3 Virtual Memory . . . . . . . . . . . . . . . . . . . . . . . . . . 141

7.2.4 Message Passing . . . . . . . . . . . . . . . . . . . . . . . . . 142

7.2.5 Scheduling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151

7.3 KUCS: Evaluation and Analysis . . . . . . . . . . . . . . . . . . . . . 157

7.3.1 Security . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157

7.3.2 Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159

7.3.3 Future Work . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168

7.4 KUCS: Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177

8 Conclusion 180

Bibliography 183

vii

List of Tables

3.1 Summary of Privilege Escalation Mitigation Methods . . . . . . . . . 40

3.2 Characteristics of Privilege Escalation Mitigation Methods . . . . . . 41

3.3 Existing Execute-Only Memory Techniques . . . . . . . . . . . . . . . 44

3.4 Existing Code Diversification Techniques . . . . . . . . . . . . . . . . 45

4.1 Processor Benchmarks for ExOShim . . . . . . . . . . . . . . . . . . . 70

4.2 ExOShim’s Bear Test Suite Performance . . . . . . . . . . . . . . . . 72

6.1 Fork/Exec Test with Überdiversity . . . . . . . . . . . . . . . . . . . 110

6.2 Processor Benchmarks with Überdiversity . . . . . . . . . . . . . . . 112

7.1 System Call Mechanism Performance . . . . . . . . . . . . . . . . . . 164

7.2 Processor Benchmarks for KUCSBear . . . . . . . . . . . . . . . . . . 165

7.3 System Benchmarking for KUCSBear . . . . . . . . . . . . . . . . . . 167

viii

List of Figures

1.1 Threats and Mitigations Explored in this Thesis . . . . . . . . . . . . 3

2.1 x86 Virtual Memory Translation . . . . . . . . . . . . . . . . . . . . . 14

2.2 Recursive and Conventional Paging Structure Addressing . . . . . . . 16

2.3 Traditional Operating System Design . . . . . . . . . . . . . . . . . . 21

3.1 Return-to-User Privilege Escalation Attack . . . . . . . . . . . . . . . 28

3.2 SecVisor’s Protection Against ret2usr Attacks . . . . . . . . . . . . . 31

3.3 kGuard’s Protection Against ret2usr Attacks . . . . . . . . . . . . . . 36

4.1 Example Memory Disclosure Vulnerability . . . . . . . . . . . . . . . 52

4.2 Overview of ExOShim Protections . . . . . . . . . . . . . . . . . . . . 55

4.3 ExOShim Benchmark Suite Performance . . . . . . . . . . . . . . . . 71

5.1 Function Calls with KPLT . . . . . . . . . . . . . . . . . . . . . . . . 77

5.2 Assembly KPLT Entry . . . . . . . . . . . . . . . . . . . . . . . . . . 78

6.1 A modified FYS Algorithm . . . . . . . . . . . . . . . . . . . . . . . . 99

6.2 Cache Performance Benchmarks with Überdiversity . . . . . . . . . . 111

6.3 Worst-Case Memory Overhead with Überdiversity . . . . . . . . . . . 113

7.1 KUCS Design Overview . . . . . . . . . . . . . . . . . . . . . . . . . 122

7.2 Asynchronous System Calls . . . . . . . . . . . . . . . . . . . . . . . 129

ix

7.3 Kernel Mappings to a KUCS Remote Process . . . . . . . . . . . . . 143

7.4 Initializing the Message Passing Ring Buffer . . . . . . . . . . . . . . 148

7.5 KUCS System Call Mechanisms . . . . . . . . . . . . . . . . . . . . . 149

7.6 System Call Mechanism Performance . . . . . . . . . . . . . . . . . . 163

7.7 System Benchmarking for KUCSBear . . . . . . . . . . . . . . . . . . 167

x

List of Code Snippets

2.1 State-of-the-Art Message Sending from User-Space . . . . . . . . . . . 19

2.2 User-space “ping” System Call . . . . . . . . . . . . . . . . . . . . . . 20

4.1 Building ExOShim’s EPT . . . . . . . . . . . . . . . . . . . . . . . . 61

5.1 Creating a KPLT Entry . . . . . . . . . . . . . . . . . . . . . . . . . 79

7.1 Interrupt Handling Control Flow . . . . . . . . . . . . . . . . . . . . 133

7.2 Naive Remote Memory Mapping . . . . . . . . . . . . . . . . . . . . . 141

7.3 Message Passing with the Ring Buffer from User-space . . . . . . . . 145

7.4 Kernel’s Initialization for Message Passing Ring Buffer . . . . . . . . 146

7.5 Message Passing with the Ring Buffer from the Kernel . . . . . . . . 147

xi

List of Algorithms

4.1 ExOShim’s Initialization Routine . . . . . . . . . . . . . . . . . . . . . 59

6.1 Greedy Diversity Loader Algorithm . . . . . . . . . . . . . . . . . . . . 91

6.2 Scaling Diversity Loader Algorithm . . . . . . . . . . . . . . . . . . . . 92

6.3 Generic Diversity Loader Algorithm . . . . . . . . . . . . . . . . . . . 94

6.4 Fisher-Yates Shuffle Algorithm . . . . . . . . . . . . . . . . . . . . . . 98

xii

Chapter 1

Introduction

Problem Current operating system designs enable multiple privilege escalation

mechanisms by which an attacker can leverage simple bugs to compromise the system.

Hypothesis Modern multicore hardware can significantly mitigate the risk of priv-

ilege escalation with acceptable performance cost.

Isolation is a cornerstone of security in modern computer systems. As the com-

plexities associated with even the most benign instances of computation skyrocket

and processors are called on to handle hundreds or thousands of such instances si-

multaneously, isolating instances from one another is the only practical way to protect

them.

One of the most fundamental forms of isolation is separating the user or appli-

cation from the kernel or operating system. As a trusted actor that manages all

applications on the system, the kernel has the direct ability to compromise every

application running on a system. If a malicious actor manages to compromise the

kernel, they can easily compromise all applications on the system. Consequently,

isolation is most commonly enforced by hardware. In particular, system memory can

be marked for the user or for the supervisor (kernel) and the processor can operate

1

in different modes, corresponding to user and kernel.

Unfortunately, hardware protections are not 100% successful. The ability of a

malicious user to gain read, write, or execution privileges on memory reserved ex-

clusively for the kernel is known as privilege escalation. Quantifying the impact of

this issue is difficult. Although as of early 2018 only about 5% of reported Common

Vulnerabilities and Exposures (CVEs) since 1999 are listed with type “Gain Privi-

leges,” privilege escalation attacks may also be classified as “Gain information” (9%)

or “Code Execution” (29%) [2]. Furthermore, these statistics only discuss vulnera-

bilities. There are also privilege escalation mechanisms that use seemingly unrelated

vulnerabilities to achieve the escalation of privileges. Using a mechanism such as

return-to-user [93] which uses a compromised return address to force code execution

into user-space without changing the CPU privilege mode, a simple buffer overflow

(15% of all reported vulnerabilities) located in kernel code could give the user kernel

privilege.

The Meltdown vulnerability [109] provides a clear example of how devastating

privilege escalation can be. This vulnerability enables a user-space process to read

arbitrary memory, circumventing paging permissions. This would be listed as a single

vulnerability in the statistics referenced previously, but the protected memory of

almost any operating system running on any one of millions of affected processors

produced over almost a decade could be read by a simple user program.

In an age where many of the world’s secrets are entrusted to computers and the

operating systems that run them, privilege escalation constitutes a major threat. This

thesis explores a collection of significant privilege escalation mechanisms and presents

novel techniques for mitigating them. While they are applicable to operating systems

in general, each of the research efforts described in this thesis was prototyped on a

research microkernel called Bear [123]. The capabilities described here, as well as

the Bear microkernel itself, are all available as open source software under the MIT

2

Privilege Escalation

ExOShim

Execute

Read

Memory Disclosure

Meltdown

Kernel Code Injection

Diversi�cation

KUCS

ret2usr

Kernel ROP

Threat Solution

Figure 1.1: The left side of this figure shows a breakdown of the vulnerabilities and mechanisms examined in this thesis. The right side pairs the techniques explored in this thesis to the privilege escalation mechanisms they address.

software license at https://github.com/SCSLaboratory/BearOS.

1.1 Approach

This thesis presents novel mitigation techniques for five different privilege escalation

mechanisms. The mechanisms and their associated mitigation techniques are listed

in Figure 1.1.

Privilege Escalation Methods

Memory Disclosure Often, “memory disclosure” refers to a vulnerability, rather

than a mechanism. In a memory disclosure, information leaks from a privileged source

to an unprivileged receiver. This is a common issue in monolithic operating systems

that load drivers, potentially buggy pieces of software written by third parties, into

the operating system with privilege. The memory disclosure mitigation explored

in this thesis, ExOShim, enforces a security policy that identifies and prohibits any

3

memory disclosure, regardless of the mechanism used to accomplish such a disclosure.

Meltdown First appearing in the public eye in January 2018, Meltdown [109] is

a vulnerability found in most Intel x86 processor implementations and some other

processors, including ARM and PowerPC. Meltdown takes advantage of speculative

execution and cache timing attacks to recover the results of computations that cause

a fault before such a fault is actually delivered. In other words, a user process can

issue reads to kernel memory and then retrieve the data of the read before the fault

is delivered. This is a demonstration of the dangers of leaving such a vital security

property – the separation of kernel- and user-space memory – to a complex hard-

ware implementation that is all but invisible to the programmer. The Kernel- and

User-space Core-based Separation (KUCS) operating system design provides stronger

separation between the user- and kernel-space such that the hardware vulnerability

allowing Meltdown can no longer impact the system.

Kernel ROP Return-oriented-programming (ROP) is a process by which a care-

fully crafted stack uses return instructions to force control flow through a series of

snippets of existing code (called “gadgets”) to preform computation. Even small pro-

grams have enough gadgets to perform arbitrary execution with a carefully controlled

stack, so any program as big and complex as an operating system can be subjected

to complex ROP attacks. This is an example of how mechanism differs from vul-

nerability ; any simple kernel stack overflow (the vulnerability) can enable a kernel

ROP attack (the mechanism). Software randomization techniques are the standard

technique for mitigating ROP as they make it more difficult to discover the location

of the gadgets of interest and subsequently build the stack necessary to preform the

desired computation. This thesis explores the widespread use of non-determinism on

many existing systems and offers novel techniques and analysis that compliment this

rich field of research.

4

Kernel Code Injection Perhaps the most naive method of escalating to kernel

privilege is to inject malicious instructions directly into the kernel code. One example

of how this may be done is with a kernel routine that copies data from user memory

into kernel memory. If the attacker can force execution to jump to this previously

copied memory, they can execute code that they control with all the privileges of

the kernel. While most kernels use Write xor Execute (W⊕X) security policies, this

is an example of a defensive strategy that targets a vulnerability; not necessarily

the mechanism itself. On the contrary, KUCS enables security policies that ensure

that no code absent from the kernel at boot-time can ever be executed with kernel

privilege.

ret2usr If the attacker can control a single return address in the kernel, they can

force the kernel to jump to user-controlled memory without passing through the

normal context-switching routine required to change the mode of the processor into

user-mode. After its discovery, chip manufacturers began to implement hardware fea-

tures such as Intel’s Supervisor Mode Execution Protection (SMEP) in order to cause

a system fault if user-marked memory is executed without the CPU in user mode.

Despite this hardware patch, which may not be used by corrupted, old, or buggy ker-

nel implementations, this mechanism demonstrates the dangers of weak separation

between kernel- and user-space on modern hardware. As previously discussed, Melt-

down is another attack exploiting this weak separation. KUCS enforces a stronger

separation that is not vulnerable to ret2usr type attacks.

Privilege Escalation Mitigation Techniques

ExOShim is a memory disclosure mitigation that uses the virtualization features

of modern processors. In particular, the extended page tables (EPT) included with

Intel’s VT-x virtualization extensions provide the capability to mark memory pages as

5

execute-only, a feature not available in the kernel paging system. ExOShim provides

a shim layer that enables virtualization and uses these fine-grained permission bits

in the EPT to mark the kernel code as execute-only. Furthermore, it keeps the

operating system as the most basic trusted software entity. Rather than implementing

a trusted hypervisor (which becomes a large attack surface in its own right) it uses

the virtualization features to enforce a security policy that the kernel initializes. In

fact, ExOShim manages to offer this protection in a way that cannot be hijacked,

modified, or disabled after initialization.

Diversification Although there have been decades of research into software ran-

domization techniques, this thesis extends this body of knowledge with two new tech-

niques: the Kernel Procedure Linkage Table (KPLT) and Überdiversity. The KPLT

treats the kernel, which is conventionally mapped into every process, as a shared ob-

ject. It provides a method for randomizing kernel mappings on a per-process basis, so

that each process uses unique virtual addresses to access shared kernel routines. The

second technique, Überdiversity, presents a prototype that explores just how far load-

time diversity could go, and a corresponding study of load-time diversity techniques

and implementations.

KUCS The largest contribution of this thesis, Kernel and User Core-Based Separa-

tion (KUCS) is a kernel design paradigm that utilizes multicore hardware to provide

hard separation between the kernel and the user contexts. Rather than map the ker-

nel into every user process behind a CPU mode bit, KUCS loads the kernel onto a

processor core entirely by itself; it loads user processes onto other cores without any

mapping to the kernel. This eliminates attacks like ret2usr and Meltdown, which take

advantage of the weak separation of kernel- and user-space on a shared processor core.

Additionally, it enables stronger security policies than previously possible, including

one that eliminates the threat of kernel code implants.

6

1.2 Metrics of Success

As in any approach to computer security, it is vital that both the security and per-

formance are well understood. This thesis goes to considerable length to study these

factors and, where feasible, quantify them.

Measuring the performance impact of a mitigation technique is generally done by

running benchmark tests and comparing performance with and without the technique.

Although the Bear operating system cannot support arbitrarily complex benchmarks

the same way a system like Linux could, it does provide industry standard bench-

marking tools such as the AIM9 processor benchmarking suite [6] and a malloc

memory-intensive benchmark written by Chuck Lever and Chuck Boreham at the

University of Michigan [103].

In addition to these benchmarking tools, Bear also contains a test suite specific to

its own system utilities. These can be used to measure overall system performance.

Finally, microbenchmarks are occasionally used to isolate the specific system

events and illustrate how a technique may change performance in that particular

situation. These narrow tests were developed specifically to understand each partic-

ular technique. For example, the Überdiversity study contains a specific study of the

performance implications of cache thrashing in extreme cases of load-time randomiza-

tion. Similarly, the KUCS analysis isolates the performance of system calls in order

to study the effect of changing the system call mechanism used to invoke the kernel

from user-space.

Unfortunately, security in general is not as easily quantified. The Überdiversity

study discusses this at length, and contains a significant effort to quantify its security

properties and provide a framework for quantifying code randomization efforts in

general by using two mathematical characterizations: program variants and entropy.

In spite of the assessments available with program variants and entropy, these

present only approximate security quantifications. Furthermore, they are only ap-

7

plicable to code randomization efforts. Quantifying the security properties of other

methods is extremely difficult. As such, a careful qualitative analysis of security prop-

erties is included wherever quantification is not practical. This is especially true when

examining mechanism instead of specific vulnerabilities, since any number of known

or unknown vulnerabilities can support a particular privilege escalation mechanism.

1.3 Publications and Contributions

The research efforts described in this thesis are summarized in the following publica-

tions:

• Scott Brookes and Stephen Taylor. Containing a Confused Deputy on x86: A

Survey of Privilege Escalation Mitigation Techniques. IJACSA, April 2016

• Scott Brookes, Robert Denz, Martin Osterloh, and Stephen Taylor. ExOShim:

Preventing Memory Disclosure using Execute-Only Kernel Code. In Proceed-

ings of the 11th International Conference on Cyber Warfare and Security, IC-

CWS’16, pages 56–66, April 2016

• Scott Brookes, Robert Denz, Martin Osterloh, and Stephen Taylor. ExOShim:

Preventing Memory Disclosure using Execute-Only Kernel Code. International

Journal of Information and Computer Security (IJICS), 2018, In Press

• Scott Brookes, Martin Osterloh, Robert Denz, and Stephen Taylor. The KPLT:

The Kernel as a shared object. In Military Communications Conference, MIL-

COM 2015 - 2015 IEEE, pages 954–959, Oct 2015

• Scott Brookes, Martin Osterloh, Robert Denz, and Stephen Taylor. Überdiversity:

Exploring the Limit of Load-Time Software Diversification. Technical report,

Thayer School of Engineering at Dartmouth College, April 2018

8

• Scott Brookes and Stephen Taylor. Rethinking operating system design: Asym-

metric multiprocessing for security and performance. In Proceedings of the 2016

New Security Paradigms Workshop, NSPW ’16, pages 68–79, New York, NY,

USA, 2016. ACM

The thesis synthesizes these works to make the following contributions:

• A survey of research techniques that aim to defeat common privilege escalation

mechanisms deployed against commodity operating systems and hardware [30].

• ExOShim: a self-protection mechanism in which the kernel deploys the virtual-

ization features of its processor to mark its code as Execute-Only, eliminating

the risk of memory disclosures that would otherwise reveal its code to a mali-

cious user wishing to reverse-engineer it or find gadgets to build a kernel ROP

payload [26,27].

• KPLT: a mechanism to allow shared kernel functionality to be loaded at unique

virtual addresses in each process, increasing attacker workload at the gadget

collection phase of a kernel ROP attack [28].

• Überdiversity: a study of load-time software diversification techniques that con-

tributes both an advanced and novel implementation and a study of the details

of load-time randomization [29].

– The Überdiversity implementation advances the state-of-the-art in load-

time randomization techniques by:

∗ interleaving kernel- and user-space sections, producing an address space

without unique regions designated for the kernel- and user-space.

∗ simultaneously diversifying every layer of the software stack including

the hypervisor, kernel, and user-space application.

9

∗ providing a higher level of entropy that approaches the hardware-

imposed maximum.

– The Überdiversity study discusses some “dark corners” of diversity imple-

mentations that are rarely presented or formalized, including:

∗ the properties of the algorithm used to generate a random program

layout.

∗ the number of program variants possible with an implementation: a

measurement that compliments the traditional “entropy” measure-

ment.

• KUCS: A novel operating system design that isolates kernel- and user-spaces

on separate physical processing cores of a multicore processor [31]. This allows

for:

– Complete virtual memory isolation and “sandboxing” of every user appli-

cation.

– Device drivers with the security of user-space encapsulation and the per-

formance of kernel modules.

– Hardware-enforced secure contexts available for application-specific use.

– Real-time “watchdog” security monitors in the kernel for intrusion detec-

tion in applications.

– Fine-grained security policies enabled by per-core virtualization and sepa-

ration of the kernel and the application.

– Opportunities to increase performance in novel ways.

• KUCSBear: a prototype operating system that utilizes a KUCS design on the

Bear research microkernel.

10

• Detailed documentation of the KUCSBear prototype implementation, with spe-

cial care taken to generalize implementation details in order to facilitate future

efforts to bring the design to other systems.

• A study of the security properties provided by each project: ExOShim, KPLT,

Überdiversity, and KUCS.

• A suite of performance measurements and analyses in order to estimate the

overhead of each project: ExOShim, KPLT, Überdiversity, and KUCS.

• Open-source implementations of each project: ExOShim, KPLT, Überdiversity,

and KUCS. The implementations are available with the source code for the Bear

operating system at https://github.com/SCSLaboratory/BearOS.

1.4 Thesis Organization

With the understanding of how the different projects are linked in their efforts to

mitigate the threat of different privilege escalation mechanisms, the remainder of this

thesis contains some more general and background content followed by a detailed

description of each project. In particular:

Chapter 2 presents a brief background discussion of operating systems in general

and the Bear operating system in particular. It is intended for the reader that is

unfamiliar with the intricacies of an operating system but wishes to understand the

offensive and defensive techniques described in this thesis, or the reader that is familiar

with operating systems but wishes to understand the platform used for the prototype

of each project in order to give context to the technical details described in the thesis.

Chapter 3 surveys prior work related to the research described in this thesis. Re-

lated work is separated into several categories corresponding to each of the projects

11

presented later in the thesis.

Chapter 4 describes ExOShim; the virtualization-based kernel self-protection mech-

anism to mitigate kernel code memory disclosures by marking kernel code memory

as execute-only.

Chapter 5 describes the first diversification technique, the KPLT. The KPLT takes

inspiration from the procedure linkage tables used for shared user-space libraries and

maps shared kernel code at unique virtual addresses in each process.

Chapter 6 finishes the discussion of diversification techniques by examining Überdiversity:

an advanced load-time diversification implementation, along with a study of certain

theoretical aspects of load-time diversification implementations in general.

Chapter 7 presents KUCS, an operating system design paradigm that addresses

the concerns of weak separation of kernel- and user-space in modern hardware. First,

it discusses the design in general as it could be applied to operating systems. It

also presents a detailed study of KUCSBear, a proof-of-concept prototype operat-

ing system that implements a KUCS design. Finally, it examines the security and

performance of a KUCSBear and KUCS operating systems in general. It contains a

qualitative security analysis for KUCS as a general design and estimates the perfor-

mance implications by comparing micro- and macro-benchmarks run on vanilla Bear

and KUCSBear. Finally, there is a discussion of performance-enhancing techniques

and additional features that could be deployed on top a KUCS design in future work.

Chapter 8 offers concluding thoughts. In particular, it discusses how the four

separate projects presented in this thesis can be deployed together and strengthened

by one another.

12

Chapter 2

Background and State of the Art

This chapter provides a brief introduction to the existing methods of accomplishing

selected operating system tasks. The content in this chapter is not designed to be a

complete introduction to operating systems. Instead, it highlights specific operating

system mechanisms that are utilized or modified in the research efforts described later

in the thesis. This chapter is designed for the reader that is unfamiliar with operating

systems and looking for some context for high level descriptions of the research efforts

described later, or for the experienced kernel developer who is looking for context to

better understand the particular implementation details described later.

Except where otherwise specified, the details and examples in this chapter are

given in terms of the Bear operating system, as presented in [123]. As the platform

for the major research efforts described in this thesis, its implementation details are

especially relevant. However, the chapter can also be used as a case study in operating

system techniques more broadly.

2.1 Virtual Memory

The virtual memory abstraction is a fundamental hardware feature of modern com-

putational systems. With virtual memory, software uses addresses that do not cor-

13

Page Translation and Protection 119

24593—Rev. 3.24—October 2013 AMD64 Technology

Figure 5-1. Virtual to Physical Address Translation—Long Mode

513-200.eps

PML4E PDE

Physical Address

PDPE

PTE

Physical Page Offset

Sign Extension

63 0

Page Directory Offset

Page Map Level-4 Offset

Page Directory Pointer Offset

Page Table Offset

Page Map Base Register CR3

64-Bit Virtual Address

Page Directory Pointer Table

Page Directory Table

Physical Page Frame

Page Table

Page Map Level 4 Table

Figure 2.1: In the x86 virtual memory system, the MMU does a 6 step walk through the four-level page tables to resolve a particular virtual address to the appropriate physical memory. Graphic from AMD Processor Architecture guide [11].

respond directly to physical addresses in memory. Instead, the hardware translates

virtual addresses used by software through a configurable set of tables in order to

produce a corresponding physical address.

On Intel’s x86 processors, this memory translation is done by an onboard Memory

Management Unit (MMU). In 64-bit mode, the tables are four levels deep. The top

level – a Page Map Level 4 Table (PML4T) – contains 512 pointers. Each points to

a Page Directory Pointer Table (PDPT), each of which contains 512 pointers to Page

Directory Tables (PDT), each of which points to 512 Page Tables (PT), which each

points to 512 physical frames of memory. This structure is shown in Figure 2.1.

The virtual memory abstraction allows for each pointer within the tables to be

marked with permissions specific to the memory reached via that pointer. The page

tables provide bits to choose between user and supervisor memory (U/S), writable

14

or non-writable memory (R/W), and executable or non-executable (NX). This allows

for protections to be applied at the granularity of 4k pages.

On a particular core, Control Register 3 (CR3) defines the virtual memory context

of the core. This control register contains the physical address of a PML4T. From

that address, the memory management unit follows a chain of pointers down through

the multi-level paging structures until it resolves the physical address corresponding

to the inputted virtual address as a function of the currently loaded paging structures.

So, to perform a write to a particular memory address the MMU first reads the

address from CR3 in order to find the PML4T (step 1). It then uses the most

significant bits of the address to index into that table in order to find the appropriate

PDPT (step 2). It works its way down through the PDT (step 3) and PT (step 4).

From the appropriate index in the PT it finds the relevant physical frame of memory

(step 5) and can finally use the least significant bits of the address to index to the

correct byte on the frame (step 6). Despite being implemented in hardware, this multi-

step translation process is expensive. In order to avoid the costly translation process

wherever possible, the processor stores recently used translations in the translation

look-aside buffer (TLB).

2.1.1 Case Study: Recursive Paging

Recursive paging (or self-referencing page tables) will serve as a case study to demon-

strate the full flexibility of the virtual memory abstraction as implemented in x86

processors.

In order to motivate the self-referencing page tables, consider the question: how

would system software update the pointer to a PT stored in a particular PDT? The

key observation to answer this question is that each paging structure is a frame of

memory just like any other frame of memory. Naively, writing to a PDT requires

that its physical address is stored in a PT. Thus the same 6 step process described

15

513-200.eps

PML4E PDE

Physical Address

PDPE

PTE

Physical Page O�set

Sign Extension

63 0

Page Directory O�set

Page Map Level-4 O�set

Page Directory Pointer O�set

Page Table O�set

Page Map Base Register CR3

64-Bit Virtual Address

Page Directory Pointer Table

Page Directory Table

Physical Page Frame

Page Table

Page Map Level 4 Table

4

1

2

3

5

6

(a) The conventional method for address- ing paging structures: a mapping is cre- ated like any other for the physical mem- ory on which the paging structure resides.

513-200.eps

PML4E PDE

Physical Address

PDPE

PTE

Physical Page O�set

Sign Extension

63 0

Page Directory O�set

Page Map Level-4 O�set

Page Directory Pointer O�set

Page Table O�set

Page Map Base Register CR3

64-Bit Virtual Address

Page Directory Pointer Table

Page Directory Table

Physical Page Frame

Page Table

Page Map Level 4 Table

4

1

5

6

3

2

(b) The recursive method for address- ing paging structures: a single pointer in the top level table points to its own ta- ble, allowing steps of the translation to be “burned” by cycling in place over the same memory.

Figure 2.2: Paging structures can be addressed conventionally or recursively. In these figures, the numbered dots represent steps of the virtual memory translation undertaken by the hardware. In particular, these dots are shown as the MMU tries to write to a particular PDT. The recursive method serves to illustrate the flexibility of the virtual memory abstraction on x86 processors.

before will carry the MMU from the CR3 down through the PML4T, PDPT, PDT,

PT, and onto the physical frame (which happens to be the PDT) before offsetting

onto the frame and executing the write. This process is shown in Figure 2.2a.

Figure 2.2b shows an alternative method for providing virtual addresses for paging

structures. This method is known as recursive paging or self-referential page tables.

In this method, one entry in the PML4T points back to the base of the PML4T

itself. This is the self-referencing or recursive pointer. A carefully constructed virtual

address will use that pointer to “burn” one or more steps of the memory translation

process. Short circuiting the hardware page table walk like this allows a carefully

constructed address to “cast” the PML4T as a PDPT, PDT, PT, or physical frame

16

of memory depending on how many of the steps of the translation are “wasted” by

returning back to the base of the PML4T itself.

Bear uses recursive pointers in its virtual memory management. The benefit of

such a method is that all possible paging structures attached to the currently loaded

CR3 have a valid virtual address already created for them. Unfortunately, the caveat

is that it becomes much more difficult to address the paging structures of other

contexts.

Regardless of the benefits or costs of using this method, its inclusion here is

simply pedagogical. This method demonstrates the true flexibility of the virtual

memory abstraction. The abstraction will feature heavily later in the thesis as the

privilege escalation methods discussed all rely on the virtual memory layer. For

example, Überdiversity uses fine-grained permissions to interleave kernel and user

code in virtual memory, the KPLT uses different virtual addresses to point to the

same physical memory and the same virtual addresses to point to different physical

memory, and the KUCS prototype omits mappings to the page tables entirely in order

to isolate processes on a core. A deep understanding of virtual memory is vital to a

deep understanding of these projects, and a deep understanding of recursive paging

is a good indicator of a deep understanding of virtual memory.

2.1.2 The Virtual Address Space

Generally, each process has its own set of page tables and therefore its own PML4T

or “CR3 Target.” In many ways, the definition of a process can be reduced to its

CR3 Target since, especially in a scheme like Linux’ where the process structure is

loaded at a known location in virtual memory, it can be all that is required to collect

all information about a process.

The virtual memory regions defined within a process’ page tables come together

to define the “Virtual Address Space” for the process. The virtual address space

17

contains all of the memory needed by the process, including its code, heap, stack,

and data regions. It also contains any libraries in use by the process. Conventionally,

it also includes mappings to the kernel memory, including code, heap, stack, and data

regions as well as kernel libraries and drivers.

The kernel mappings within a process’ virtual address space are enabled by the

flexibility of the virtual memory abstraction. In particular, the permissions associ-

ated with the paging structures protect unprivileged process code from accessing or

modifying the kernel memory. Additionally, the same physical implementation of the

kernel is shared by all processes. Typically, the virtual address space will be segre-

gated between kernel- and user-space. A single address will serve as the boundary

between these two regions of memory, and all addresses above (or below) that ad-

dress are marked with supervisor privileges while all addresses below (or above) it

are accessible by the user.

Understanding that the kernel is mapped into the virtual address space of every

process is vital for understanding the research projects discussed in this thesis. It

explains some limitations of ExOShim, it motivates the efforts of KPLT, it adds

richness to the Überdiversity prototype and it is a major motivator for the KUCS

kernel design. This weak separation between kernel- and user-space is the direct and

exclusive source of the ret2usr privilege escalation mechanism.

2.2 Message Passing

Bear, like many microkernels, uses a message passing interface for all communication

between two processes, or between a process and the kernel. In Bear, the kernel acts as

a mediator for all messages. It accepts messages sent from a process and will dispatch

the message based on the state and identity of the message’s intended recipient. For

example, if a message is sent to the kernel, it is interpreted as a system call and

18

void msg_send(int destination , void *buffer , int length) {

Message_t m;

m.dst = dst;

m.len = buflen;

m.buf = buf;

swint(MSEND ,&m);

}

int msgrecv(int src , void *buf , int buflen) {

Message_t m;

m.src = src;

m.len = buflen;

m.buf = buf;

return swint(MRECV ,&m);

}

Snippet 2.1: This is the user-space implementation of message sending and receiving.

dispatched to a kernel syscall handler. Meanwhile, a message sent to another process

is either injected directly into that process’ address space or saved for future use

depending on whether the receiving process is already blocked waiting for a message

or not.

From user-space, message passing is a matter of packaging the necessary infor-

mation into a Message t structure and then passing that structure to the kernel.

Snippet 2.1 shows the user-space routines that do the packaging.

In order to illustrate the functionality of the message passing system, consider the

case of a “ping” system call that does nothing but receive a status from the kernel.

This system call is shown in Snippet 2.2.

The important observation from Snippet 2.2 is that the process wants to send

data to the kernel by providing the address of a structure it has prepared, and also

wants for the kernel to send data back by writing to a structure specified by the

process. With the kernel and process sharing a virtual address space, the kernel can

19

int ping_kernel(void) {

Ping_req_t request;

Ping_resp_t response;

request.type = PING_SYSCALL;

msg_send(KERNEL , &request , sizeof(Ping_req_t ));

msg_recv(KERNEL , &response , sizeof(Ping_resp_t ));

return response.value;

}

Snippet 2.2: User-space implementation in state-of-the-art message passing microker- nel of a simple “ping” system call

simply dereference the memory address provided by the process (&m in msgsend as

shown in Snippet 2.1) to read the process’ request, and write to the memory addresses

provided by the process (m.buf in msgrecv as shown in Snippet 2.1) in order to deliver

its response. This greatly simplifies the kernel handling of the message decoding and

delivery.

Furthermore, the process is suspended as soon as it issues the message send/re-

ceive. This allows the kernel to safely read and write to the messages using addresses

provided by the user. The guarantee that the process cannot change the parameters of

the message while the kernel is processing alleviates concerns about a Time-of-Check

vs. Time-of-Use (TOCTOU) attack [24,84].

System Calls

In order for the user process to request work from the kernel (such as with a system

call) the process executes a local interrupt on its core. The int instruction forces ex-

ecution through the Intel interrupt control structures including the Global Descriptor

Table (GDT) and Interrupt Descriptor Table (IDT) in order to determine what action

to take. In this case, the IDT will be preconfigured by the kernel to trigger change

from the process’ privilege mode (known as “ring 3”) into the kernel’s (known as

20

User-Space

Kernel-Space

Hardware-Enforced Privilege Mode Boundary

INT/SYSENTER

IRET/SYSEXIT

User-Space

Kernel-Space

Hardware-Enforced Privilege Mode Boundary

INT/SYSENTER

IRET/SYSEXIT

User-Space

Kernel-Space

Hardware-Enforced Privilege Mode Boundary

INT/SYSENTER

IRET/SYSEXIT

Core 0 Core 1 Core N

Hypervisor

Figure 2.3: The conventional design of an operating system maps the kernel into every process. Processes request kernel work by triggering a CPU privilege mode switch and redirection to a predefined kernel landing site with an int or sysenter

instruction. The kernel can return execution to the user through the opposite privilege mode change using the iret or sysexit instructions. On a multicore system, this paradigm is simply repeated once per core.

“ring 0”).

Every process has an instance of the kernel mapped into its virtual memory space.

As a result, the kernel code that begins executing in response to the interrupt is the

instance of the kernel specific to the process that triggered the interrupt. There can

be no confusion about which process initiated the request for work because the entity

that services the request (the kernel) is contained within the process itself.

This also facilitates memory manipulation and data sharing between the kernel

and the process. The kernel has direct mappings in its own context to all memory

that the process can access. Accessing a message from the kernel is as easy as reading

an address out of a register (the process stored the address of its message in a register

before triggering the interrupt) and dereferencing the address.

When the kernel has completed working on the user request, it issues an iret

instruction. This instruction “undoes” the int instruction by changing the CPU

21

privilege mode back to ring 3 and reading the stack to restore the previously saved

state of the user process.

The INT/IRET system call mechanism is used by most conventional kernels. Some

may use the x86 instructions SYSENTER and SYSEXIT, but the basic mechanism is the

same. On a system with multiple cores, this paradigm is simply copied independently

on each core. A summary of this type of system is shown in Figure 2.3.

2.3 Scheduling

Scheduling between two processes in a conventional operating system takes advan-

tage of the kernel mappings that all processes share. First, the kernel will identify

the need to schedule a new process based on a timer interrupt or blocking system

call. Remember that the “kernel” that has identified this need is actually the kernel

instance specific to process A; the process that must yield the core.

Knowing that a new process needs the core, the kernel mapped into process A can

consult the scheduling subsystem to determine which process should run next. Bear

uses a round-robin scheduler where processes that want to run are kept in a queue.

When a process is done running on core, it enters the back of the queue to await its

chance to run again. Each core has an “idle” process that will occupy the core when

no other user process is available to be run. The idle process is similar to any other

in that it has a complete kernel mapping in its context. The only difference is that it

does not preform any useful work.

Once the next process to run has been identified, process B, the kernel does some

state saving and then simply writes the address of the paging structures for that pro-

cess into CR3. The page table swap does not change the instruction pointer or other

registers, but the context is valid because the kernel is mapped into process B at the

same place as it was mapped into process A. The kernel can restore some state from

22

the last time it was scheduled (such as the stack pointers) and then simply return

to pick up execution where it left off. Routinely, this means eventually executing an

iret which will restore the state saved by process B a full time quantum (or more)

ago.

In summary, processes share cores by scheduling one another via their shared

kernel mappings. This process is straightforward because the kernel is resident in

each process on the same core, the kernel mappings are identical between processes,

and the process suspended its own execution when the kernel was invoked.

23

Chapter 3

Related Work

In order to accommodate the most thorough possible discussion of the threats of

privilege escalation, this thesis explores several diverse techniques, each of which

contains its own body of related research. This chapter reviews the literature of each

of these fields. It begins by examining a broad swath of the many diverse techniques

for addressing privilege escalation; especially the three mechanisms for escalating

execution privilege as shown in Figure 1.1, as in [30]. Further detail is provided for the

specific techniques explored in this thesis. In particular, Section 3.2 examines previous

work in the field of execute-only memory in order to provide context for ExOShim,

Section 3.3 provides a brief overview of research in the field of code randomization

as background Überdiversity and KPLT, and Section 3.4 examines other efforts that

utilize asymmetrical multiprocessing: the primary characteristic of the KUCS kernel

design. Finally, since KUCS goes beyond a simple technique that extends the state

of the art, Section 3.5 summarizes other previously proposed operating system design

paradigm shifts.

24

3.1 Privilege Escalation Mitigation

The modern operating system kernel is one of the most basic building blocks of any

complex computing or control system. It exists to provide a controlled interface to the

hardware and to protect multiple processes and users from each others’ actions. In

order to accomplish these tasks securely, it must operate with a higher privilege level

than user processes, making it an attractive target for attackers. As security research

steadily enhances the security of individual processes, the kernel is being attacked

more regularly. Despite the recent increase in popularity of attacking the kernel,

system designers have long recognized the need for kernel security. MULTICS [40,

47] was one of the first operating systems to take security seriously and laid the

groundwork for the most popular kernel security mechanisms still used today. In

particular, it defined operating system “rings”, designated by processor modes, and

memory segmentation and paging structures with flexible read, write, and/or execute

permission bits to allow memory partitioning and protection.

Unfortunately, almost all modern operating systems share a common vulnerability:

a “weak” separation between kernel- and user-space. While the operating system

provides a unique address space for each process in order to isolate processes from

one-another, each address space must still allow access to kernel functionality. This is

generally accomplished by sharing the address space of the kernel with each process.

In contrast to the rare instances of “strong” separation between kernel- and user-space

(such as the 4G/4G split Linux patch [118], 32-bit XNU [94], and certain systems using

the hardware facilities provided by SPARC V9 hardware [115]), this weak separation

protects the kernel from unauthorized access only with the mode of operation of the

processor. A process that successfully manages to operate in supervisor mode has

carte blanche access to all of the code and data of the kernel.

Often assisted by the weak separation of kernel- and user-space, all of the most

popular kernels have been compromised by “rootkits” that give the attacker the

25

highest level of privilege (i.e. “root”) [95, 138, 161]. This thesis aims to address

privilege escalation attacks that:

• Hijack the facilities of the kernel to create a “confused deputy” that is acting

on behalf of the attacker [71]. This does not include attacks that are correctly

exercising badly designed features of the kernel [48] or attacks that operate

outside of the purview of the kernel [90].

• Persist even without a specific kernel-level bug or design flaw. Although most

rootkits do require some kernel level bug (such as a buffer overflow) to be

invoked, attacks that utilize a specific bug such as [7, 9, 116] are beyond the

scope of this work. Additionally, attacks such as [92] that are enabled by a

specific kernel design flaw will not be considered. These cases typically have

trivial solutions.

• Elevate local privilege to root rather than using “horizontal” privilege escalation

such as [4].

• Effect x86 Architectures. The focus of this work is on the x86 architecture

because of its wide use in data centers and workstations [120]. However, some

techniques specific to ARM will be examined because they do make valuable

and interesting contributions to the state of the art.

The privilege escalation attacks that fit these criteria fall into three main cate-

gories: kernel code implants [108], kernel-mode return oriented programming (ROP) [33,

78,148], and return-to-user (ret2usr) attacks [93].

Kernel code implants are attacks in which the adversary manages to overwrite

existing code with (or inject) arbitrary instructions into the kernel space, and then

direct the kernel to execute those instructions. Well-known examples of this type of

attack include exploitation of classic buffer-overflow vulnerabilities associated with

26

system calls [127]. If an attacker manages to overflow a buffer on the kernel stack

using some malformed arguments to a system call, it is possible to write shell-code

onto the stack and overwrite a return-address so as to invoke the shell-code. This

particular attack vector has largely been mitigated by techniques that mark the stack

non-executable or provide canary code to detect overflows [41, 42] but it illustrates

the core concept of kernel code injection.

Kernel return-oriented programming (ROP) attacks defeat the use of a non-

executable stack by using a payload, not of code directly on the stack, but of carefully

crafted stack-frames that direct computation through a series of gadgets found in nor-

mal kernel code [33,78,148]. Research has shown that even small programs are likely

to contain the gadgets necessary to generate a ROP Turing machine controlled only by

a carefully crafted payload delivered to the stack [5]. All operating systems are large

and complex enough to guarantee that the necessary gadgets will be present. As a re-

sult, an attacker with the appropriate knowledge can perform arbitrary computation

using a ROP payload.

A return-to-user attack is enabled directly by weak kernel- and user-space sepa-

ration. In this attack, illustrated in Figure 3.1, a user-controlled target associated

with some kernel-code branch is set to an address in the normal user-space code. The

compromised branch creates a path of execution that leaves kernel-code, entering

user-code, without changing the CPU privilege level from supervisor mode to user

mode. This attack results in the execution of user-controlled code with kernel-level

privileges. Although hardware extensions such as Intel’s SMEP [62] aim to mitigate

this threat, these extensions are only slowly being adopted by operating systems and

SMEP bypass techniques have already been demonstrated [91,143].

Unfortunately, mitigation techniques for privilege escalation do not operate in iso-

lation and it is important that they do not undermine other security features. For in-

stance, it is easy to inadvertently inhibit techniques for enhancing security using non-

27

U se

r- Sp

ac e

Ke rn

el -S

pa ce

Hardware-Enforced Privilege Mode Boundary

INT/SYSENTER

IRET/SYSEXIT

fn: ... JMP kernel_target

attacker_target

attacker_target: ... execve(shell);

Figure 3.1: The return-to-user (ret2usr) privilege escalation mechanism uses a cor- rupted branch instruction in the kernel to jump into user-controlled code from the kernel without passing through the CPU privilege mode switch. As a result, the processor will be executing user memory while still in supervisor mode.

determinism. This general class of techniques was initially described by Cohen [38]

and Forrest [64]. In the years since these seminal papers, many have explored the

idea further. A recent survey of the area was presented in [99]. Address Space Layout

Randomization (ASLR) is one of the most widely used applications of this technique.

First implemented by the Linux PaX team [3], many other operating systems have

implemented some form of ASLR including Mac OS X [8], Windows [162], and others.

ASLR loads distinct memory regions including main program code, libraries, and the

stack and heap at random locations within a program’s virtual address space making

it difficult to predict code entry points. More fine-grained techniques for diversifying

the memory layout of a process [87, 89] require even more flexibility than traditional

ASLR. This field of research is examined further in Section 3.3.

In Summary, this section surveys the primary technologies presented in the liter-

ature to mitigate execution privilege escalation. It provides a comparative analysis

based on their effectiveness, performance impact, and implementation complexity.

28

It also specifically considers whether the technologies provide sufficient flexibility to

coexist with state of the art address space layout randomization techniques. ASLR

is chosen to provide a window to whether the techniques presented “play nicely”

with other kernel security efforts because it is has widespread application on real sys-

tems and requires flexibility in order to be implemented fully. Section 3.1.1 examines

techniques based on hypervisors and virtualization while the remaining techniques

are discussed in Section 3.1.2. These techniques are compared and contrasted in

Section 3.1.3.

3.1.1 Techniques based on Virtualization

Virtualization has dramatically changed the face of computing, not simply in terms

of security and the way individual users interact with computers, but also by enabling

cloud computing by allowing virtual machines to be migrated between servers. By

adding a layer to the standard software stack, known as a hypervisor [17] or Vir-

tual Machine Monitor (VMM) [70], an abstraction layer is introduced to isolate the

operating system kernel from the hardware. In many ways, the hypervisor is to an

operating system what an operating system is to a user process – serving to protect

virtual machines from each other just as a kernel isolates user processes. The fol-

lowing approaches use virtualization as a means to deliver security guarantees to the

kernel.

NICKLE

NICKLE [139] provides memory integrity to kernel code and thereby denies the ex-

ecution of kernel code implants. It uses a VMM to maintain a “shadow” copy of

memory that is verified when any kernel-code is loaded. This is achieved by com-

paring the memory to be loaded against a pre-computed cryptographic hash of the

“clean” code distributed by the manufacturer or developer of the code. At boot time,

29

a known clean copy of the kernel is loaded into the shadow memory and whenever a

kernel module is loaded at runtime, it is verified and added to the shadow memory.

With the integrity of the shadow memory guaranteed by off-line a priori crypto-

graphic hashes of trusted code, NICKLE can ensure that no unauthorized kernel code

is executed by directing all memory accesses targeting kernel code to retrieve from

the shadow memory rather than from regular memory. Although no attempt is made

to deny an attacker from modifying or injecting kernel code, kernel-mode execution

is contained within trusted memory.

This is achieved transparently to the operating system kernel, allowing for com-

modity operating systems to be executed with NICKLE with no modification of kernel

code. Additionally, NICKLE permits the mixing of kernel code and data within mem-

ory pages; this distinguishes NICKLE from many alternative approaches that require

code and data to be loaded onto unique pages.

Unfortunately, NICKLE requires the off-line computation of cryptographic hashes

for any code that may be executed; this poses a significant logistical issue for main-

taining NICKLE on real systems and adds additional vulnerabilities associated with

protection and distribution of hash values. NICKLE imposes a “minimal to moder-

ate impact on system performance, relative to that of the respective original VMMs”

averaging 1%-5% [139].

SecVisor

SecVisor [147] is an alternative virtualization technology leveraging hardware facili-

ties to virtualize physical memory associated with modern processors. By utilizing

this additional layer of translation from “guest physical” to “real physical” memory

addresses, additional hardware memory protections can be enforced. This capability

typically provides additional flexibility in creating memory access security; namely,

any combination of read, write, and execute permissions can be allowed or denied on

30

U se

r- Sp

ac e

Ke rn

el -S

pa ce

Hardware-Enforced Privilege Mode Boundary

INT/SYSENTER

IRET/SYSEXIT

fn: ... JMP kernel_target

attacker_target

attacker_target: ... execve(shell);

Figure 3.2: SecVisor uses the processor’s virtualization features to mark only one of user- and kernel-space as executable at a time. This defeats ret2usr attacks by providing an opportunity to verify that the CPU privilege mode has changed as appropriate whenever the code execution has changed from user- to kernel-code or vice versa.

a particular page of memory [80].

SecVisor uses physical memory virtualization to mark only one of kernel- and

user-space executable at a time. When a violation of security rules is detected, the

protections can be swapped if the CPU has indeed changed privilege level, but are

otherwise denied. This defeats ret2usr attacks by preventing unauthorized processor

mode switches as shown in Figure 3.2. Additionally, the same virtualization allows

SecVisor to enforce standard W⊕X rules on all kernel code pages that the user has

approved. This mitigates the possibility of a kernel code implant by verifying that all

executable kernel code is non-writable and has been approved for execution by the

user.

The security benefits of SecVisor are packaged in a tiny VMM that provides a small

attack surface: only 4092 lines of source code in total. Unfortunately, SecVisor does

have several weaknesses. The kernel running on top of SecVisor must guarantee that

it does not share code and data on a single page. Additionally, the kernel has to be

31

modified to cooperate with SecVisor by issuing vmcalls to designate that it is loading

or unloading kernel code. Finally, it imposes an overhead as high as 97% due to the

additional translation required by the virtualization of physical memory. For a full

discussion of performance overhead costs, the interested reader should consult [147].

It is worth noting that [65] discovered two different bugs in the SecVisor imple-

mentation that allowed an attacker to violate rules that SecVisor claimed to enforce.

Although these were implementation rather than design issues, and easily remedied,

it is clear that even in a small code base security properties are difficult to reason

about and correctly enforce.

SVA

The Secure Virtual Architecture (SVA) [46] is a set of architecture independent in-

structions that allow an operating system to interact with hardware. A kernel is

ported to use these instructions, similar to porting a kernel to any new hardware ar-

chitecture. Offline, an SVA compiler produces SVA byte-code from the kernel source

code. This compiler has advanced features to provide memory safety and control-flow

integrity at compile-time, similar to “safe” programming languages such as Java. The

byte-code is distributed to users and executed on top of a virtualized SVA interpreter

that performs the final step of translating to native target-dependent machine code.

The effort required to port an operating system to execute on SVA and the large

performance cost are balanced by a promise of a substantial increase in security.

Guaranteed memory safety and control-flow integrity deny common methods used to

initiate ret2usr, kernel ROP, and kernel code implant attacks. An important point

is that SVA does not set out to deny these attacks explicitly. Instead, it attempts to

deny the vulnerabilities that enable these forms of attack, such as buffer overflows.

Unfortunately, the infrastructure needed to support SVA presents a significant hurdle.

In addition to porting a kernel to a new architecture, SVA imposes restrictions on

32

the kernel’s memory allocation mechanisms that are likely to require modifications

in kernel subsystems such as kmalloc. The performance cost is high, measured at

approximately 50% on average, but at times reaching a 4x cost.

KCoFI

Kernel Control Flow Integrity (KCoFI) [45] leverages the mechanics of the SVA im-

plementation discussed previously, but offers only control flow integrity. Specifically,

KCoFI ensures that function calls always enter at the beginning of some function’s

code, and that all returns from a particular function target the location of a possible

call site. In order to prevent user-space applications from imitating the labels that

KCoFI uses to validate branches, allowable address transitions are restricted to those

within a certain pre-defined “kernel” range of virtual addresses. This limits the capa-

bilities of advanced load-time randomization schemes. KCoFI also provides advanced

treatment for the issues that make control flow integrity particularly difficult in the

context of operating systems. In particular, it takes special care to handle interrupts,

signals, DMA/devices, incomplete branch target information at compile-time, and

page faults.

By verifying all branches at run-time, while the processor is in kernel mode, KCoFI

manages to deny each of the three primary privilege escalation techniques described in

this survey. Unfortunately, as with SVA, there is a large performance cost. Although

the average performance impact on a standard application was 13%, worst-case costs

up to 3.5-fold were reported. In addition, the method shares the SVA framework and

therefore also requires porting the OS to a new “architecture,” and pre-compiling the

kernel and all of its modules with specialized SVA compilers.

33

SBCFI

State-based control-flow integrity (SBCFI) [134] provides course grained control-flow

integrity for the operating system kernel. It sets itself apart from traditional control-

flow integrity solutions, such as [58], in two ways. First, it implements monitoring

externally from the kernel, in a hypervisor. Additionally, it assumes that attackers will

generate persistent control-flow violations, therefore necessitating that kernel state is

checked only periodically. Consequently, its introspection techniques allow SBCFI to

detect any attack that persistently modifies the kernel’s known control-flow graph.

The authors of [134] argue that trading strict security rules for performance by

using SBCFI instead of complete CFI is acceptable because SBCFI will still detect

most rootkits. In particular, they examined 25 rootkits found “in the wild” on Linux

and found that all but one were detected by SBCFI. They suggest that attacker goals

such as packet-sniffing or keystroke logging demand persistent rather than transient

control-flow changes.

Unfortunately, SBCFI focuses on detection rather than prevention. This, com-

bined with the focus on only persistent control-flow changes, leaves many avenues

open to the attacker. SBCFI verifies the state of the kernel by checking a pre-

computed hash of the kernel code and checking all function pointers stored in the

kernel heap to verify that nothing has been changed. These checks would not detect

a process that has achieved escalated privilege via a ret2usr attack or a kernel ROP

payload. Overall, SBCFI manages to effectively deny persistent modifications to the

kernel control-flow graph with minimal performance costs of less than 1% on average.

However, it fails to address the general threat associated with privilege escalation.

34

3.1.2 Other Techniques

kGuard

kGuard [93] aims to deny ret2usr attacks by inserting guards on the kernel’s control-

flow at compile time as shown in Figure 3.3. On the x86 platform, the call, jmp, and

ret instructions all redirect control-flow and therefore are vulnerable to being hijacked

in order to redirect kernel execution into user-controlled code. kGuard places an inline

check before any of these instructions. The checks are provided in two different forms

depending on whether the target address is stored in a register or in memory. The

checks simply verify that the branch target lies within kernel-space. Unfortunately,

if an attacker controls the target of two branches, he can direct the first to jump

directly to the second branch, bypassing the kGuard check completely. Since the

second branch is in kernel-space, the check on the first branch would allow the control

transfer. To avoid this attack, kGuard includes a compile-time code diversification

mechanism that makes it difficult for the attacker to locate the address of the second

branch.

One of the most significant advantages of kGuard is that, as a purely compile-time

technique, it is portable to any operating system on any target hardware. It does not

require any special hardware or impose many restrictions on the implementation of

kernel features. Additionally, its average performance cost is low at approximately

1%, making it deployable on existing systems. Unfortunately, kGuard does suffer

from a variety of weaknesses. Although its simplicity lends itself to easy deployment,

it is unable to protect against kernel-code implants or kernel-ROP. Although these

are outside of the scope of kGuard, a kernel code implant could be used to create a

ret2usr attack by implanting an unguarded jump into a user-space region. Therefore,

another technique must be used in combination with kGuard to deny the possibility

of a ret2usr scenario. This quickly increases in complexity and performance cost as

35

U se

r- Sp

ac e

Ke rn

el -S

pa ce

Hardware-Enforced Privilege Mode Boundary

INT/SYSENTER

IRET/SYSEXIT

fn: ... if target Є kernel JMP target else JMP fault_handler

attacker_target

attacker_target: ... execve(shell);

Figure 3.3: kGuard is a compile-time technique that includes run-time checks around kernel branch instructions. These checks verify, before the branch, that the target of the branch is within kernel space. Combined with a code diversification routine, this prevents ret2usr attacks.

multiple techniques need to be deployed on the same system. Additionally, kGuard’s

inline checks verify that the target of a control-flow transfer lies in kernel-space only by

checking that it falls within a predefined range. This limits the capacity for deploying

advanced code randomization during the loading of the kernel.

Return-less Kernels

Recall that Kernel ROP attacks requires “return” instructions in order to move from

one gadget to another. In [106] the author utilizes “return indirection,” introducing

additional jumps at compile-time to disrupt this mechanism and defeat kernel ROP

attacks. This approach uses a pre-computed table of all legal return addresses. Rather

than pulling a return address from the stack and jumping to it at the end of a function,

this method reads an address from the specified index in the return address table.

If this table is trusted, the attacker could only modify which legal return address is

used. It is assumed that most gadgets begin in a location other than a legal return

36

address, and as a result this technique defeats the possibility of an attacker to craft

a malicious payload.

In addition to introducing return indirection, [106] introduces compiler modifica-

tions to avoid instructions with an embedded “return” opcode. On an architecture

such as x86, variable length instructions make it possible to read different instructions

if the instruction pointer is offset some distance into an opcode. Without taking care

at compile-time to avoid these scenarios, an attacker could still create gadgets by in-

dexing the instruction pointer at unintended positions in the middle of the intended

instruction.

The idea of using a return-less kernel is clearly beneficial as it effectively mitigates

a very particular risk. Unfortunately, it does impose a performance overhead of

approximately 6%. In addition, it does require modification to the kernel source.

Although functionality provided by compiling a higher-level language, such as C,

does not need to be modified, any functionality defined in assembly language must

be manually modified to follow return-less principles. Since the kernel interfaces with

hardware directly, there is a non-trivial amount of assembly code included in most

kernel implementations.

PaX

One of the first kernel-hardening efforts was implemented on Linux by the PaX

team [133] circa 2000. UDEREF [153] utilizes segmentation to create a stricter sep-

aration between kernel- and user-space (denying ret2usr), while the PAGEXEC and

Restricted mprotect() features essentially generate and enforce typical W⊕X secu-

rity rules on kernel code and data to mitigate kernel code implants.

PaX is valuable as a case study in hardening kernels. Unfortunately, it is less

valuable as a mechanism for protecting modern kernels on today’s hardware. Its

protection mechanisms were based on Linux-specific software mechanisms (such as

37

mprotect()) and x86-32-specific hardware features (such as segmentation). Addi-

tionally, the performance cost was significant, according to [93]. PaX-reported data

about performance cost was available at the time of writing.

Sprobes and TZ-RKP

Sprobes [67] and TZ-RKP [14] both utilize the ARM TrustZone [1] hardware facili-

ties included in modern ARM processors. TrustZone is a hardware-protected context

that can run tangentially to the regular operation of the processor. The hardware dis-

ables the normal processor context from accessing anything within the “secure world”

created by TrustZone and transitions between the regular context and TrustZone’s

secure context are limited by hardware to a small and well-specified interface.

Sprobes [67] utilizes TrustZone by installing an introspection handler in the secure

world and installing, at load- or run-time, special instructions that invoke the secure

world at predetermined points in the execution of the kernel. When one of these

probes is executed, control transfers to the secure world in which kernel state can be

interrogated, control flow or memory contents verified, or any other number of actions

can be taken. Furthermore, restrictions are placed on the normal world’s ability to

manipulate the virtual memory settings of the processor. The requirement that these

systems be updated by the secure world guarantees that a kernel cannot manipulate

virtual memory in order to bypass the probes.

TrustZone-based Real-time Kernel Protection (TZ-RKP) [14] is a similar approach

that forces vital control operations involving the virtual memory layer to be routed

through the secure world. TZ-RKP forgoes the probes provided by Sprobes, but takes

a more extreme approach by limiting the kernel’s control over important system state

such as virtual memory. TZ-RKP forces all attempts to control virtual memory and

other hardware resources through the secure world, providing a mechanism to verify

any changes to the system state. With a controlled and static system state, it is easier

38

to make claims about what an attacker may do to manipulate the kernel state.

Both [14] and [67] are built on the TrustZone architecture. The hardware under-

lying their implementation allows each to be implemented with a reasonable perfor-

mance cost (typically 10%). TrustZone is also attractive because it manages to avoid

the “turtles all the way down” problem in which software layer x is protected by

introducing software layer x− 1, which simply becomes the new target for attackers

and instantiates the same problem again. Traditional virtualization can be criticized

for this problem [25, 141], but TrustZone holds itself off to the side of layer x rather

than existing underneath it.

Unfortunately, TrustZone is an ARM-specific technology. Although ARM is used

extensively in mobile and embedded applications, the x86 architecture continues to

dominate desktop and server applications. Although these techniques are interesting,

their utility is limited by a reliance on specialized hardware.

3.1.3 Comparison

Table 3.1 summarizes and compares the techniques discussed in the previous sections

on the basis of their ability to mitigate privilege escalations and their expected cost:

• Kernel Code Implant/Kernel ROP/ret2usr : Does this technique mitigate the

risk of privilege escalation associated with these particular attack vectors?

• Typical/Maximum Performance Cost : What is the typical and worst-case re-

ported performance costs?

The performance costs listed represent only the maximum performance cost and

an estimated average used only to illustrate differences between the techniques. In

some cases these come from micro-benchmarks corresponding to small code segments,

in other cases they come from macro-benchmarks corresponding to full applications.

For the estimated average, they are often a mix of these tests. Each of the techniques

39

Project Kernel Code

Implant

Kernel ROP

ret2usr

Typical Reported

Performance Cost

Maximum Reported

Performance Cost

KCoFI [45] 3 3 3 13% 3.5× SVA [46] 3 3 3 50% 4×

SecVisor [147] 3 7 3 20% 97% NICKLE [139] 3 7 7 1-5% 19.03% SBCFI [134] 3 7 7 <1% 13% kGuard [93] 7 7 3 1% 23.5% Return-less Kernel [106]

7 3 7 6% 17.32%

PaX [133,153] 3 7 3 No Data No Data Sprobes [67] 3 3 3 10% ∼10% TZ-RKP [14] 3 7 3 3% 7.65%

Table 3.1: This table summarizes the methods of mitigating privilege escalation that were described in this Section. In particular, it shows which of the three main privilege escalation techniques each method targets, and summarizes some performance cost data from each effort.

offers thorough performance cost analyses that could not be summarized in a simple

table. Interested readers should consult the original paper for each technique for a

more complete treatment.

Table 3.2 compares the techniques on the basis of general observations regarding

their operation:

• x86-64 compatible: Most desktop and server-class systems use the 64-bit x86

architecture. Is the technique viable with the hardware provided by the x86-64

hardware?

• Memory and/or Control Flow Integrity : Which is the primary mechanism by

which the tool delivers its security guarantees?

• Code-Diversity Compatible: Is the technique sufficiently flexible to allow for

advanced fine-grained address space layout randomization techniques?

• Code Size: How many lines of code (LoC), as a measure of the attack surface

40

Project x86-64

Compatible

(M)emory and/or

(C]ontrol [F)low Integrity

Code-Diversity Compatible

LoC

NICKLE [139] 3 M 3 932 KCoFI [45] 3 CF 7 5579 SVA [46] 3 M & CF 3 No Data

SecVisor [147] 3 M 3 4092 SBCFI [134] 3 CF 3 No Data kGuard [93] 3 CF 7 1000

PaX [133,153] 7 M & CF 7 No Data Return-less Kernel [106]

3 CF 3 2100

Sprobes [67] 7 M & CF 3 No Data TZ-RKP [14] 7 M 3 No Data

Table 3.2: Any security technique is hard to summarize in terms of only the attack it aims to neutralize and the performance cost it incurs. These techniques often change deep and fundamental parts of the operating system’s functionality. They may introduce new complexity in the system. This table presents additional features of each technique to provide a more complete picture of how they interact with other system components.

presented, are used in the implementation of the technique as presented?

It is clear from Table 3.1 that while KCoFI and SVA offer the most protection

against the three different techniques associated with privilege escalation, they also

come with dramatically more performance overhead than the other methods. This

conforms to expectations in that the more thorough the security measure, the higher

its performance impact. Sprobes and TZ-RKP appear exceptional as they enjoy the

lowest performance costs and strong security claims. Unfortunately, each utilizes the

ARM TrustZone architecture and consequently are unavailable on the Intel x86 archi-

tecture. Additionally, vulnerabilities have already been discovered in some TrustZone

hardware implementations [142].

41

3.1.4 Privilege Escalation Mitigation: Conclusion

Each of the techniques examined in this section makes valuable contributions to the

security of modern operating systems. Those that offer the most comprehensive

security suffer from high performance costs or specialty hardware requirements. On

the other hand, many mitigate a specific, focused risk to kernel security while suffering

only a small performance cost. Unfortunately, there is no single solution that offers

both acceptable performance and comprehensive security coverage on the popular x86

platform. The impact of combining the techniques to improve coverage is not well

understood in terms of complexity, performance, or security.

Overall, the kernel developer has a wide variety of techniques to choose from,

but must balance individual strengths in privilege escalation prevention with the

associated penalties in performance and complexity. Future work aimed at mitigating

privilege escalation is likely to continue to have performance issues without some

change in the underlying hardware or kernel design paradigms. Modern commodity

operating systems are so highly developed that there is unlikely to be some technique

hiding in a dark corner that will not decrease performance while requiring extra work.

3.2 Execute-Only Memory

ExOShim, discussed in Chapter 4, uses the Intel’s VT-x virtualization extensions to

implement execute-only permissions on kernel code pages. In order to provide context

for this project, a series research efforts examining execute-only memory is presented

here for review.

Execute-only memory has been used in a variety of systems for protection of

user code. For example, Readactor [43] is a kernel implementation that allows the

administrator chose to mark user-space applications execute-only. ExOShim on the

other hand, was specifically crafted with the goal of preventing ExOShim from being

42

disabled or hijacked. Furthermore, ExOShim protects the operating system pages,

not user-space applications.

Backes et al [15] have shown in Execute-no-Read (XnR) another way of achieving

execute-only code pages. Though, unlike ExOShim, it only emulates execute-only

paging. XnR uses a sliding window mechanism to keep the last n pages both readable

and executable. All other pages are marked as non-present. Note that a page is

readable while it is marked executable. Unfortunately, an attacker could craft an

exploit once access is gained to the page. Once a gadget has been crafted, XnR could

be tricked into jumping to malicious code pages and thus marking other pages as

readable. ExOShim on the other hand, offers true execute-only memory enforced by

the hardware. Utilizing Intel’s EPTs, a page is marked execute-only and not readable.

This prevents an attacker from obtaining information about running the programs

code.

Gionta et al [68] proposed HideM. Here, the concept of a Split Transaction Looka-

side Buffer (TLB) is being used. By utilizing a Split-TLB, Gionta is able to direct

instruction fetches and data reads to different physical memory locations. An instruc-

tion fetch is a legitimate operation, as well as a white listed data read of embedded

constants. Used by PaX [156] previously to implement W⊕X, it now will most likely

not work on any modern hardware system since modern processors use unified second

level TLBs.

Non-readable memory techniques such as ExOShim must be combined with tech-

niques to randomize program layout in memory. This is because indirect memory

disclosures, such as reading a return address from a stack, are sufficient for inferring

the location of gadgets in non-readable but non-randomized code.

Table 2 illustrates and contrasts key features of memory protection mechanisms.

It shows that ExOShim makes two contributions to the area of memory disclosure

prevention. Firstly, it targets kernel code rather than user-level applications. The

43

ExOShim Readactor [43] XnR [15] HideM [68]

Stops Memory Leaks Kernel 3 7 7 7

User 7 3 3 3

Performance Cost (%) 0.86 6.4 2.2 1.49 Survives Kernel Compromise 3 7 7 7

Table 3.3: Comparison of Memory Protection Techniques. ExOShim adds to the state of the art by protecting from kernel level memory disclosures and persisting through kernel compromise.

kernel manages all processes, hardware, and other system resources. Hence, finding

vulnerability in the kernel will directly impact the security and stability of the entire

system. Thus, ExOShim explicitly targets prevention of kernel information leakage.

Furthermore, ExOShim is designed to persist despite any potential kernel com-

promise. ExOShim does not accept any input after its initialization and uses the

EPT to protect all of its own code and data from any read, write, or execute access.

As shown in Table 3.3, Readactor, XnR, and HideM could all be hijacked or disabled

in the event of a kernel compromise. Readactor provides an explicit mechanism for

the kernel to configure or disable its execute-only protections. XnR uses a page fault

handler to implement its execute-only memory protections; the kernel can change

the page fault handler at any time, completely bypassing XnR. HideM also relies on

kernel cooperation to provide the memory protection it offers. It requires the kernel

to “prime” the split-TLB with different images of memory in the read-case versus the

execute-case. ExOShim distinguishes itself from these approaches by acting specifi-

cally to survive through kernel compromise. The mechanism for this is described in

detail in Chapter 4.

3.3 Code Diversification

Computer system security via non-determinism was first proposed in various forms in

seminal papers several decades ago [13,38,64,137]. Since then, the idea has been fully

44

Study 64-bit Maximum Reported

Entropy (bits)

Discussion of

Algorithm

(U)ser-space, (K)ernel, and/or

(H)ypervisor Chew & Song ’02 [36] 7 15 None U & K

PaX ’01 [157] 7 24 None U Wartell et al. ’12 [160] 3 Unspecified None U Shioji et al. ’12 [150] 7 Unspecified None U Davi et al. ’13 [50] 3 Misspecified† None U

Bhatkar et al. ’05 [19] 3 26 None U Tanenbaum et al. ’12 [69] 7 Unspecified Partial K

Xu et al. ’09 [163] 7 28 None U Hiser et al. ’12 [75] 7 Unspecified None U Kil et al. ’06 [96] 7 29 None U & K Kanter ’13 [86] 3 27 None U & K

Überdiversity 3 47 Full U & K & H † Davi’s reported “entropy” is actually a count of program variants.

Table 3.4: This table presents a selection of fine-grained load-time memory random- ization techniques. It characterizes each in terms of the software level at which it is applied, its discussion of the algorithm used to diversify, and the maximum reported “entropy”.

embraced by the security community, leading to many research efforts exploring differ-

ent applications and implementations of randomization for security at development-,

compile-, load-, or run-time. Larsen et al. acknowledge the many works that came

before Überdiversity and KPLT more thoroughly than the scope of this section would

allow in [100,101].

Table 3.4 contains a selection of the relevant works presented in [101] and some

others. It summarizes which projects are 64-bit compatible, the maximum reported

entropy that their implementation achieves, to what extent the publication discusses

the algorithm used to implement their randomization, and which pieces of the software

stack are being randomized: user (U), kernel (K), or hypervisor (H) code.

Table 3.4 summarizes several types of load-time randomization efforts. Some ran-

domize the base addresses of entire program resources such as the stack, heap, code,

data regions, or operating system modules [36,69,96,157]. [19] presents a comprehen-

sive set of transformations to randomize these resources and introduce entropy within

45

each such as randomizing stack variable layout and code function order. Other tech-

niques use binary analysis to create programs with additional features: [160] produces

code that randomizes itself when loaded, [75] produces code embedded with meta-

data that is interpreted by a virtualized run-time environment, and [96] permutes

instructions and data in the binary. The code can also be shuffled after the primary

loading and linking, but before the code begins execution [50]. [150] extends the other

randomization techniques by choosing addresses that contain self-validating check-

sums that can provide some measure of run-time control flow integrity. Fine grained

per-function load-time randomization is combined with compile-time transformations

in [88].

3.4 Asymmetrical Multiprocessing

The KUCS kernel design relies on separating the operating system and user applica-

tions onto separate cores. This modifies a fundamental assumption of most modern

operating system designs: that all cores are treated equally. In this status quo, the

same union of kernel and user code runs on every processor. This is symmetrical

multi-processing (SMP).

Some research has investigated the possible merits of asymmetrical multiprocess-

ing (ASMP) compared to SMP. Allocating specific software tasks to specific hardware

resources is already commonly used within computing systems. Hardware units such

as network cards or graphics processing units (GPUs) demonstrate how matching spe-

cific software tasks with specialized hardware can greatly increase the performance of

normal computation.

In fact, many multiprocessors are built with cores that have heterogeneous per-

formance. These processors allow many low-performance cores to lower the heat,

power, and cost of the chip and provide large degrees of parallelization while a few

46

high-performance cores provide valuable service for high-cost serial computation. Ex-

amples include the “Cell” microprocessor architecture used in the Sony Playstation

and the Apple A10 Fusion chipset used in the iPhone 7. Research that examines

the application of asymmetrical multiprocessing in these types of processors is plen-

tiful [16, 117, 119]. However, research that investigates asymmetric application of

software to symmetric multicore hardware resources is particularly relevant due to its

similarities to the role of the cores as proposed by the KUCS design.

Early work surrounding the introduction of multiprocessors explored a master-

slave relationship between processors, with the operating system running only on the

master core. This is summarized in [57], where Enslow writes that “[a]lthough the

master-slave type of system is simple, it is usually quite inefficient in its control and

utilization of the total system resources.” Fortunately, this assessment is unlikely

to persist in the face of the speed and quantity of modern multiprocessors. Early

attempts had limited hardware resources and could not afford to isolate a single core

for the kernel alone; the kernel shared its core with general purpose processes. This

resulted in delayed servicing of slave core requests and interruption of software on

the master core during slave core requests. With the low cost and high speed of

processor cores modern systems, these issues are less likely to plague modern KUCS

implementations.

The master-slave paradigm is also used in [85] to implement a scheme designed to

easily allow a uniprocessor operating system implementation to manage software on

a multicore processor. In particular, it provides lightweight kernel implementations

that record system calls made by applications on other cores, and a daemon that

scans for these records and requests the appropriate computation from the kernel

on the main core. This work is similar to KUCS in its concept, but because it was

designed to shoehorn a uniprocessor kernel onto a multicore processor, it does not

realize the security or performance benefits that may be possible with KUCS,.

47

The Twin-Linux project [83] examined utilizing symmetric multiprocessor cores

asymmetrically by running two completely independent instances of the Linux oper-

ating system on the same CPU - each using its own subset of the processor’s cores.

Their work provides a thorough illustration of the flexibility of x86 IPIs and processor

cores to be used in a capacity beyond their traditional use in commodity operating

systems.

AsyMOS [121] assigns cores of a symmetric multicore processor to specific tasks

such as network communication or disk I/O. These cores run the appropriate device

driver and a Lightweight Device Kernel (LDK) that implements only the functional-

ity that those drivers might need. The prototype described demonstrated improved

performance over traditional SMP operating systems. Similarly, Corey [22] explores

using specific cores to do application-specific kernel-intensive work on behalf of an

important process in parallel to the application itself.

3.5 Operating System Design Paradigm Shifts

The techniques described thus far each provide a modification to some part of the

conventional kernel design, implementation, or build process that mitigates a par-

ticular threat. There are a few approaches, however, that attempt to offer similar

security benefits by redefining the security paradigm rather than simply patching the

status quo best practices. This radical departure from the current state of the art

means that they cannot easily be compared to the previously described techniques.

In all cases, it also means that they have not yet been widely accepted.

3.5.1 Microkernels

The idea of a microkernel departs from the standard “monolithic” kernel architec-

ture by emphasizing a small codebase for the operating system kernel. There have

48

been several examples of microkernels presented in the literature such as Mach [10],

Minix [73], L4 [135], QNX [74], and many others. Bear, the platform for the projects

discussed in this thesis, is an example of a microkernel.

All microkernels aim to minimize the source code in order to decrease the likeli-

hood of vulnerabilities [130]. Additionally, a small code base allows for the possibility

of using formal analysis and formal verification techniques [18, 98]. In order to keep

the microkernel small, core functionality such as device drivers are migrated into user

level processes. Additionally, many microkernels use message-passing for all commu-

nication between two processes or a process and the kernel. This provides a narrow

and more easily verified and secured interface between components.

By exporting core functionality such as device drivers into user-space, microkernels

struggle to offer the same levels of performance as monolithic kernels. Consequently,

they have yet to replace monolithic kernels in common applications on commodity

hardware.

3.5.2 ExoKernel

The ExoKernel [56] suggests redefining the nature of the kernel entirely. Rather than

providing abstractions that the application developer can use to access hardware, the

ExoKernel provides only the thinnest possible layer to manage the multiplexing of

hardware resources. Therefore, the ExoKernel circumvents tasks normally reserved

for the kernel such as buffering network communications, interrupt or exception han-

dling, virtual memory management, and other normal kernel functions. Instead, each

individual application must define its own abstractions to handle these tasks.

Although likely to offer more security for a system overall, the ExoKernel appears

significantly complicate application development. Many of the tasks that a secure

kernel can provide to protect all processes, such as virtual memory management,

become the responsibility of the application developer. This is likely to make indi-

49

vidual applications less secure since application programmers may lack the technical

sophistication to interact directly with hardware, interrupts, atomicity, and concur-

rency. These central parts of the operating system exist to provide applications with

well-defined interfaces to this complex functionality. The ExoKernel eliminates those

interfaces by design.

3.5.3 Unikernels

Unikernels trade flexibility for security and performance by running a single process

within a single address space [111]. Eliminating the requirement to support multiple

processes and/or multiple users simplifies the code base required to implement a

unikernel and reduces the overhead required to complete a single unit of useful work.

Several examples have been deployed alongside virtualization technologies in cloud

applications [23, 97, 114]. Despite their proven usefulness for providing fast, highly

focused applications, unikernels don’t, in isolation, provide protection from most of

the attack vectors discussed in this thesis. Additionally, in order to support the

multiple-user multiple-job paradigm that conventional applications require to operate

effectively, they require a hypervisor for scheduling and other process-management

type tasks. In a sense, this is simply asking the hypervisor to act as an operating

system and the same issues with conventional operating system design will simply

move one layer deeper in the software stack.

50

Chapter 4

ExOShim

Memory disclosures in modern computer systems provide attackers with a valuable

method of discovering vulnerabilities. Figure 4.1 illustrates a realistic instance of

this type of memory disclosure. By uncovering the code stored in memory, an at-

tacker can craft Return-Oriented Programming (ROP) payloads to perform arbitrary

computation. Memory disclosures are especially important for Just-in-Time ROP

techniques that can defeat compile- and load-time code randomization defenses [151].

These same disclosures enable an attacker to reverse-engineer the source code and

design or uncover other zero-day exploits [152]. Information leakage from the kernel

is particularly concerning because a compromised kernel gives an adversary access to

all system resources.

To mitigate this threat, ExOShim uses the Extended Page Tables (EPT) provided

by the Intel VT-x instructions to configure hardware-enforced execute-only memory

access control primitives on all kernel code pages on x86 platforms. This eliminates

the capability of an attacker to dynamically, at run-time, disassemble kernel code

stored in memory. While Section 3.2 discusses several execute-only memory protection

schemes, ExOShim distinguishes itself by configuring the processor’s virtualization

features so that it can never be modified or disabled without rebooting the machine.

51

U se

r-S pa

ce Ke

rn el

-S pa

ce driver_code: ... addr <- usr_request; if ( within_device(addr) ) return *addr;

1

attacker_program: ... for (addr=0; addr<0xBEEF; addr++) *(copy+addr) = device_req(addr); build_attack(copy); 0xBEEF

2

3

1 MEMORY DISCLOSURE: Buggy driver fails to check that user-passed address lies within its memory-mapped device

2 Attacker queries buggy driver to reconstruct kernel code

3 Attacker uses copy of kernel code to attack - harvest gadgets for Kernel-ROP - reverse engineer code

Figure 4.1: Memory disclosures allow the attacker to retrieve a copy of the code executing on a system in order to reverse-engineer the implementation, locate vulner- abilities, or construct a ROP payload. This figure illustrates how a simple bug in a device driver can create a dangerous memory disclosure.

This ensures that the attacker can never disable, modify, or hijack its protection

mechanisms, even with complete control of the operating system kernel.

4.1 ExOShim: Background

Direct memory disclosures can be eliminated if kernel code is marked execute-only as

in MULTICS. Intel’s 32-bit virtual memory mechanism provided execute-only access

control primitives with segmentation [159]. Unfortunately, although more flexible

in many ways, the current Intel virtual memory model, based on paging, does not

support execute-only memory protection [79]. While side channel attacks have been

developed to exploit this fact [59], these too can be mitigated through execute-only

protection of code.

4.1.1 The Kernel and Virtual Memory

Although modules might be added or removed at runtime and virtual addresses to

kernel functionality may change, kernel code is generally loaded once and only once.

As a result, after initial bootstrapping, its location in physical memory does not

change. The kernel therefore represents high-value target that, when coupled with

52

its persistent nature, makes it an important resource to protect.

In general, the kernel provides applications with an interface to the hardware;

every application needs to access the functionality of the kernel in order to run on

the processor. In most modern operating systems the kernel is mapped directly into

the address space of every process. The virtual memory abstraction allows for each of

these “instances” of the kernel to resolve to a single copy of the kernel binary, stored in

physical memory. On modern Intel 64-bit processors, the virtual memory abstraction

is achieved by traversing a 4-level deep page table [79] as shown in Figure 2.1 on

Page 14 in Section 2.1.

The entries in this paging structure contain bits that describe the permissions

required to access all memory associated with the entry. These bits include a no-

execute (NX) bit, a supervisor bit (U/S) and a write bit (R/W). Unfortunately,

read permissions are implied as long as the privilege level requirement is satisfied.

This means that although the virtual memory abstraction does provide some access

control primitives it is not possible to directly mark memory as execute-only in the

page tables.

4.1.2 Virtualization and the EPT

Intel’s virtualization extensions (VT-x), provided in modern commodity processors,

allow a hypervisor to abstractly represent the hardware environment. This is most

commonly used to allow multiple operating systems to operate concurrently on a

single processor.

Extended Page Tables (EPTs) are part of the hardware abstraction provided by

virtualization. These tables provide a similar abstraction to the virtual memory

abstraction discussed previously, with one major difference: while the kernel’s page

tables translate from virtual to physical addresses, the hypervisor’s EPT translates

from guest physical to real physical addresses. When virtualized, the kernel’s page

53

tables resolve to a physical address that is valid in the virtualized image of memory.

The EPT is a hardware-enforced link between the virtualized interface to system

memory and the actual system memory.

There are a few other differences between the EPT and the normal page table

that are relevant. EPT entries provide unique read (R), write (W) and execute

(X) bits that are enforced by hardware [79]. This makes it possible to mark pages

directly as execute-only. Additionally, the EPT entries can only be managed by a

hypervisor, not by a kernel. If an attacker compromises the kernel, the normal virtual

memory abstraction cannot provide any security; however, the attacker would have

to compromise the hypervisor in order to manipulate the EPT.

4.2 ExOShim: Overview

ExOShim is a lightweight hypervisor that reclaims the MULTICS-style hardware-

enforced execute-only memory protection using modern Intel virtualization technolo-

gies. It was designed as a “shim” that can be inserted beneath a running kernel,

as in [55]. A conscious effort was made to minimize its size to open the door for

formal reasoning [93] and reduce the likelihood of introducing vulnerabilities [131].

The on demand insertion process allows ExOShim to enable execute-only memory for

a running system using Extended Page Tables (EPTs).

4.2.1 Assumptions

ExOShim assumes that a trusted boot process is undertaken by the operating system

it will be deployed on [102]. The security implications associated with the race to call

vmxon [144] are well known and must be addressed within the trusted boot process.

ExOShim currently assumes that it is the first hypervisor on the system [61] and

makes no attempt to verify the absence of another hypervisor. The requirement that

54

Process A Virtual Memory Space

Process B Virtual Memory Space

Kernel Process BProcess AUnused

Virtual Memory Abstraction

Guest Physical MemoryC

od e

C od

e

C od

e

C od

e

ExOShim Extended Page Tables

Real System Physical Memory

X -O

nl y

X -O

nl y

Ex O

Sh im

Ex O

Sh im

Ex O

Sh im

Ex O

Sh im

Figure 4.2: Overview of Protections Offered by ExOShim. The EPT table identity maps memory, but provides execute only protections on kernel code pages and denies access to ExOShim pages. All other memory is marked with the most liberal possible permissions.

data sections in the kernel ELF binary are readable mandates that attention be paid

during the compilation, linking, and loading of the kernel to distinguish between code

and data. Upon the completion of the loading process, it is assumed that no code

section will share any page in memory with a data section.

4.2.2 ExOShim

The design of ExOShim is illustrated in Figure 4.2. Recall that the “shim” exists only

to provide full access control to physical memory. In particular it exists to enforce

execute-only protection on all kernel code pages.

ExOShim builds an identity map associated with all of physical memory using the

EPT structure. In other words, the output from the translation process is identical

to the input for all possible inputs. However, for kernel code pages only, both the R

55

and W bits are cleared and only the X bit is asserted. This will cause a fault on any

read or write access to the physical memory on which the kernel code resides. Most

other mappings are marked with the most liberal permissions, allowing the kernel

to enforce further protections using the normal paging mechanism. Pages used by

ExOShim itself are marked in a unique fashion that is discussed in Section 4.3.2.

In order for ExOShim to build the correct EPT structure, it must have access to

the addresses where kernel code is loaded. This would be a challenge if ExOShim

were booted onto the hardware first, initialized itself, and then allowed the kernel

to bootstrap itself in the virtualized environment it creates. Although this is the

conventional way to think about virtualization, this method requires that ExOShim

initialize its EPT without knowing where the kernel will eventually load code. Un-

fortunately, detecting which pages hold kernel code from a hypervisor is a non-trivial

forensics task. Instead ExOShim uses a method developed by Embleton et al [55]

that allows a hypervisor to install itself under a running kernel.

During bootstrapping, the kernel creates a single kernel-level ExOShim process.

When scheduled, this process builds an appropriate EPT and queries the kernel to

locate where its code has been loaded. After this initialization, it turns on the virtu-

alization features of the Intel processor and launches a virtual machine whose state

matches the state of the kernel prior to scheduling the ExOShim process. The re-

sult is that the kernel continues execution as a virtual machine with the protections

enforced by the underlying EPT.

If compromised, ExOShim would provide a dangerous security risk. For this

reason, ExOShim is implemented in a manner that ensures it cannot be reconfigured

or turned off. Intel’s virtualization extensions allow a hypervisor to respond to events

in the guest. However, ExOShim explicitly prevents any event eligible to be handled

by the kernel from triggering an exit to the hypervisor. Additionally, any event that

must trap to the hypervisor (such as a violation of the EPT permissions) results in

56

an immediate assumption of compromise of the kernel. Depending on the goals of

the system, this could trigger a reboot, a halt, a logging feature, or any number of

other responses. It explicitly does not, however, return control to the kernel. In other

words, ExOShim does not take any input from the kernel and consequently presents

a hardened target without a viable attack vector.

Although it does not accept input of any kind from the kernel after initialization,

ExOShim also enforces additional protection measures. When a kernel is booted on

top of a hypervisor, it is presented, at initialization time, with a modified image of

system memory that does not include the hypervisor code and data. Since ExOShim

is loaded only after the kernel has already been initialized, it exists in memory that

has already been observed by the kernel. As such, the kernel could alter the mem-

ory used by ExOShim after initialization. To mitigate this vulnerability, ExOShim

marks all pages containing its own code or data as non-readable, non-writable, and

non-executable in the EPT, thereby ensuring that access to ExOShim code or data

is denied. Memory marked as completely untouchable in the EPT includes: the Ex-

OShim initialization and error handling code, the ExOShim’s call stack, structures

necessary for initializing the virtual machine context, memory on which the EPT is

stored, and the paging structures of the ExOShim kernel process. Any attempt to

read, write, or execute any memory related to ExOShim after its initialization will

be denied.

Obviously, the protections on memory exerted by ExOShim could be used to deny

service to the kernel. However, any memory access that would cause ExOShim to

be invoked already implies a dangerous security flaw in the operating system: For

example, under normal operation, the kernel should not be reading its own code.

Another method that could be used to invoke ExOShim is attempting to access the

memory being used by ExOShim itself. Since the kernel initializes the ExOShim

process, all memory used by ExOShim is marked as “taken” in the kernel’s internal

57

bookkeeping. Therefore, there is no scenario in which normal operation of the system

would result in ExOShim detecting a violation of its own access control primitives. In

other words, any scenario in which ExOShim is invoked is a clear sign of compromise,

and service ought to be denied to the kernel in the interests of security.

4.3 ExOShim: Implementation

Since ExOShim is an operating system feature that utilizes virtualization, its imple-

mentation will by highly dependent on the particular kernel and processor on which

it is used. However, all implementations must tackle the same core tasks: provid-

ing a context in which the ExOShim initialization routine is to execute, building an

EPT structure, enabling virtualization, and resuming kernel execution. This section

describes these core tasks, and Algorithm 4.1 lays out psuedocode of the procedure

required.

4.3.1 Providing ExOShim a Context

ExOShim does not require special consideration by the kernel beyond that required

by any other process. All that is required is that it executes with supervisor privileges

and in a unique virtual address space.

In order to use the Intel virtualization instructions without causing faults, the

processor must be operating in ring 0, privileged mode. This could be accomplished

by loading ExOShim as a binary but allowing it special privileges beyond a typical

user-space process. Some operating systems provide the notion of a kernel module to

accomplish this type of task. Alternatively, ExOShim could be implemented directly

in the kernel. As long as the ExOShim functionality is not located on pages shared

by other memory (as stated in the assumptions in Section 4.2.1) and a unique virtual

address space is created before jumping to the ExOShim initialization routine, it can

58

Algorithm 4.1: Psuedocode describing the ExOShim initialization routine. Steps are elaborated upon and described in detail throughout Section 4.3

1 Remove mappings to kernel heap; 2 Flush TLB; 3 Initialize arrays of frames for shim (As), kernel code (Ak), and non cacheable

memory (Anc); 4 Reserve memory for EPT structure; 5 As ← shim function code and data frames; 6 As ← shim context paging structure frames; 7 As ← shim context stack memory frames; 8 As ← frames to be used for EPT; 9 Anc ← memory mapped IOAPIC address;

10 Anc ← memory mapped APIC address; 11 Anc ← VGA memory addresses; 12 Anc ← memory mapped IO addresses; 13 Ak ← all kernel code frames; 14 Build EPT; 15 Populate VMCS; 16 Free As, Ak, Anc; 17 Set stack base pointer for next process (rbp); 18 vmlaunch;

be compiled into the kernel. For the proof-of-concept prototype described in this

chapter, all ExOShim functionality was compiled into the kernel directly.

It is important to understand why it is necessary to provide a unique virtual

address space for ExOShim. Some functionality must be provided to handle an event

in which a memory access breaks the primitives of ExOShim. This functionality is

invoked by hardware in the event of an EPT violation. A set of page tables provides

access to this functionality through the virtual memory abstraction. A unique set of

page tables for ExOShim allows for the frames of memory where these page tables

are stored to be marked as non-readable, non- writable, and non-executable in the

EPT. This prohibits the kernel from manipulating the paging structures that will be

used to find the fault- handling functionality if ExOShim is invoked.

59

4.3.2 Building the EPT

The Intel manuals clearly show how to format memory to build a four-level EPT.

C code from the ExOShim prototype implementation is shown in Snippet 4.1. The

task is simplified by the fact that ExOShim’s EPT is simply an identity map of all

available physical memory. The kernel should therefore return the correct amount of

physical memory when queried appropriately.

The challenge here for ExOShim is in determining the permissions to apply to

each entry in the EPT. The default permissions are read, write, and execute. The

most liberal permissions in the EPT allow for the kernel to continue with its own

memory management and access control. Enabling caching by default is also impor-

tant for performance reasons. However, there are three cases where the permissions

will differ. These are kernel code frames, ExOShim frames, and volatile (uncacheable)

frames. Line 3 in Algorithm 4.1 discusses initialization of arrays to store these types

of memory. After they’ve all been populated in lines 5-13, line 14 calls for the actual

construction of the EPT.

Kernel Code Frames

As stated in the assumptions shown in Section 4.2.1, kernel code is loaded on frames

of memory that contain only kernel code. Each such frame should be marked in the

EPT as non-readable, non-writable, but executable. This is the entire premise of

ExOShim.

The kernel must provide a mechanism with which ExOShim can locate all frames

of memory that contain kernel code. In the case of the prototype described here,

kernel code frames are identified by a unique configuration of a process’ page table;

kernel code frames are marked as present, global (for caching purposes), executable,

non-writable, and supervisor-mode only. No frame on the system can be marked

with these permissions and not be a kernel code frame, and every kernel code frame is

60

/∗ bu i ld an EPT. Use frames from the ept f rames s e c t i o n o f shim frames . I d en t i t y map except s p e c i a l pe rmi s s i on s f o r frames in kern , shim , or nocache ar rays ∗/

pml4t = map phys to temp virt ( shim frames [ ept f rames ++]); f o r ( pml4t idx = 0 ;

( u i n t 64 t ) idx2vaddr ( pml4t idx , pdpt idx , pd idx , p t idx ) <= memory present && pml4t idx < 512 ;

pml4t idx++ ) {

/∗ get a new PDPT and i n i t i a l i z e PML4T entry ∗/ pdpt = map phys to temp virt ( shim frames [ ept f rames ] ) ; ( ( u i n t 64 t ∗) pml4t ) [ pml4t idx ] = ( shim frames [ ept f rames++] & ˜0 x f f f ) | 0x7 ; f o r ( pdpt idx = 0 ;

( u i n t 64 t ) idx2vaddr ( pml4t idx , pdpt idx , pd idx , p t idx ) <= memory present && pdpt idx < 512 ;

pdpt idx++ ) {

/∗ get a new PD and i n i t i a l i z e the pdpt entry ∗/ pd = map phys to temp virt ( shim frames [ ept f rames ] ) ; ( ( u i n t 64 t ∗) pdpt ) [ pdpt idx ] = ( shim frames [ ept f rames++] & ˜0 x f f f ) | 0x7 ; f o r ( pd idx = 0 ;

( u i n t 64 t ) idx2vaddr ( pml4t idx , pdpt idx , pd idx , p t idx ) <= memory present && pd idx < 512 ;

pd idx++ ) {

/∗ get a new PT and i n i t i a l i z e the pd entry ∗/ pt = map phys to temp virt ( shim frames [ ept f rames ] ) ; ( ( u i n t 64 t ∗)pd ) [ pd idx ] = ( shim frames [ ept f rames++] & ˜0 x f f f ) | 0x7 ; f o r ( p t idx = 0 ;

( u i n t 64 t ) idx2vaddr ( pml4t idx , pdpt idx , pd idx , p t idx ) <= memory present && pt idx < 512 ;

p t idx++ ) {

/∗ i n i t i a l i z e the pt entry ∗/ ( ( u i n t 64 t ∗) pt ) [ p t idx ] =

( u in t 64 t ) idx2vaddr ( pml4t idx , pdpt idx , pd idx , p t idx ) | 0x37 ;

/∗ s t r i p out chaching i f noncacheable ∗/ f o r ( i = 0 ; i < nocache f rames idx ; i++ ) {

i f ( idx2vaddr ( pml4t idx , pdpt idx , pd idx , p t idx ) == nocache frames [ i ] ) { ( ( u i n t 64 t ∗) pt ) [ p t idx ] &= ˜( u in t 64 t )0 x30 ; break ;

} }

/∗ s t r i p out read /wr i t e pe rmi s s i ons i f k e rne l page ∗/ f o r ( i = 0 ; i < ke rn page s idx ; i++ ) {

i f ( idx2vaddr ( pml4t idx , pdpt idx , pd idx , p t idx ) == kern pages [ i ] ) { ( ( u i n t 64 t ∗) pt ) [ p t idx ] &= ˜( u in t 64 t )0 x3 ; ( ( u i n t 64 t ∗) pt ) [ p t idx ] |= 0x4 ; break ;

} }

/∗ s t r i p out a l l pe rmi s s i on s i f shim page ∗/ f o r ( i = 0 ; i < sh im frames idx ; i++ ) {

i f ( idx2vaddr ( pml4t idx , pdpt idx , pd idx , p t idx ) == shim frames [ i ] ) { ( ( u i n t 64 t ∗) pt ) [ p t idx ] &= ˜( u in t 64 t )0 x7 ; break ;

} } } } } }

Snippet 4.1: C code for building ExOShim’s EPT table. The table is an identity map, but frames that are kernel code, used for the shim, or uncacheable are set with special permissions. Cleanup from this routine should include deleting all temporary virtual mappings

61

marked with these permissions. With access to a paging structure for any process into

which the kernel is fully mapped, ExOShim can thereby infer which frames contain

kernel code, shown in line 13 of Algorithm 4.1. Subsequently, ExOShim can mark all

corresponding frames in the EPT as execute-only at the time of EPT creation.

ExOShim Frames

ExOShim is designed so that an attacker cannot disable, modify, or hijack its pro-

tection mechanism after initialization no matter how deeply the rest of the system

is compromised. In order to achieve this, ExOShim does not take input after initial-

ization time and, more importantly, marks all ExOShim memory, as non-readable,

non-writable, and non-executable in the EPT.

Care is needed to thoroughly and systematically mark all memory needed by

ExOShim with no permissions in the EPT. All of the frames supporting each of the

following parts of ExOShim need to be protected in order to prevent an attacker from

tampering with it.

ExOShim Code Preventing the kernel from accessing ExOShim code (line 5 in

Algorithm 4.1) is possible only when all its code is loaded onto frames with no other

data. While having access to the code might allow an attacker to attempt to install

a second virtualized monitor, since ExOShim has already been instantiated, this op-

eration would fail and ExOShim would consequently enter its fault handling routine.

However, a compromised kernel could overwrite ExOShim’s error handling routine if

it is not marked as inaccessible in the EPT.

ExOShim Call Stack When a fault occurs and ExOShim is invoked, it needs a

stack to operate. The address of this stack is loaded by the ExOShim initialization

routine. If the stack were not protected, an attacker could store a ROP style payload

on the stack, cause an ExOShim fault, and run arbitrary code without the protection

62

offered by ExOShim. By disabling kernel access to the ExOShim stack using the EPT,

this attack vector is closed to the attacker. The call stack is reserved as protected

shim memory in line 7 of Algorithm 4.1.

ExOShim Data There is certain data that ExOShim needs to maintain during its

initialization routine. This includes the list of kernel, ExOShim, and uncacheable

frames that need special handling and virtualization- specific structures as discussed

in Section 4.3.3. If an adversary could tamper with this data, it would be possible to

compromise the state of ExOShim. To prevent this, the data is marked as inaccessible

in the EPT. However, this cannot be achieved if the data is stored in the kernel’s heap.

In order to guarantee that no ExOShim data is stored in the heap, the prototype

unmaps the heap from the ExOShim context on entry to its initialization routine,

as shown in lines 1 and 2 of Algorithm 4.1. As a result, when ExOShim needs

memory, it must use the underlying kernel memory management layer to request new

frames. These are then marked appropriately in the EPT. This is shown in line 5 of

Algorithm 4.1.

The EPT Perhaps the most important memory that the kernel must be prevented

from accessing is the EPT tables themselves. These are frames of memory that the

hardware traverses in order to uncover the permissions associated with a particular

frame. If an attacker could modify these, it would be possible to disable or hijack

ExOShim. Therefore, all frames used for the construction of the EPT structures

must be marked in the EPT as untouchable as shown in line 6 of Algorithm 4.1. Any

attempt to read, write, or execute any of the memory in the EPT structure will cause

ExOShim to interrupt the kernel.

ExOShim Paging Structures Finally, line 8 in Algorithm 4.1 shows that the

paging structures that define the ExOShim context itself must be set with null per-

63

missions in the EPT. When an EPT violation occurs, the hardware switches into the

non-virtualized context of the ExOShim and directs control to the pre-defined fault

handler. If the attacker could modify the page tables that define this context, it

would be possible to disable or hijack ExOShim. Marking all of the paging structures

that define ExOShim ensures that in the event of a fault, the code that is executed

is exactly what was intended at initialization time.

Uncacheable Frames

The abundance of internal processor cache memory improves performance of all sys-

tem tasks. The Memory Management Unit (MMU) manages what memory addresses

are contained in and outside of the cache internal to the processor boundary. This is

especially important when traversing the four level paging structures of the virtual

memory or the EPT system. Having these structures internal to cache reduces page

translation times for the MMU. However, there are certain system memory pages

contained in these structures that must never be maintained in the cache. If these

pages are cached, read operations may return stale or incorrect information about

the state of the page. The particular pages that must be considered will vary on

each kernel being protected. Those described here serve as a guide to the types of

pages that must be considered. For ExOShim on Bear, pages that must be marked

uncacheable in the creation of the identity mapped EPT are concerned with the VGA

driver, Interrupts, and Memory Registers associated with the Network Card. These

correspond to lines 11, 9 and 10, and 12 in Algorithm 4.1, respectively.

The prototype kernel uses legacy Basic Input/Output System support for its VGA

driver. This is a block of low memory, which when written into, determines what

characters, color, and screen position to place information on the monitor. If this

memory is cached, when the processor accesses the VGA memory, the MMU will not

traverse the EPT, but access the internal cache. When the cache page is accessed the

64

last character or configuration setting will be returned to the processor, which will

result in an undefined opcode exception. When these pages are marked uncacheable

the MMU traverses the EPT and is forced to go to external memory to find the latest

information contained in that memory page.

For interrupt handling the Advanced Programmable Interrupt Controller (APIC)

and Input/Output APIC (IOAPIC) are used. The processor determines the type of

interrupt that occurs by reading memory mapped registers on pages that are written

to by these controllers. These pages refer to internal control logic rather than memory.

However, when caching is enabled, just as in the VGA memory, the processor inter-

prets these pages as actual memory. Thus, instead of fetching information internal

to the processor the MMU attempts to access information contained on these pages,

which results in a page fault. Disabling caching of these pages forces the processor to

read its internal control structures, as the interrupt subsystem requires.

For the network card to perform Direct Memory Access (DMA), it also makes use

of memory-mapped registers called Memory Mapped Input/Output (MMIO) regis-

ters. The network card reports to the PCI subsystem what virtual memory pages it

requires for MMIO registers. The prototype kernel writes to these virtual memory

pages to program the network card for DMA. However, for the network card to be

aware of these changes it must poll these registers through the actual physical mem-

ory. However, if the pages are contained in the cache, the card will never receive

updated programming from the kernel on how to perform DMA. Forcing the pages

associated with MMIO registers to be uncacheable allows the network card to receive

the necessary information for DMA.

4.3.3 Starting Virtualization

Virtualization as seen on modern systems is typically described in terms of Type-1 or

Type-2 hypervisors. A Type-1 hypervisor runs directly on top of the hardware and

65

acts as an abstraction layer between the guest and the hardware. Type-2 hypervisors

run on top of an operating system and use system hooks into the operating system

kernel to enable the running of guest operating systems. ExOShim is unique in that it

must insert itself as a Type-1 hypervisor after the kernel has already started running.

This, and the requirement to protect kernel memory, requires some special handling

while enabling virtualization features.

Required Data Structures

Intel processors support a wide range of virtualization features and settings. The pro-

gramming of these is accomplished through the Virtual Machine Control Structure

(VMCS), shown on line 15 of Algorithm 4.1. To support any level of guest abstraction

the VMCS contains over one hundred configurable fields. These fields can be subdi-

vided into Execution Control, Exit Control, Entry Control, Host-state, Guest-State,

and Exit Information fields. ExOShim programs the control fields with only those

settings required to enable EPT and exit on catastrophic failure. It does not use Exit

Information fields as they are populated on vmexit; ExOShim simply halts execution

if a vmexit occurs. How ExOShim uses the Host-State (hypervisor) fields and the

Guest-State fields requires more detailed consideration.

Host VMCS State

The Host-State fields in the VMCS define the context of the hypervisor when control

is transferred from the guest to hypervisor during a vmexit. For ExOShim, they

contain the information needed to run an exit handler upon hypervisor reentry. In a

Type-1 hypervisor the CR registers, selectors, Descriptor Tables, and stack are setup

by the hypervisor itself and then programmed into the Host-State fields. ExOShim’s

unique status “between” Type-1 and Type-2 is advantageous in this case. The kernel

has already initialized these fields. Thus, they can be read from memory and loaded

66

into the Host-State fields. There is only one unique Host-state field: the location

of the exit handler to call when a vmexit occurs. This field is straightforward to

initialize, the address of the appropriate exit handler function is written directly into

the appropriate Host-State field.

Guest VMCS State

Typically, this is the initial state needed by a kernel to begin operation. In this

case, the kernel is already operating. Instead of launching another new kernel, the

goal of ExOShim is to let the existing kernel resume as if ExOShim had never been

initialized.

If ExOShim were a normal process, it would yield to the next process. This in-

volves saving and suspending the kernel context of the yielding process, and restoring

the context of the next process to be scheduled. ExOShim aims to emulate this proce-

dure; the guest instruction pointer (RIP), stack pointer (RSP) and paging structures

(CR3) are extracted from the next process’ saved state, accessed by invoking the ker-

nel scheduler. By invoking the scheduler and setting the guest state to match that of

the next process to run, ExOShim enables the kernel to carry on undisturbed when it

launches the virtual machine. One important note included in line 17 of Algorithm 4.1

is that the VMCS does not have a field for the guest stack base pointer (RBP), so

the base pointer must be manually set to that of the guest just before launching the

VM.

Additional challenges associated with the Guest-State fields are the Global De-

scriptor Table (GDT) and Task State Segment (TSS). Unlike the Host-State fields,

the guest requires significant detail regarding the information contained in the GDT

pertaining to access rights and segmentation. ExOShim must restore the state found

in the kernel exactly. If a single bit in this field does not match the running guest

state, the guest cannot resume operation. For Type-1 and Type-2 hypervisors the

67

guest can populate these fields for the hypervisor. The nature of ExOShim’s insertion

process makes this impossible. Fortunately, the GDT containing this information can

be read from memory using the sgdt instruction. From there it can be dissected and

loaded into the necessary Guest-State fields. The TSS information for guest operation

is contained at a set offset inside of the GDT.

Launching the VM (Resuming the Kernel)

To return operation to the kernel from ExOShim, a vmlaunch instruction must be

executed to change the processor’s operating context, as shown in the final line of

Algorithm 4.1. Upon execution of this instruction, the Transition Lookaside Buffer

(TLB) is flushed to prevent accidental, or unauthorized, guest and hypervisor sharing.

The processor then reads the data stored in the VMCS fields to ensure they are in a

legal configuration. Upon passing these checks the processor then resumes operation

of the guest context, which is the next process to be scheduled by the kernel.

From this point forward ExOShim will only run again in one of three cases: The

first is an unlikely catastrophic failure in the guest’s interrupt system, which will

cause a triple fault and vmexit. Without a hypervisor, the processor would hard

freeze in this case. The second case occurs if the guest attempts to read or write a

protected execute-only kernel code page. This will result in a vmexit associated with

an EPT violation. Finally, if the guest attempts to read, write, or execute a protected

ExOShim page; this will again result in a vmexit due to an EPT violation.

In the event that any of these conditions occur, the ExOShim vmexit handler is

invoked. In the prototype, this routine executes a processor halt instruction immedi-

ately. However, the handler could also preform other actions such as log the failure

or restart the kernel from a known gold-standard.

68

4.4 ExOShim: Evaluation and Analysis

In this section, the complexity of the ExOShim prototype is evaluated by counting its

lines of code; it is well known that the number of vulnerabilities is directly proportional

to lines of code [131]. The prototype’s performance is examined by comparing the

cycles consumed by the processor on a standard test suite, running on the same kernel,

both with and without ExOShim. Finally, a qualitative discussion of the threats that

ExOShim seeks to combat explores the protections offered by the prototype.

4.4.1 Prototype Complexity

ExOShim is implemented in terms of only two functions: an initialization/installation

function and the exit handler function. The initialization function requires some

memory management code, a handful of routines to detect the 3 types of special

memory by the EPT as discussed in Section 4.3.2, and initialization for the VMCS

structure. All told, the initialization routine comprises 290 lines of code. The simple

handler that deals with terminating execution if the kernel violates any of the EPT

permissions set forth adds only 35 lines of code. However, these routines rest upon

kernel functions already present in Bear. As a point of comparison regarding the size

of attack surface, the popular Xen hypervisor is approximately 150,000 LOC while

ExOShim totals 325 LOC.

4.4.2 Performance

Performance benchmarks were run on a Dell Optiplex 9010 workstation with 4 GB of

RAM and an Intel core i7-3770k quad-core 3.5 GHz processor with an 8 MB cache.

In addition to the industry-standard AIM9 arithmetic [6] and malloc [103] tests, a

macro-scale benchmark suite packaged with [123] was included. This test suite con-

sists of a series of tests that exercise different parts of the kernel. This primarily

69

Test (CPU Cycles) Multiply (×1010) Add (×1010) Divide (×1011) malloc (×1011)

No ExOShim Max 2.2214 1.3435 1.0733 1.1227 Min 2.2213 1.3248 1.0733 1.1225 Avg 2.2214 1.3364 1.0733 1.1226

ExOShim Max 2.2215 1.3392 1.0734 1.1228 Min 2.2214 1.3285 1.0734 1.1226 Avg 2.2215 1.3315 1.0734 1.1227

Average Performance Cost (%) 0.005 -0.4 0.01 0.01

Table 4.1: AIM9 Benchmark Suite [6] and Malloc Test [103] Performance Measure- ments - 100 Trials per test (column). Each test reveals a negligible performance impact.

consists of memory intensive queue, heap, hashmap, and inter-process communica-

tion tests. Network tests were not included in the test suite in order to eliminate

non-deterministic network based latencies from hiding the effects of ExOShim on

performance.

Table 4.1 shows that the AIM9 [6] and malloc [103] benchmarks reveal truly neg-

ligible performance costs. This is a startling testament to modern caching technology.

Without ExOShim, every memory access uses a 4-step page table translation mecha-

nism (if not found in the TLB). With the EPT from ExOShim, however, each of those

4 steps must go through its own 4-step EPT walk (if not cached). To accomplish this

additional level of indirection with such little performance cost is truly impressive.

While there may be a risk of exhausting the EPT’s cache layer, the memory-intensive

malloc benchmark test did show any such tendency.

In order to test the system on a more macro-scale, the memory-intensive Bear

test suite was run 425 times on the same machine – once with ExOShim installed

and once without. In each instance of each test case, the clock cycles needed to

complete all tests in the suite was recorded. The results of these tests are shown as

boxplots in Figure 4.3. Note that the extreme outliers in Figure 4.3a hide detail from

the boxplots; Figure 4.3b shows a closer view of the boxplots, hiding the extreme

outliers.

The overall performance cost of ExOShim is estimated by comparing the average

cycle count for a full test suite iteration without ExOShim to that with ExOShim. A

70

With Shim Without Shim

5. 0e +1 0

1. 0e +1 1

1. 5e +1 1

2. 0e +1 1

C lo

ck C

yc le

s

(a) Outliers Included

With Shim Without Shim

3. 8e +1 0

4. 0e +1 0

4. 2e +1 0

4. 4e +1 0

C lo

ck C

yc le

s

(b) Outliers Excluded

Figure 4.3: Boxplots of Bear’s Benchmark Suite Performance. Plots are shown both with and without the observed outliers included. Because the outliers were found in both the experimental and the control test case, they clearly represent and unexpected artifact of the underlying system.

summary of the test results can be found in Table 4.2, shown both with and without

the outliers.

Even with the outliers included there is only a 4.85% performance cost for using

ExOShim to make the kernel code execute-only. However, the outliers appear both

with and without ExOShim. Clearly, they represent an unanticipated artifact of the

test suite or kernel. Without the outliers, the performance overhead drops to a nearly

negligible 0.86%.

4.4.3 Security Implications

The security benefits of using ExOShim are clear: it substantially increases attacker

workload. Without read permission on kernel code pages, traditional memory disclo-

sure bugs at the kernel level are obsolete. This mitigates an entire range of information

leakage-based attacks, including various forms of ROP. An execute-only kernel binary

71

Test Case Without ExOShim With ExOShim Outliers Included No Yes No Yes

CPU Cycles (×1010) Max 4.492 20.64 4.511 20.67 Min 3.686 3.686 3.706 3.706 Avg 3.850 3.921 3.883 4.111

Performance Cost (%) 0.86 4.85

Table 4.2: Performance Benchmark Test Results. Benchmark test suite is packaged with Bear and includes many varied test cases, approximating throughput for a nor- mal general-purpose system load. Data is listed for cases with and without including the outliers found in the data.

also inhibits most reverse engineering techniques, which rely on reading code.

Even with ExOShim, attackers can still learn some facts regarding the layout of

the address space. For example, it is possible to read return addresses off of the

stack and function pointers stored in data to learn about the location of code. This

has been used to construct JIT-ROP attacks [49]. However, a system that employs

fine-grained function and block level diversity [86] devalues this information. With

thorough code randomization, an attacker must read the code in order to know exactly

where gadgets are located; finding the entry point to a function is not sufficient.

An added security benefit of using ExOShim comes simply from turning on virtu-

alization. The first entity to enable virtualization owns the processor [144]. Without

a virtualized component, an operating system running on bare metal is vulnerable to

VMM rootkits such as [55]. With ExOShim initialized, any attempt to install such a

rootkit would invoke the ExOShim exception handler.

4.5 ExOShim: Conclusion

4.5.1 Future Work

There are many possible extensions of the current prototype that can be explored

further.

72

The current prototype of ExOShim replaces the hypervisor otherwise packaged

with [123]. This defeats the opportunity for more complex tasks being executed in

the hypervisor. Fortunately, Intel virtualization technology allows nested virtualiza-

tion. This would allow for the shim to initialize underneath the kernel, even if the

kernel is already operating on top of another hypervisor. Support for this capability

requires adding functionality to the underlying hypervisor, not to ExOShim. Addi-

tionally, ExOShim-like protections can be added to the EPT tables of any hypervisor.

However, because the more complex implementation will take input, either of these

solutions sacrifices the desirable property that ExOShim cannot be modified, hijacked,

or disabled.

Although the microkernel is a full 64-bit multicore implementation, ExOShim

currently operates on a single core. To generalize the implementation will require

initializing ExOShim once per core; each core must be virtualized independently. It

would also be possible to place different permissions in the ExOShim EPTs with

different cores assigned to specific tasks.

Currently, devices that support Direct Memory Access (DMA) are a known threat

to the protection provided by ExOShim. These devices make memory requests with-

out passing through the EPT translation provided by ExOShim. Intel’s VT-d vir-

tualization extensions are designed to eliminate this problem by providing a table

structure similar to the EPT that defines a translation mechanism through with

these devices must pass. In fact, the same memory can be used for both the EPT and

the VT-d tables. Therefore, ExOShim can eliminate the DMA threat by enabling

and configuring VT-d’s DMA remapping functionality.

4.5.2 Final Thoughts

ExOShim is a novel technique in which an operating system deploys a thin “shim”

virtualization layer that uses Intel’s EPT hardware to provide execute-only protection

73

to all kernel code. This prevents all direct memory disclosure scenarios. The attacker

cannot read kernel code directly. Thus, an entire class of kernel-level attack vectors

is denied by prohibiting the attacker from disassembling kernel code pages to find

gadgets for any type of code reuse attack.

The method illustrated by ExOShim relies on kernel code being loaded onto unique

pages. This can be performed at compile- or load-time, and can be combined with

software diversity and randomization. When combined with function and block level

diversity, even an indirect memory disclosure (such as reading a return address from

the kernel stack) does not provide sufficient information to harvest gadgets. The

ExOShim prototype discussed here operates in conjunction with Überdiversity and

KPLT.

This work demonstrates that modern commodity hardware does support a hardware-

enforced execute-only primitive that can be deployed to prevent kernel level exploits.

It also shows that the flexibility of Intel’s virtualization technologies allows the shim

layer to protect itself from being disabled or hijacked by an attacker.

The 325-line ExOShim prototype denies an entire class of common attacks at

the kernel level while significantly increasing the workload associated with surveil-

lance, reverse engineering, and exploit development. Due to caching technologies in

modern Intel processors, the ExOShim prototype here was measured to introduce a

performance cost of only 0.86%.

74

Chapter 5

Diversification: KPLT

In Bear, one important security feature is the absence of shared objects. Despite

the protections offered by methods such as copy-on-write (CoW), Bear chooses a

strict security model in which there is no shared memory between processes. Process

communication happens via an MPI-based message passing interface. The fact that

each process has its own copy of all executable code easily allows the same code

for each process to be loaded in two unique locations, increasing the diversity of the

system to include unique images per-process. However, the kernel is rarely considered

in the same way as user processes. It is standard practice for the kernel to be mapped

into the virtual address space of every process. After all, every process needs access

to the kernel’s functionality. The kernel is never considered “shared” memory in the

conventional sense, but upon closer inspection, the kernel does show similar attributes.

More precisely, the kernel is commonly mapped in the same location for every user

process. The idea of treating the kernel like a shared object is explored in this chapter.

By setting up a Kernel-level Procedure Linkeage Table (KPLT), the diversity of the

system was increased by mapping the same (shared) kernel functions into a different

virtual address for each process.

While Section 3.3 reviews prior work in the field of software diversificaton, this

75

chapter explains the framework and mechanisms that allow the KPLT to increase

diversity by changing the addresses of kernel functions on a per-process basis. It

also gives a detailed account of the implementation of a prototype KPLT within the

context of the Bear operating system. Furthermore, it contains a discussion of the

general approach and its strengths and weaknesses as well as possible extensions of

the current prototype.

5.1 KPLT: Overview

The KPLT is a table that allows for the same kernel functions to be loaded at different

addresses in different processes’ address spaces. It allows the footprint of the kernel

in a process to be unique not only per-boot, but also per-process. This increases the

diversity of a system.

In this section, the core ideas and features of the KPLT are explored. The use of

the virtual memory abstraction is discussed, and the contents of the entries inside of

the KPLT are presented. These entries are documented at the byte level, in order to

see how each entry in the KPLT uniquely operates.

5.1.1 Using the Virtual Memory Abstraction

The idea behind the KPLT uses the paradigm of dynamic loading to map common

kernel functionality at different virtual addresses in each process’ address space. In

order to illustrate the mechanics involved, it is important to first look at the way

control flow would go through a series of function calls without the KPLT. The

appropriate scenario to illustrate this is a function, load elf proc, calling another

function, kputs. These two functions are used in the Bear system to load an elf

binary into user-space and print a string from kernel-space onto a serial back channel.

Figure 5.1a shows the normal control flow when these are loaded.

76

Process A Virtual Address Space

System Physical Memory

Process B Virtual Address Space

kprintf

kernel_do_exec ... call <kprintf> ...

kernel_do_exec

kprintf

1

3

2

kernel_do_exec

kprintf

1

3

2

(a) The normal path of execution when calling kputs

from load elf proc for two processes Process A Virtual

Address Space System Physical

Memory Process B Virtual

Address Space

kprintf

kernel_do_exec ... call <kprintf> ...

kernel_do_exec

kprintf

1

3

2

kernel_do_exec

kprintf

1

3

2

KPLT KPLT

KPLT_A

KPLT_B

4

5

4

5

(b) The path of execution when calling kputs from load elf proc for two processes with KPLT

Figure 5.1: Control flow shown with and without KPLT. Normally, the call instruction changes the instruction pointer directly to the address of the function. With the KPLT, the call instruction forces execution into the table, where it then jumps to the process-specific address of the function.

In Figure 5.1a, the control flow for each of Process A and Process B is laid out in

numbered arrows. A key thing to note is that the second step of the process, the call

from within load elf proc to kputs, will always resolve to the same virtual address

for each process, because each process is reading the same instructions from physical

memory. This is what requires that each process have the physical implementation

of kputs mapped into the exact same virtual memory location.

The introduction of the KPLT is shown in Figure 5.1b. It is important to note

that the second step in the control flow for each process still resolves to the same

place in memory. However, this time that place is not the implementation of kputs

but rather an entry in the KPLT. The kputs entry in the KPLT is located at the

77

same virtual address in both processes, but the virtual memory abstraction allows a

unique mapping of underlying physical frames for the same virtual address ranges in

the two different processes. The unique physical memory allows each process to read

different data at the common virtual address, which explains how the third labeled

step for each process resolves to a unique address.

Another important note is illustrated in step number 4 for each process. During

the second step, the virtual memory abstraction allowed the system to map different

physical memory to the same virtual address for two different processes. At this fourth

step, the virtual memory abstraction provides the opposite functionality; different

virtual addresses in each process resolve to the same physical memory: the physical

implementation of kputs. This allows the processes to share the kernel functionality,

but forces them to use different addresses to access it.

5.1.2 The Contents of a KPLT

Figure 5.1b shows how control flow passes through the KPLT to the process-specific

virtual address of the function requested, but it just shows a dotted line to describe

this process. In fact, the actual KPLT entry is not much more difficult than a dotted

line. The objective of the entry is to jump to a given virtual address. However, the

jumps beginning and ending points are loaded non-deterministically into the 64-bit

virtual address space.

In order to facilitate the largest possible jump, the KPLT entries move a full 64-bit

address into a register, and then jump to the address in that register. The assembly

code for this operation is shown in Figure 5.2.

movabs $0xfeedcafebabefood,%rax

jmpq *%rax

Figure 5.2: Assembly instructions for a KPLT entry that points to 0xFEEDCAFEBABEFOOD.

78

unsigned char kplte [20];

kplte [0] = 0x48;

kplte [1] = 0xb8;

for ( i = 2; i < 10; i++ )

kplte[i] = (function_addr >> ((i -2)*8)) & 0xFF;

kplte [10] = 0xff;

kplte [11] = 0xe0;

*( uint32_t *)(& kplte [12]) = function_size;

*( uint32_t *)(& kplte [16]) = function_align;

Snippet 5.1: This code snippet creates a code entry in the KPLT that jumps out of the table to the implementation of the function being sought.

At least 12 bytes are needed for the type of KPLT entry shown in Figure 5.2.

However, there is some other metadata that needs to be recorded about functions

that are routed through the KPLT. In particular, the function size and alignment re-

quirements are both useful after load-time, when such information would normally be

discarded. As such, the prototype uses an additional 8 bytes to store this information

after the byte-code shown in Figure 5.2. This makes the overall size of a KPLT entry

20 bytes. Building such an entry is shown in Snippet 5.1.

5.2 KPLT: Implementation

This section discusses the implementation details of the KPLT. There are two main

parts of the prototype implementation: the load-time part and the run-time part.

The load-time part refers to the work needed to be done at the load-time of the

kernel. This work is done by a bootloader, not the kernel itself. Meanwhile, the

run-time part occurs during the run-time of the kernel, at the load-time of each user

process.

79

5.2.1 KPLT Creation at Load-Time

In this chapter, “Load-Time” refers to the load-time of the kernel unless otherwise

noted. This section describes Bear kernel loading process for context and the work

that needs to be done to initialize the KPLT at the kernel’s load-time.

Bear’s Kernel Loading Process

The kernel is loaded by a second-stage bootloader, boot2. A first stage, boot1, brings

the processor from real mode into long mode. Then, boot2 loads and bootstraps the

ELF file that will run on the processor at steady-state; the kernel.

Because boot2 operates in long mode, it has all of the capabilities that the kernel

will have. In order to facilitate its work, it initializes a kernel-heap and a frame array,

which is made up of structures describing physical memory available on the system.

The kernel inherits these structures by reading general purpose registers as soon as it

starts running. This process is important because boot2 chooses random addresses

for the kernel heap and frame array, so the kernel must retrieve the location at which

they were stored.

In order to support diversification of the kernel binary, boot2 uses an ELF “di-

versity” loader. This loader has the capabilities of not only loading the kernel, but

also randomizing the addresses of each of its sections. The kernel is compiled with

GCC’s -ffunction sections so that every function is a section that can be loaded

dynamically.

After loading each section into a random virtual address, the diversity loader calls

two routines per loadable unit: move unit and fixup unit

The move unit function redefines the unit’s symbols to reflect its new location.

It also updates relocation entries associated with the unit.

After all units have had their symbols and relocations updated with their new

randomized values, fixup unit is called for each of the units. This routine resolves

80

external relocations for the unit. In other words, after all of the symbols have been

updated, each unit finds the updated value of the symbols it needs to resolve.

The KPLT at Kernel Load-Time

Now, the work that was needed at kernel load-time to support the KPLT prototype

will be discussed. The only tasks that need to be accomplished for the KPLT at

kernel load-time are to allocate and construct a KPLT, and point references to KPLT

functions at the table.

The facility that scans memory and picks random locations for arbitrary diversity

units is used to place and allocate a KPLT. Thus, by defining a size and alignment re-

quirement, the diversity subsystem chooses a random place within the virtual address

space to place the KPLT.

Building the KPLT and ensuring that the references are fixed to point to the table

instead of the function itself are both handled in move unit. As each unit is passed

to the routine, move unit iterates through all symbols defined by that unit. First, it

is decided whether a given symbol belongs in the KPLT. The ELF standard explains

how to make sure the symbol is the symbol of a function using the ELF64 ST TYPE

macro. The ELF metadata also determines whether the function is static using the

binding attribute.

Once a symbol is determined to belong in the KPLT, a KPLT entry is made for

it as shown in Snippet 5.1 and stored at the next empty index in the KPLT. Then,

importantly, the symbol’s value in the symbol table is set to be the address of the

KPLT entry that was just constructed and written. This way, when other functions

request the value of the function symbol during the fixup unit routine, they will be

told that they should call the function at the address of the KPLT entry. This is the

mechanism that creates the second numbered control flow transition in Figure 5.1b.

The only remaining detail is passing the address of the KPLT through to the

81

kernel from the bootloader. When the kernel is booted, it retrieves the address of the

KPLT from the appropriate register.

5.2.2 Run-time KPLT Maintenance and Manipulation

Most of the work for the KPLT comes during run-time. Specifically, whenever a new

process is created, the addresses of the functions in the KPLT are re-randomized

for the new process. Although a particular process’ KPLT could be randomized

throughout its lifetime, that was not implemented for this prototype.

New Process Creation in Bear

For both practical and security reasons, it is not straight forward to alter the memory

of another process. However, process creation is defined as (dramatically) changing

the memory of another process. To create a new process in Bear, the parent examines

all of its own virtual memory. Whenever it finds a mapped frame, it chooses what

actions to take on behalf of the new process. For example, a global supervisor page

should create a new mapping for an existing frame of memory while a user page

should map a new frame and copy the data.

If mapping a new frame is required, the parent uses the virtual memory layer

to request a page of memory at a temporary address. It uses that address to write

data as necessary to the frame, and will later delete the virtual mapping without

clearing the memory on the frame. This will remove its temporary mappings into

another process’ memory from its page tables, but will also mark the frame as being

unmapped. As such, a queue of mapped frames is maintained so that after creating

the new structures, the parent can manually update the frame array so that it knows

that the frames are actually in use by the new process.

In general, when it is found that a new process needs some memory, the parent

allocates it, constructs the appropriate linkages to create a meaningful paging struc-

82

ture using the temporary mappings to the memory, then upon completion removes

the temporary mappings, and then retroactively corrects the virtual memory layer to

leave the memory as “taken” as if the new process had requested it itself.

Working with the KPLT during Process Creation

At process creation, there are three tasks to handle with regard to the KPLT. The

implementation will be described in terms of each of these three steps.

1. Recalculate: Find new random addresses to use for the functions found in the

KPLT

2. Rebuild: Build the new KPLT with the new random addresses

3. Remap: Remap the functions found in the KPLT from the address the parent

uses (where the function was copied during process creation) to the new random

address

The entire process creation, less the freeing of the temporary mappings and the

frame array fixups, is completed before the three steps listed above. After taking care

of the KPLT, the normal cleanup previously described is completed.

Recalculate The Recalculation step involves generating new random addresses for

each function in the KPLT. This step is fairly straightforward, and is largely unrelated

to the details of process creation. Since it is known that all addresses of all the

functions in the KPLT will be changed and the size of the KPLT functions are stored

in the table itself, each KPLT entry can be read from the table in its current form

and the memory consumed by the function described by each entry is added to the

pool of available memory for placing functions. Note that this prototype does not yet

reclaim virtual memory occupied by the parent’s user pages even if the child will not

share those pages.

83

One important implementation note has to do with the alignment of the new

random addresses chosen for a given function. When a function was placed at load

time, the function was copied onto one or more 4Kb physical frames of memory at

some offset. When a new address is chosen for a function, it is selected so that the new

address by which the function is accessed is the same physical frame. This results in

loss of control over the offset from a page boundary at which the function will start,

thus forcing the address of the new function to have the same offset off of PAGE SIZE

as the original address.

Rebuild The Rebuilding step is slightly more complicated. It is easy to generate

a new KPLT entry each time a new address is discovered for a KPLT function. This

process was shown in Snippet 5.1. The challenge comes when writing this new KPLT

entry into the new process’ KPLT. Recall that the KPLT, marked supervisor and

not global, was allocated with a temporary mapping during the process creation. To

recover that temporary mapping in order to write to the new process’ KPLT, a queue

search function is used on the list of temporary mappings that were previously made.

Once the temporary mapping to the child’s KPLT is found, it could be suspected

that it is trivial to write the KPLT entry. However, the KPLT entry might cross a

page boundary. Although that is not a problem when writing to one process’ own

KPLT, the temporary mappings to the multiple pages of the child’s KPLT may not

have been made sequentially. Therefore, if the KPLT entry crosses a page boundary,

the temporary mapping queue must be searched twice in order to copy the KPLT

entry in two pieces, one copy for each page.

Remap Remapping the functions from the parent’s KPLT-specified address to the

new address specified by the child’s KPLT is the most challenging part of the process.

It can be broken up into two pieces: preparation and execution.

A single loop over all KPLT entries can do the recalculation, the rebuilding, and

84

the preparation portion of the remap step for each KPLT entry. The preparation

portion of the remap step involves unmapping the function from its parent’s KPLT-

designated address and making a record that it needs to be moved to its new address.

All unmappings need to be done before any actual remapping because two functions

could have been swapped: trying to remap one without unmapping the other would

trample important state. Although unmappings could leave unused paging structures

dangling off of the child’s page tables, this prototype ignores that risk. The virtual

memory system will reuse those structures if anything is mapped into their range in

the future, and will free them at process destruction.

After zeroing the appropriate page-table entry, an address is found or created for a

record structure to describe the physical address of the function implementation and

the virtual address by which the child will eventually wish to access it. This record

is stored into a “todo q”. The process of deleting a page table entry and saving a

record to handle later is in a do-while loop to handle functions that reside on multiple

pages.

Finally, all of the records kept in the “todo q” are traversed. For each record, the

creation of the appropriate mapping from the correct place in the child’s page tables

to the physical address of the function is made. This involves diving down one layer

at a time and either finding the current temporary mapping to the child’s paging

structure, or allocating and setting up a new chain of paging structures for the child.

Finally, when all of the todo records have been handled, the standard cleanup of

process creation can be finished. When the new child runs, the addresses in its KPLT

entries will differ from those of its parent. Consequently, it will use different virtual

addresses than its parent to access the same physical memory where kernel functions

are implemented.

85

5.3 KPLT: Evaluation and Analysis

5.3.1 Security Implications

Exploitation Resilience

The use of a KPLT to change the virtual addresses of common kernel functions on a

per-process basis has huge potential for defeating exploits. On the most basic level,

an attacker looking for a vulnerable instruction in the diversified kernel will now not

only have to search for the instruction per-boot, but also per-process.

In particular, the KPLT increases a system’s resilience to Return-Oriented Pro-

gramming (ROP) [140]. With the KPLT, the attacker’s gadgets are only valuable

within the address space of a single process. Suppose an attacker correctly guesses

the address of a gadget; if one wrong guess crashes their current process, the previ-

ously correct guesses are now invalid. This dramatically increases attacker workload

and decreases the likelihood of success for a ROP attack.

Security Concerns

The main concern with the KPLT is that it is a sort of “honey-pot.” It gathers a

lot of valuable information in one place. If an attacker needed access to n particular

functions, the KPLT makes their job only 1 n

as hard as it was before. Finding the

KPLT can give him access to all of the functions within the current address space.

Furthermore, the KPLT is in a constant location across address spaces. So, finding

the KPLT eliminates the challenge posed by randomizing kernel function addresses

on the fly.

Fortunately, there are ways to mitigate the risk that the KPLT poses. The fist has

been implemented by this prototype: randomize the location of the KPLT at boot.

This means that finding the KPLT is just as hard as finding a particular function.

For any attack that does not require access to multiple specific functions, the KPLT

86

does not help the attacker.

Lastly, there are several ways that the security concerns presented by the KPLT

can be mitigated. One of these includes using page-fault handlers or virtualization

features to implement an execute-only policy rule on the KPLT memory. This is

compatible with ExOShim, as discussed in Chapter 4. Another is to place the KPLT

into a hypervisor: the KPLT entries would be vmcalls. Finally, one could randomize

the address of the KPLT itself at run-time by maintaining and updating the symbol

relocation data and modifying the relocations at run-time.

5.3.2 Increase in Diversity

The increased diversity from the KPLT comes from changing the addresses of kernel

functions in each address space. Speaking quantitatively, this increases the number

of possible variations in a program. In Chapter 6 Überdiversity calculates that there

are as many as 108025 different variations of a basic shell that the algorithm could

produce; but that number only held true for the first shell launched between boot

ups. Subsequent shells had a fixed kernel layout, so given the layout of the first, the

number of variations of the second decreased dramatically.

The KPLT recovers some of that loss. Due to the fixed location of the KPLT itself

and the requirement that all function addresses across process address spaces have

the same offset from PAGE SIZE, it fails to bring the number of variants for the second

instance all the way back to the number for the first. However, it does increase it

dramatically. This is bad news for an attacker that wants to persist on a system and

gather information about it; the system is a much more quickly moving target with

a KPLT implementation.

87

5.3.3 Performance Cost

Although the KPLT increases the number of instructions to do the same unit of work,

that is not the main cause of performance cost. When different address spaces using

different virtual addresses for the same code, global TLB entries cannot be used. This

limits the performance enhancements of caching. There are also obvious kernel and

user load-time overheads associated with the extra work of maintaining a KPLT.

Preliminary results suggest a performance cost of around 16%. This was measured

by running the standard Bear test suite multiple times both with and without the

KPLT and comparing the clock cycles required on average during each case.

5.3.4 Remaining Work and Challenges

The KPLT prototype discussed here still needs several enhancements that require

further research. It does not yet account for static functions. Special handling for

static functions in the ELF format allow for multiple static functions with the same

name to be defined in multiple different objects. This prototype also does not support

variable number argument functions. Note that a jump table will also not work with

the KPLT. The compiler must be configured to avoid using jump tables.

5.4 KPLT: Conclusion

In conclusion, the KPLT is a strong complement to the popular ideas about ASLR

and system diversity. The prototype implementation works for all global functions

as a proof of concept. Static functions and functions with variable-length argument

lists are not yet supported. Unfortunately, the technique is not compatible with the

hardware-provided caching technique of using global pages to lock kernel pages in the

TLB. A performance cost of 16% was measured for the prototype.

88

Chapter 6

Diversification: Überdiversity

A significant concern in the modern computer security landscape is vulnerability am-

plification; since all network connected systems and programs are instances of a very

small pool of possible operating system variants and programs, a single exploit can

be reused on many machines. This includes classes of exploits such as buffer over-

flows [105], return-oriented programming (ROP) [140], return-to-libc [34], and

many more.

Vulnerability amplification multiplies the potential reward for developing a single

exploit many-fold. Injecting randomness into software systems is a very popular

technique used to limit the impact of a vulnerability as summarized in Section 3.3. In

the case of load-time diversity, the attacker’s workload is also increased dramatically

because the effects of the randomization are not easily visible. Therefore, attackers

are forced to design exploits without a priori knowledge of the layout of a system.

Load-time diversity involves loading some number of discrete units of code or data

(sections) into memory at random locations. Überdiversity follows the example set

by [163] by loading the sections at different places within the vast virtual memory

space with blank space in between them. Code sections typically represent individ-

ual functions, but additional sections include global data, the heap1, and the stack.

1More precisely, a pre-set maximum size is set aside as a special section in which the contiguous

89

Although Überdiversity does not explicitly randomize within these sections, it is con-

ceptually compatible with other projects that randomize within functions [75, 132],

the heap [125], and the stack [35, 36, 64]. The address space generated after ran-

domization is extremely unlikely to share exploitable addresses with other program

instances loaded from the same source code.

Diversification efforts are often measured and compared in terms of “entropy”.

Unfortunately, this term is often ambiguous and has had multiple interpretations

in the literature. As an example, Davi et al. [50] claims that their implementation

achieves n! entropy, where n is the number of program resources being randomized.

Although the claim that [50] can produce n! address spaces is correct, the use of the

term “entropy” in this context is fundamentally different from other works. Davi was

actually discussing program variants, and this semantic confusion is not uncommon.

Both program variants and entropy are important metrics for any diversity implemen-

tation. This study clarifies the difference between these two complementary ideas and

formalizes a method for computing the less popular program variants measurement.

Although some of the other projects discussed in Section 3.3 do use a 64-bit

platform, the highest reported entropy is 29 bits in [96]. With a close upper bound

at 47 bits, Überdiversity provides the highest entropy. This makes it particularly

interesting as an exercise studying the effects of approaching hardware limitations,

since commodity 64-bit hardware provides an address space of only 248.

None of the other studies surveyed in Section 3.3 included substantial discussions

about the algorithm used to inject randomness. This is often overlooked as a trivial

component of an implementation. In the case of simple ASLR, which might choose

a single random address for a single loaded resource, this may be a fair assessment.

However, several studies in Table 3.4 are performing more complex and fine-grained

randomization. It is easy to consider an algorithm that seems to give random outputs

heap will grow at run-time.

90

Algorithm 6.1: Greedy Diversity Loader Algorithm. This algorithm produces address spaces in which any particular section is more likely to have a low address than a high one.

Input: The Address Space A & Sections to be Loaded S Output: Random location si.location ∀s ∈ S

1 Randomly shuffle S; 2 for i = 1 to |S| do 3 Choose random k : 0 ≤ k < |A|; 4 si.location← k; 5 A← {0, 1, ..., k − 1, k};

but actually produces some address spaces with higher probabilities than others, and

non-trivial to prove that an algorithm generates all possible address spaces with equal

probability.

6.1 Diversification Algorithms

Algorithm 6.1 produces random program variants with linear running time. Due to

these attractive properties, it is the algorithm used in the implementation presented

in [86], although it is not discussed in that paper. However, it is an example of an

algorithm that clearly does not produce address spaces with uniform probability. In

it, the sequence of random numbers chosen is monotonically decreasing, therefore sec-

tions are more likely to have low addresses than high ones. An even more convincing

algorithm, suggested as a uniformly random alternative to Algorithm 6.1 at an early

stage of this research, is shown in Algorithm 6.2. This algorithm picks a list of ran-

dom numbers, each one to represent a gap between two sections loaded into memory

(or between a section and the boundaries of the virtual address space). Then, the

numbers are scaled to fit the virtual address space. Surprisingly, even this algorithm

does not produce address spaces with uniform probability.

Proof. Line 2 of Algorithm 6.2 generates R, a set of |S|+ 1 random integers. Line 3

transforms R into T using a scaling routine, f(), so that the sum of the size of the

91

Algorithm 6.2: Scaling Diversity Loader Algorithm. This algorithm chooses enough random numbers for each to be a gap between two sections, then scales the random numbers to fit the available address space.

Input: The Address Space A & Sections to be Loaded S Output: Random location si.location ∀s ∈ S

1 Randomly shuffle S; 2 Choose |S|+ 1 random integers from 0 to RAND MAX {r1, r2, ..., r|S|+1} = R;

3 T ← f(R) where f(R) = ⌊ R×

( |A|−

∑ s∈S s.size∑ r∈R r

)⌋ ;

4 s1.location← t1; 5 for i = 2 to |S| do 6 si.location← si−1.location + ti;

sections plus the random numbers in T will fill the address space entirely. Then, the

|S|+ 1 scaled random numbers in T become the gaps between the sections when they

are loaded into memory.

The vital observation is that T to R is not a 1:1 mapping. If f(R) = T , then

f(2R) = T , f(3R) = T , etc. However, each T results in exactly 1 possible address

space. Let f−1(T ) be set of all sets of R such that f(R) = T . So, f−1(T ) = {R :

f(R) = T}. Since all possible sets R are equally likely and each T generates a unique

address space, uniform randomness implies that, given T , @T ′ : |f−1(T )| 6= |f−1(T ′)|.

Unfortunately, this is not true. Intuitively, if a set of numbers in R is not big

enough to fill the address space and needs to be scaled up, then the resulting numbers

in T will be larger. In other words, the process of scaling means that address space

layouts with small gaps between sections are less likely than those with large gaps

between sections. More formally, consider a set T in which all members are even.

In this set, f−1(T ) ⊇ {0.5T, T, 2T, 3T, 4T, ...}. However, if T ′ contains one or more

elements of value 1, 0.5T ′ /∈ f−1(T ′) since only integers are relevant, and no element

of an R that had to be scaled up could result in a value of 1. Therefore, ∃T, T ′ :

|f−1(T )| 6= |f−1(T ′)|. By proof by contradiction, this algorithm does not produce

address space layouts with uniform probability.

92

This is because during the scaling step, random number sequences that are too

small to fill the address space (need to be “scaled up”) will have an effective minimum

gap size between sections. In other words, program variants with a gap of size 1

between two sections are less likely than program variants with larger gaps, because

they can be produced only by random number sequences that exactly match or exceed

the size of the available virtual memory space.

While Algorithm 6.2 may be “random enough” for most applications, it is not

uniformly random across all possible address spaces. The choice between algorithms

that modulate efficiency, ease of implementation, and degree of randomness should

be an important and clear part of the design of any randomization scheme.

To contrast non-uniformly random Algorithms 6.1 and 6.2, this study presents an

algorithm based on the “Fisher-Yates Shuffle” [63] which shuffles an ordered list to

produce a permutation uniformly at random. Section 6.1.2 transforms the algorithm

into an instance of the Fisher-Yates Shuffle, proving that it produces a given permu-

tation of the address space uniformly at random; given some input (and assuming a

sufficient source of randomness2) no output is more likely than any other. The algo-

rithm represents a less efficient but more random point on the spectrum of techniques

from which a system designer can choose, and is shown in Algorithm 6.3.

One of the most powerful features of this algorithm is its ability to load sections

randomly within a discontiguous virtual memory range. This flexibility allows, for

example, loading some program within the entire available virtual address space, triv-

ially avoiding the hardware-unimplemented “noncanonical” addresses that split the

virtual address space into higher and lower halves. It also allows a unique capability

when diversifying programs within the conventional paradigm of user- and kernel-

space sharing a virtual address space. Specifically, a user process can be loaded into

all available virtual memory space after the kernel is loaded. This means that at

2Insufficient sources of randomness can lead to non-uniformly random address space layouts as in [113].

93

Algorithm 6.3: Generic Diversity Loader Algorithm. This algorithm chooses random numbers within the space of available loadable addresses, then walks the address space (skipping over already loaded sections) to find the address corresponding to that random number in the current state of the address space.

Input: The Address Space A & Sections to be Loaded S Output: Random location si.location ∀s ∈ S

1 for i = 1 to |S| do 2 Find Ai, the set of addresses available to load si; 3 if Ai = ∅ then 4 error; 5 Pick random k : 0 ≤ k < |Ai|; 6 Find the kth eligible address: ai,k ∈ Ai; 7 si.location← ai,k

run-time, there are no deterministic address ranges for kernel or user sections. This is

a departure from previous implementations of load-time diversity in which the kernel

was diversified in isolation within a predefined “kernel-space” virtual memory range,

and user programs only within a predefined “user-space” virtual memory range. Inter-

leaving kernel and user sections adds a significant level of complexity for an attacker

to deal with when developing an exploit.

6.1.1 Run-time Complexity and Termination Analysis

Algorithm 6.3 iterates over a fixed set S, and nothing within the loop could cause a

condition where it does not exhaust the set. Therefore, it will terminate.

The algorithm iterates over S and twice over A within the main loop (at lines 2

and 6). If the address space is represented as regions of available memory instead of

individual addresses, the number of regions will be proportional to |S|. Therefore,

the run-time complexity is O(|S|2).

There is a way to formulate FYS (and therefore may be a way to formulate the

algorithm presented here) in O(N) time [53]. However, in this case |N | is the size of

the available virtual address space. For any program, |N | >> |S|2. The algorithm

proposed here also has a much smaller memory overhead. For these two reasons, the

94

version we’ve presented is actually better than its in-place alternative.

The algorithm will terminate, but there is also a simple condition for the more

interesting outcome: completion without error. Expression 6.1 describes the condition

necessary for the algorithm to succeed without an error.

∀ si ∈ S, |Ai| 6= 0 (6.1)

First, observe that an address space is big enough for S if it is at least as big as

2× |S| copies of the largest section in S. This is stated as Lemma 1.

Lemma 1. Algorithm 6.3 will always succeed to load a set of sections S with size

S.size = ∑

s∈S s.size if, |A| ≥ 2|S| × sl.size where @ s ∈ S : s.size > sl.size.

Proof. The largest section in S, sl (given by definition: @ s ∈ S : s.size > sl.size),

will consume at least sl.size eligible addresses when it is placed. The concern is that

the space between some hard boundary, like address 0x0, and the location where sl

is loaded (al,k) is too small for any address within that range to be eligible to load

a subsequent section. In other words, the worst case is that loading each section

removes 2sl.size− 1 from the available address space. Therefore, for all |S| sections,

the maximum address space that could be consumed is |S|(2sl.size − 1) = 2|S| ×

sl.size − |S| < 2|S| × sl.size. Therefore, a single contiguous address range of size

2|S| × sl.size will be sufficient to load any set of sections. �

Lemma 1 shows that the algorithm will successfully load the kernel without error.

|A| is as big as 255 TB before anything is loaded. According to Lemma 1, this would

allow as many as 127,500 1 GB sections to be loaded. Since it is common for the

entire kernel to be loaded into a virtual memory region of just 1 GB, this is clearly

sufficient to load even the largest monolithic kernel into a blank-slate virtual address

95

space. No kernel section will be large enough to divide the address space such that

there is no sufficiently large memory space for some section. However, leveraging

the full strength of this algorithm involves loading a user-space application into the

virtual memory gaps left between the sections of a diversified kernel. In the typical

case, this will not pose any problem in the loading of a user process. Additionally,

the condition shown in Expression 6.1 can be satisfied even in the most dramatic case

with only a small consideration made at the load-time of the kernel.

The memory footprint of a monolithic kernel such as Linux is less than a few

gigabytes. As a worst-case scenario, assume that the memory footprint of a kernel is

many orders of magnitude larger: 20 TB. In another worst-case assumption, assume

that there are 1,000,000 unique sections for the kernel3.

Using these assumptions, there will be 235 TB of virtual address space available

for the user process. The concern is that this address space will be segmented into

unusable chunks. The 1,000,000 sections will force the address space to be segmented

into a maximum of 1,000,001 pieces for the user-space load-time iteration of the

diversity algorithm. These partitions will average about 235 MB. This means that

there will be at least one partition whose size is at least 235 MB; big enough for the

entirety of many user programs. However, it is highly unlikely that sections will be

placed so uniformly as to have only partitions of 235 MB remaining. The probability

is high that somewhere within the 1,000,001 available partitions, there is a single one

whose size is big enough to hold the entire user program as described in Lemma 1.

One can guarantee that an arbitrarily large user program will fit. Only a small

modification is needed to the initial diversification of the kernel to do so; add a

section su to the set of kernel sections, Sk, before the diversity algorithm is invoked

to load the kernel. Allow su to be randomly placed according to the algorithm, but

3Ubuntu’s exuberant-ctags package counted less than 500,000 functions in the entire source directory for Ubuntu version 10.10 and Linux Kernel version 3.13.0. Note that this includes all architectures and options, not all of which will be included in any particular kernel build.

96

do not map any physical memory into the virtual address space corresponding to su,

meaning that this section for the kernel becomes a contiguous region of eligible virtual

addresses for the user process. This guarantees that at load-time of a user process,

|A| ≥ su.size. By controlling su.size, one can satisfy Expression 6.1 for arbitrarily

large user programs. Lemma 1 shows exactly how large su.size should be for a given

program. Note that making this allowance does threaten the claim for uniformly

distributed outcomes; in an exceptional case where the entire user program must be

loaded into su, user sections will be more closely grouped than if this method is not

used.

6.1.2 Proof of Uniformly Distributed Variants

The benefit of randomization is increasing the number of possible variants of a pro-

gram to suppress vulnerability amplification and to increase the search space of an

attacker hoping to brute-force the system. These benefits are reaped most completely

if the search space is distributed uniformly across all possible variants. That is to say

that no variant of the program should be any more likely than any other variant. Al-

gorithm 6.3 produces a permutation of sections within the address space uniformly at

random; proven by showing that it is actually equivalent to the Fisher-Yates Shuffle

(FYS), which itself produces permutations of an array uniformly at random [63].

Throughout this section a particular visualization of the task of diversifying the

address space will be helpful. The diversification process is modeled as the task of

generating a random permutation P of the set N , where N is the set of sections to

load, S, combined with the blank units of memory, B, that will be present in the

address space after all of S is loaded. In other words, N = S ∪ B where |B| =

|A|− ∑

s∈S s.size. Each permutation of N represents a unique variant of the program

loaded into a virtual memory context.

The Fisher-Yates Shuffle is shown in Algorithm 6.4. Figure 6.1a gives a visual-

97

Algorithm 6.4: The Fisher-Yates Shuffle is an algorithm that produces a ran- dom permutation of a list. It is proven that all possible outcomes are equally likely [63].

Input: Some set N Output: P , a random permutation of N

1 while |N | 6= 0 do 2 Pick a random number k : 1 ≤ k ≤ |N |+ 1; 3 Pi++ ← the kth element of N ; 4 Remove the kth element of N ;

ization of Algorithm 6.4 and reflects the composition of N as the sections to load

combined with the blank spaces to build a representation of the entire address space.

Figure 6.1b shows a small modification to FYS. The algorithm that this represents

will be denoted FYS’. FYS’ is the first step towards showing that Algorithm 6.3 is

equivalent to FYS. Lemma 2 states that FYS’ is still uniformly random. The simple

proof is included.

Lemma 2. The algorithm (FYS’) that differs from FYS by iteratively choosing a

“place in line” for each member in N instead of iteratively choosing a random member

of N to place “next in line” produces variants uniformly at random.

Proof. In each of FYS and FYS’, a particular sequence of random numbers chosen

during the algorithm corresponds to a single and unique permutation of the address

space. Because the method of choosing random numbers is the same between algo-

rithms, the probability of a given sequence of random numbers is the same in each.

If the probability of any two variants is equal in FYS, then the probability of any

two sequences of random numbers is equal. If the probability of any two sequences

of random numbers is equal in FYS, then the probability of any two sequences of

random numbers is the same in FYS’. Finally, if the probability of any two sequences

of random numbers is equal, then the probability of any two variants resulting from

FYS’ is equal. Therefore, FYS’ produces program variants with uniform and evenly

distributed probability. �

98

i = 1 i = 2 i = 9...

...

...

pick for slot 1 pick for slot 2 pick for slot 9

(a) Representation of the Fisher- Yates Shuffle

i = 1 i = 2 i = 9...

...

...

place section 1 place section 2 place section 9

(b) FYS’; Modification of the Fisher- Yates Shuffle

Figure 6.1: The original Fisher-Yates Shuffle and an equivalent but slightly modified FYS’. Each produces permutations uniformly at random. FYS’ is a step on the path to transform Algorithm 6.3 into FYS.

Note that with the input given to the FYS’ algorithm, some of the steps that

it took were unnecessary. Because each of the “empty space” inputs is identical,

all possible placements of these units results in an identical address space if the

placements of the first sections is fixed. Therefore, assuming that all slots are empty

and then running the algorithm for just S produces an identical result as running the

algorithm with N (S and the empty spaces).

This transformation arrives at Algorithm 6.3 while maintaining the FYS property

of producing variants uniformly at random at each step. Therefore, Algorithm 6.3

produces variants uniformly at random.

Finally, note that running the algorithm twice (for kernel-space and then for user-

space) maintains the uniformly at random property. The order of the input sections

does not matter, and running the algorithm twice is really equivalent to just pausing

to do some other work between loading the kernel sections and loading the user

sections. However, no other work changes the conditions required for the algorithm.

Since doing the algorithm twice can be reduced to doing it just once with different

input, the sequence of doing it twice maintains the uniformly random properties that

were proven for running the algorithm once.

99

6.2 Überdiversity: Implementation

The Überdiversity prototype was implemented on Bear [123], a Minix-like research

microkernel with an integrated hypervisor that supports full 64-bit multicore Intel

hardware. While fully featured, the kernel and hypervisor are less than 10,000 lines

of code, with extensive sharing between the components.

This chapter provides references to resources describing the different components

of the implementation and adds a discussion of the details specific to the prototype.

6.2.1 ELF & the Diversity Loader

The compiler stores a program’s code, data, and metadata in the ELF [39] format. In

order to maximize the impact of load-time diversification, the compiler should take

care to split the code into as many separate ELF sections as possible4. The ELF

ABI and the associated loading process is well documented [104]. Adding support for

diversification requires three additional steps. First, sections are loaded at randomly

chosen addresses rather than those specified in the ELF file. After loading each section

into a random virtual address, the diversity loader calls two routines per loadable unit:

move unit and fixup unit.

The move unit function updates the global symbol table loaded from the ELF

metadata to reflect the new addresses of each section. It also updates internal relo-

cation entries associated with the unit. These are places in a particular section that

reference an absolute address within the same section.

After all units have had their symbols and relocations updated with their new

randomized values, fixup unit is called for each of the units. This routine resolves

external relocations for the unit. In other words, after all of the symbols have been

updated, each unit finds the updated value of the symbols it needs to resolve.

4In GCC, this is accomplished with the -ffunction-sections option

100

Random numbers are notoriously difficult for computers to generate. Sources of

randomness are out of scope for this work, but there are many published research

efforts examining how to obtain random numbers from a computer [136]. The pro-

totype uses the x86 hardware random number generator, accessed using the rdrand

instruction. Despite some controversy about closed-source manufacturer provided

hardware random number generators [124], this is sufficient for the proof of concept

prototype.

6.2.2 The Virtual Memory Abstraction

The page tables [79] used by the MMU on the 64-bit x86 architecture are four levels

deep. Each level contains protection bits used to moderate software’s access to the

memory mapped below that particular entry. These include a U/S bit to differentiate

between user mode and supervisor mode access, a R/W bit to control read or write

access, and an NX bit to control execution privilege, among others.

Despite the fact that these permissions are present at each level of the page tables,

the protection bits at higher levels are not used in the prototype. An NX bit set in the

top level table marks every virtual address decomposed through that particular entry

as non-executable. With the maximum degree of randomization, however, there are

likely to be multiple sections of different permissions interleaved within a single high-

level table entry. In order to maximize the possible entropy achieved at load-time,

permissions are enforced at the smallest possible granularity. For this reason, higher

level table entries are always set with the most liberal permissions possible. The

permissions required for a given section are set only in the final table entry. A small

performance improvement is possible by marking higher-level tables that happen to

contain only sections of a given type, but the prototype does not implement this case.

The entropy that can be achieved by the diversity loader has an upper bound

defined by the amount of virtual memory space into which a given section can be

101

loaded. In the case of the current x86 64-bit implementation, there are 48 bits of

addressable virtual address space. This corresponds to the capability to virtually ad-

dress 256 TB of memory. There is an architectural definition that, until the full 64-bit

address width is implemented on the memory bus, the address must be sign extended.

This creates a region of addresses that are illegal to access, called noncanonical ad-

dresses. The current 48-bit implementation of x86 does not allow addresses between

0x7FFFFFFFFFFF and 0xFFFF800000000000.

The fact that the virtual address space is so large allows for the large degree

of entropy that can be achieved by harnessing the entire available virtual address

space for loading sections. Recall that the flexibility of the algorithm described in

Section 6.1 makes avoiding the noncanonical address range while still utilizing the

entirety of the virtual address space trivial.

Interleaving kernel- and user-space sections raises concerns about the accessibility

of kernel memory by unprivileged user processes. For instance, if the user stack is

loaded directly adjacent to some kernel code, one might worry about a stack overflow

attack to overwrite kernel code. The hardware memory management unit (MMU)

mitigates this risk with hardware protections stored in the page tables. If the kernel

code is loaded with the supervisor bit set in the page tables, any user process will

fault trying to read or write that memory. Unfortunately, the page table protections

are set and enforced only in PAGE SIZE5 intervals. This has important implications

for the implementation of the algorithm: no two sections with differing permissions

can share a page. The stricter rule that no two sections of any type can share

a page simplifies the implementation. This rule is enforced by making sure that

addresses on any page that a section is loaded onto are marked as unusable during

Algorithm 6.3. So, removing addresses from the pool of eligible addresses is done

in PAGE SIZE increments. Note that this rule does not mean that the address of a

5The typical value of PAGE SIZE on the x86 architecture is 4 KB

102

loaded section must be on a page boundary.

6.2.3 Diversifying the Entire Software Stack

The idea of diversity was first applied to user-space programs. Running on top of the

operating system means that there is a powerful program (the kernel) used to load

user-space code, with arbitrary complexity.

Kernel code randomization (KASLR) was the natural next step. Some KASLR

implementations use the normal kernel boot loader to provide the randomization

functions [54] while others use a hypervisor to load the kernel [86]. The prototype

uses the former method. A first-stage bootloader (boot1) manages the processor

configuration needed to enter 64-bit mode at which point a second-stage bootloader

(boot2) performs the randomized loading of the kernel.

A fully general diversity loader implementation in boot2 allows for the additional

accomplishment of randomizing a hypervisor that is loaded with the bootloader. The

hypervisor is diversified in its own address space. While the kernel- and user-spaces

share an address space, the hypervisor is not mapped in to any set of page tables used

in the kernel. Its own unique set of page tables makes it a completely separate unit

from the kernel- and user-space. However, there are still huge security implications

to having the hypervisor be loaded non-deterministically. As security technologies

improve, attacks on systems are being launched at layers closer and closer to the

hardware [61]. The conventional paradigm used to relate the hypervisor to the op-

erating system suggests that hypervisors will soon be subjected to the attacks that

motivate KASLR [141].

The result of this particular software decomposition and the flexibility of the di-

versity algorithm is a system in which every layer of the software is loaded randomly

into the available virtual memory space. This includes the hypervisor, the operat-

ing system, and the user process. Furthermore, the user process and kernel that

103

share a virtual address space are randomized together, rather than being randomized

individually in disjoint virtual memory regions.

6.3 Überdiversity: Evaluation and Analysis

6.3.1 Quantification of Diversity Achieved

There are two different ways to quantify the “amount of randomness” (more practi-

cally: “difficulty to brute-force”) that a particular diversity implementation achieves.

The first, entropy, is a familiar notion discussed in many previous papers about di-

versity [19,36,128,149,157]. The second, program variants, is a separate notion that

is sometimes confused with entropy, as in [50]. The difference between these two

measurements will be clarified, and the number of program variants possible from a

single source binary will be counted.

Entropy, the common way to measure a diversity implementation, quantifies the

search space for a particular resource. If a particular function is loaded with n bits of

entropy, then its address is one of 2n choices. Commodity 64-bit hardware imposes a

48 bit limit on entropy since it only implements 248 virtual addresses.

Program variants, on the other hand, describes how many unique layouts can be

generated within an address space with a given program as input. A single program

variant is defined by the union of the locations of all instructions and data loaded

into memory.

In order to illustrate the difference between entropy and program variants, con-

sider the effect of randomizing instructions within a function at compile-time. This

transformation does not increase the entropy beyond simply loading the ordered func-

tion at a random location in memory, since the search space for an instruction is the

same regardless of its location relative to its parent function. However, it does increase

the number of program variants possible.

104

The practical question of which of these is a more important measurement for

estimating the difficulty of brute-forcing a diversity implementation is a challenging

one that depends on the goal of the attacker. If the attacker needs to find one single

program resource (such as a particular dangerous function), entropy is the more

important measure since it describes the amount of randomness in the address of

that function. If the attacker needs to find a series of program resources (such as

gadgets for a ROP payload), program variants is the more important measure since it

helps describe how learning one location narrows the search space for other locations.

Indeed, a complete model for estimating the probability of success of an attacker

depends on these variables and many more. However, even in the absence of a fully

developed model, both program variants and entropy are important for a reasonable

estimate of the potency of a given randomization technique.

Entropy

Entropy is a convenient way to measure the potency and flexibility of a diversification

scheme, but it is important to recognize its limitations. A measure of entropy is

not necessarily a good measure of security. As pointed out in [100], an interesting

instruction loaded with infinite entropy into an infinite address space is still insecure

if all other instructions are nop. In this example, executing the nop instruction

at address 0 guarantees execution of the interesting instruction as the many nop

instructions preceding the interesting instruction will be executed with no effect until

the interesting function is found.

A discussion of the entropy of the prototype will continue with the limitations of

entropy as a quantification for diversity in mind. The prototype is implemented with

sufficient flexibility to take advantage of the entirety of the x86 48-bit address space,

with only some exceptions.

Some of the address space is unused at the top level. In particular, two entries

105

are reserved in the top level of the page table structure. The 511th entry is used for

a self-referential pointer used to provide virtual mappings to paging structures [110].

The 0th entry is unused out of convenience; the bootloader must be loaded into low

memory so the low memory region is reserved to avoid conflicts with boot code.

There are other complications to using low memory such as the memmap and RAMDISK,

structures loaded into low memory by the bios during boot that are used throughout

the lifetime of the kernel. However, most or all of this section could be reclaimed for

use during loading with some care.

Additionally, the least significant address space bits are sometimes unavailable for

loading sections. Each individual section in an ELF binary has an alignment require-

ment [39]. The alignment requirement is reported in the sh addralign member of

each ELF section header. Many section headers report an alignment requirement of 0

or 1, meaning that its section requires no special alignment: all of the least significant

bits of the address can be loaded arbitrarily. However, some sections require 2, 4, 8 or

greater byte-alignment. For any sh addralign greater than 1, log2(sh addralign)

bits of the address must be held at zero in order to conform to the ELF standard.

However, all remaining low-level bits can be randomized. Unlike some prior work,

the prototype can load sections off of page boundaries.

The inability to use the 511th and 0th entries in the top level of the page table

structures does not cut the available addresses in half, so it does not cost a full bit of

entropy. However, it does cost some fraction of a bit. Therefore, worst-case upper and

lower6 bounds on Überdiversity’s load-time entropy are 47 and 43 bits, respectively.

Program Variants

One way to measure the diversity of a system is in terms of the number of program

variants that can be generated from a given binary. The set of all possible program

6The lower bound is reported for a worst-case sh addralign of 8 bytes. This is typical for the largest alignment requirement in an ELF binary.

106

variants is the search space for an attacker that wants to learn the layout of the

address space by brute-force guessing. The larger that search space is, the greater the

workload of the attacker. Similar to entropy, this measurement does have limitations.

Many unique instances of a program will have a particularly interesting resource

located in the exact same location. It is an open research question how likely this is

to assist the attacker in practice.

The question of how many program variants are possible is a combinatorics prob-

lem that can be visualized using the common “stars and bars” method [60].

First, assume that for a best-case scenario, all sections s ∈ S require only 1-byte

alignment. Note that S = Su ∪ Sk; the sections to be loaded are both the user- and

kernel-space sections. Also assume that the hardware enforces protections at a single

byte granularity. These assumptions provide the situation for an upper bound on the

number of variants.

Since ∑

s∈S s.size is known, the total amount of virtual memory that will be unused

after the program is loaded is known. If the amount of virtual memory eligible to be

used for loading is |A|, there will be |A| − ∑

s∈S s.size unused bytes after all sections

are loaded. Each of these unused bytes will lie in a region between two sections or a

section and a virtual memory barrier. In other words, the sections can be considered

dividers that break the unused bytes up into |S|+1 “bins” or “buckets”. Each possible

assignment of unused bytes to bins describes a possible program image.

Counting the ways to place unused bytes into |S| + 1 bins is accomplished using

the following visualization. Imagine each of the |A| − ∑

s∈S s.size unused bytes is

a “star”. If |S| stars are transformed into “bars” (sections, in this case) then the

remaining A− ∑

s∈S s.size− |S| stars are divided into |S|+ 1 buckets. However, for

this system to represent the diversity loading problem, A− ∑

s∈S s.size stars must be

divided into the |S|+1 bins. Starting with |S|+A− ∑

s∈S s.size stars and picking |S|

at random to represent sections instead of unused bytes accomplishes this. Choosing

107

k objects from a set of n is an elementary combinatorial problem. Therefore, an

upper bound on the number of variants that can be produced from a given program

is shown in Equation 6.2.

( |S|+ A−

∑ s∈S

s.size

) !(

A− ∑ s∈S

s.size

) !

(6.2)

Unfortunately, alignment requirements and coarsely grained MMU protection

mechanisms mean that this upper bound is not reached. To capture a better idea

of the actual number of variants possible, some adjustments can more accurately

represent the problem. First, the MMU provides memory protections only at the

granularity of PAGE SIZE chunks. So, the memory consumed by a section will be

in units of page size. For the lower bound, a safe assumption is that each section

consumes the maximum possible amount of memory. This is ⌈

s.size PAGE SIZE

⌉ + 1. Adding

the extra page is just in case the section is loaded across a page boundary. This lower

bound is expressed in Equation 6.3.

( |S|+ A

PAGE SIZE − ∑ s∈S

(⌈ s.size

PAGE SIZE

⌉ + 1 ))

!( A

PAGE SIZE − ∑ s∈S

(⌈ s.size

PAGE SIZE

⌉ + 1 ))

! (6.3)

The lower bound does not achieve the true value because it assumes that sections

are eligible to be loaded only at page boundaries and that sections are loaded such

that each will cross over a page boundary, consuming an extra page. This is a con-

tradiction, but that is acceptable for a lower bound. A tighter lower bound examines

where a section will be loaded within the memory it can consume. In other words,

108

multiplying the original lower bound by the number of places within the memory

footprint consumed by a section that the section can be loaded. This bound is shown

in Equation 6.4.

( |S|+ A

PAGE SIZE − ∑ s∈S

(⌈ s.size

PAGE SIZE

⌉ + 1 ))

!( A

PAGE SIZE − ∑ s∈S

(⌈ s.size

PAGE SIZE

⌉ + 1 ))

!

× ∏ s∈S

((⌈ s.size

PAGE SIZE

⌉ + 1 ) × PAGE SIZE

) − s.size

s.alignment (6.4)

Equation 6.4 is a close lower bound. There is one thing that keeps it from being

an exact calculation: the equation assumes that every possible position to which a

section could be loaded would consume the maximum possible amount of memory

for that section. However, in truth only a small subsection of the eligible addresses

cause a section to consume ⌈

s.size PAGE SIZE

⌉ +1 pages. Most consume only

⌈ s.size

PAGE SIZE

⌉ pages.

Finding an exact value by including this in the calculation is beyond the scope of this

work.

The upper and lower bounds for a typical program loaded using the prototype

implementation can be calculated. The shell is the example program to calculate

sample values. On the system, A ≈ 255TB, |Su| ≈ 143, and |Sk| ≈ 444. Furthermore,∑ s∈Su

s.size ≈ 800MB7 and ∑

s∈Sk s.size ≈ 80MB. This gives an upper bound of

≈ 108440 unique address space layouts for a typical program and a lower bound of

≈ 108205.

These numbers do not include the number of variants produced by any compile-

time randomization techniques that are used; including these effects effects will greatly

7The quantity ∑

s∈Su s.size is dominated by the user-space heap which is allocated with a size

of several hundred MB in order to accommodate memory-intensive applications. Although the heap is allocated on demand as the user process requests it, a maximum size is assigned at load-time to guarantee that the heap has sufficient room to grow.

109

Fork/Exec Test CPU Cycles×109

No Diversity Max 5.2296 Min 4.8050 Avg 4.9768

Überdiversity Max 36.4030 Min 35.5999 Avg 35.9565

Average Performance Cost 7.2×

Table 6.1: This test demonstrates 50 Trials of fork and exec with and without Überdiversity.

increase the number of possible program variants. This shows that, at its limit,

diversification of software can generate a virtual address layout search space that will

overwhelm and defeat brute-force attacks.

6.3.2 Performance Cost

The performance cost of this method of diversity is spent partially at run- and par-

tially at load-time. At load-time, there is added work from running the diversity

algorithm. At run-time, the source is slightly more subtle. Consider a series of very

small functions; without load-time diversity, these are likely to all be located on a

single page of memory. This means that a single translation lookaside buffer (TLB)

entry or cache page will cache all of these functions. With load-time diversity, how-

ever, a TLB entry and cache page will be required for each of these functions. More

importantly, there will be a cache miss the first time each of these functions is exe-

cuted compared to the non-diversified case in which only the first time any of these

functions is executed results in a cache miss.

The load-time costs of the prototype are summarized in Table 6.1. This table

measures the time spent to create and transform a process using fork and exec. The

average case did show a high cost of 7.2×. Fortunately, this cost is incurred only once

in the lifetime of the process and therefore overall throughput is not heavily affected.

In contrast, run-time performance overhead can pose a real threat to overall system

110

0

100

200

300

400

P er

fo rm

an ce

C os

t (%

)

200 400 600 800 0

0.5

1

1.5

2 ·109

1000

Nested Function Depth

C P

U C

y cl

es No Diversity

Überdiversity Performance Cost

Figure 6.2: Performance Benchmarks for Nested Functions. This test illustrates that loading functions onto unique discontinuous pages instead of loading them contigu- ously can lead to cache exhaustion in extreme use cases.

throughput. Caching plays a huge role in offering the performance necessary to

make modern systems useable, and fine-grained randomization threatens that entire

subsystem. Figure 6.2 explores a worst-case scenario. In particular, it shows how the

performance cost increases with nested subroutine depth as the caching capabilities of

the hardware begin to be overwhelmed by the number of entries required for effective

caching. The tests used to generate the data for this figure simply called nested

subroutines until the function at the desired depth returned a value. Performance

costs as high as 400% are a testament to just how important caching is, and how it

can be disrupted by fine-grained randomization.

Although the experimental status of the platform for the prototype limits the type

of applications that can be measured, Table 6.2 illustrates measured performance

111

Test (CPU Cycles ×1010) Multiply Add Divide malloc

No Diversity Max 2.2619 1.3811 10.937 2.9638 Min 2.2394 1.3512 10.830 2.9563 Avg 2.2432 1.3631 10.851 2.9596

Überdiversity Max 2.2647 1.8987 11.037 4.2529 Min 2.2388 1.3307 10.373 3.0115 Avg 2.2464 1.3674 10.629 3.1393

Average Performance Cost 0.14% 0.32% -2% 6%

Table 6.2: AIM9 Benchmark Suite [6] and Malloc Test [103] Performance Measure- ments - 50 Trials per test

costs on industry standard benchmarking utilities. The AIM9 Tests [6] demonstrate

that, despite the high costs seen in Figure 6.2, code diversity has no meaningful

performance cost on code execution within a function after load time. The malloc

test [103] does show some performance due to the cache misses from malloc and all of

its subroutines. Importantly, however, even though this test uses many subroutines

in the C standard library, only a 6% performance cost was measured. This suggests

that many applications follow the anecdote that “software spends 90% of its time in

10% of its code,” and therefore that a very advanced level of randomization can be

deployed with acceptable performance cost.

Finally, there is some memory overhead associated with using such a fine-grained

randomization scheme. The code itself will have a larger memory footprint because

memory is consumed in increments of PAGE SIZE, but each function is loaded in

isolation from the others. Therefore, two functions that fit together on one page will

now consume 2 pages. Additionally, more memory is used to generate the higher-level

paging structures that provide the mappings to randomized, non-localized addresses.

Figure 6.3 demonstrates the worst-case of this cost is between 10× and 15×. This cost

may be prohibitive for some applications or low-memory systems, but the amount of

memory on state of the art systems is typically more than sufficient to accommodate

this overhead for average workloads.

Furthermore, techniques used to decrease some of these performance and memory

112

10

12

14

16

18

W or

st -C

as e

M em

or y

O ve

rh ea

d (×

)

200 400 600 800 0

1,000

2,000

3,000

1000

Number of Sections Loaded

P ag

es C

on su

m ed

No Diversity

Überdiversity Memory Overhead

Figure 6.3: Worst-Case Memory Overhead Analysis. This analysis assumes that sections are 1

4 the size of a page, and that the random locations are chosen in the

most memory-heavy way possible.

113

overheads are explored in [163]. This work explores optimizations such as bundling

leaf functions with their callers to minimize an extra cache miss in a case where

security is unlikely to be decreased. These same techniques could be applied in a

64-bit environment.

6.3.3 Security Implications

As traditional ASLR has become ubiquitous in many commodity operating systems,

attackers have developed a multitude of ways to bypass its protections. The various

classical methods of exploiting programs despite ASLR are presented in the ASLR

Smack and Laugh Reference (ASLRSLR) [122]. A selection of these methods below

contain discussions of how Überdiversity influences their efficacy.

Aggression. The first and most obvious threat listed in ASLRSLR is a brute-

force attack in which an attacker gains access to a vulnerable pointer that has been

randomized by repeatedly guessing. For example, [149] shows that a system with

only 24 bits of entropy can be brute-forced in a matter of minutes. Although [128]

reported being able to brute-force a 64-bit ASLR implementation in as little as 1

hour, the ASLR implementation used was only 28 bits. As discussed in Section 6.3.1,

the prototype achieves at least 43 bits of entropy. With 43 bits, a brute-force attack

would take months8. This means that any attempted brute-force attack will be easily

detected before it succeeds.

Return into Non-Randomized Memory Regions. In some ASLR implemen-

tations, there are some sections that are not randomized. Ways to exploit each of

the text, heap, bss, and data sections when they are not randomized are detailed in

ASLRSLR. Fortunately, the prototype successfully randomizes the location of every

memory region. The ELF maintained relocation tables allow fixing pointers to data

8Estimated. Experimental results from [149] and [128] say that 24 bits requires 4 minutes to brute- force, and 28 bits requires an hour. This conforms to intuition, as 4 min× 2(28−24) = 64min ≈ 1 hr. The same calculation for 43 bits gives 4 min× 2(43−24) = 524288 min ≈ 1 yr.

114

in the bss/data section and to functions to reflect the newly randomized locations.

The user-space malloc implementation learns the location of the randomized heap by

making a system call to request the address. Similarly, the kernel locates its heap at

run-time by referencing data passed on by the bootloader.

Information Leakage Attack. Direct or indirect memory disclosures can de-

feat address space randomization by divulging address space layout information at

run-time, enabling unique attack vectors [151]. Many methods such as execute-only

code [15,26,43,68] aim to limit the risk of memory disclosure. Most of these techniques

are compatible with Überdiversity.

Furthermore, fine-grained compile-time diversity should be used to reduce the

amount of usable information that an attacker gains from any memory disclosure.

Compile-time diversification should introduce randomness within an ELF section,

denying the ability to make assumptions about the location of gadgets within a given

ELF section by reading a single address off of the stack. Research efforts examining

the necessary type of compile-time randomization are plentiful [76,81,82,86,93], and

most are be compatible with load-time diversification techniques.

New information leakage techniques include side channel attacks, including meth-

ods developed specifically for defeating ASLR [77]. However, the authors of [77] state

explicitly that “by utilizing the complete memory range and distributing all loaded

modules to different places, it would be much harder to perform our attacks.” This is

exactly the method that Überdiversity uses, meaning that its high entropy provides

a native level of protection against these types of attacks.

Return-Oriented Programming. Return-oriented programming (ROP) [140]

is a method that uses small bits of code known as “gadgets”, each of which is a

short sequence of instructions followed by a return. A carefully crafted stack can

direct control flow through these gadgets in a particular order to preform arbitrary

computation.

115

Traditional ASLR disturbs ROP by making it more difficult to locate gadgets.

However, an attacker needs to find only one address range in order to locate all of

the possible gadgets. The prototype makes it even more difficult by putting each

function, and therefore all potential gadgets, into unique random address ranges.

... and beyond In the time since the publication of ASLRSLR, several attacks

against ASLR have been published. Some, such as [126], rely on the relatively pre-

dictable layout of the address space and are therefore defeated by using fine-grained

randomization such as that explored in Überdiversity. However, some state of the

art techniques such as BROP [21] manage to defeat even fine-grained randomization

without access to the compiled and loaded code. This powerful technique threatens

otherwise secure randomization schemes, including Überdiversity, but requires some

particular properties of a system. The authors state that “the most basic protection

against the BROP attack is to rerandomize canaries and ASLR as often as possible,”

because they rely on a restart property to guarantee that their addresses of interest do

not change upon a process crash. Although Überdiversity currently randomizes only

during a call to exec, as they point out is typical of randomization techniques, it is

possible that it could randomize during fork as well. The challenges of implementing

a system to re-randomize code on a fork is discussed in the context of per-process

kernel layout randomization in [28].

6.4 Überdiversity: Conclusion

Diversity is a well-established technique for decreasing the risk of vulnerability am-

plification and increasing attacker workload in networked systems. Although many

different techniques for injecting randomness into computer memory layouts have

been presented, none have yet approached the limits of 64-bit hardware. Further-

more, most do not discuss the method used to randomize the memory layout, and

116

many confuse entropy with program variants while attempting to quantify their ef-

fort. This study presented Überdiversity, an ELF diversity loader that randomizes

fine-grained memory regions while loading them into memory.

A study of related work reveals that an important detail in any randomization

technique, the algorithm used to produce permuted address spaces, is rarely men-

tioned, much less thoroughly examined. The study presented the algorithm used for

Überdiversity and proved that it produces address space layouts uniformly at ran-

dom, a property that is necessary for any randomization technique to be deployed

most effectively. Additionally, previous research efforts are inconsistent in the use of

“entropy” to quantify the effectiveness of a diversification tool. To address this, care is

taken to separate the measure of entropy from that of program variants; both provide

valuable but different ways to estimate the potency of a diversity implementation.

Überdiversity makes several improvements over the current state of the art. Firstly,

it manages to make use of a large majority of the available virtual memory, deliver-

ing an unprecedented 43-47 bits of entropy. Including the diversity loader into the

system’s bootloader enables the random loading of the hypervisor and the operat-

ing system kernel in addition to the more common randomization of user processes.

Furthermore, the kernel and the user process are randomized together, producing an

address space where kernel- and user-sections are non-deterministically interleaved.

The flexible x86 virtual memory abstraction maintains appropriate protections de-

spite having discontiguous kernel- and user-space virtual memory regions.

The prototype revealed high costs (7.2×) at load-time but, despite clear evidence

of an extremely slow worst-case scenario, only moderate costs in the average case

at run-time (less than 10%). This suggests that mainstream ASLR implementations

could use more thorough and fine-grained randomization techniques on an average

application, even approaching the hardware-imposed limits, without suffering unrea-

sonable performance costs.

117

Chapter 7

KUCS

The fundamental role of an operating system is to provide an appropriate context for

process execution, multiplex processes’ access to the hardware, and protect processes

from each another. The current state of the art operating system architecture, as

partially described in Chapter 2, is the product of many seminal [20, 40, 47, 145,

146, 158] and hundreds of subsequent research efforts and accomplishes all of these

tasks. Current systems provide each application with a virtual address space in which

the kernel manages the required context. The kernel provides the application with

functionality for manipulating hardware by including its own code in the virtual

address space of each process1. In order to protect processes from one another, the

kernel does not share virtual address spaces between applications and it restricts

access to the shared kernel functionality using hardware-provided CPU privilege levels.

Unfortunately, some characteristics of the current kernel design lead to security

and performance issues. For example, the popular return-to-user (ret2usr) style at-

tack [93] is enabled by sharing the virtual address space between the kernel and the

application, despite the CPU privilege levels.

The current relationship between the operating system and a user application is

1The few examples of systems that use “strong” rather than “weak” separation between kernel memory and user memory include the 4G/4G split Linux patch [118], 32-bit XNU [94], and certain systems using the hardware facilities provided by SPARC V9 hardware [115]

118

largely a historical artifact. In the early days of computing, with just one process-

ing unit, only one program could run at any given time. There needed to be a way

to pass control from one program to another (in this case, from the application to

the kernel), and some notion of differing privileges between the two programs. To

accomplish the first task, system designers simply included both “programs” in a

single computing context. For the second, the hardware provided the privilege mode

switch triggered by an interrupt (an INT instruction on x86). When the processor

reached this instruction, it would save the state of the current operating context to

a known location, change the CPU’s privilege mode, and direct execution to a pre-

defined kernel entrance routine. Later, when the kernel wanted to resume execution

of the application, it would issue a special return (IRET on x86), at which point the

hardware would demote privilege and transfer execution to a kernel-defined location

in the application’s code. More recently, the x86 architecture defined SYSENTER and

SYSEXIT as alternatives to INT and IRET for the system call (syscall) interface, but

the underlying mechanism has not changed.

With the advent of multicore processors, the need to share hardware no longer

forced this design onto kernel developers. However, since each core still provided

the functionality of the unicore processor, it became standard practice to take the

established kernel implementation formula and replicate it, instantiating one kernel

and user instance per core. The only special care needed was that no two cores

would execute the same code at the same time. Originally, this was accomplished

using a single “kernel lock,” a software mechanism that restricted access to the kernel

to only one processor at a time [107]. More recently, many kernels have narrowed

the granularity of their locked paths so that two or more fully independent paths

through the kernel may be used simultaneously by two or more different cores [155].

Overall, this scheme of employing a single mechanism on each available core is known

as Symmetric MultiProcessing (SMP).

119

The decision to replicate the unicore operating system design onto each core in

multicore hardware may have imposed unnecessary limits on the security and per-

formance attainable on modern systems. This chapter discusses an effort to revisit

that decision and explain how to replace the unicore operating system design with

one that leverages modern multicore hardware to address some of the weaknesses

found in modern designs. The resulting design is called Kernel and User Core-Based

Separation (KUCS). KUCS’ unique utilization of hardware resources fundamentally

changes how systems provide security to their different components. While micro-

kernels redistribute system components to increase the security offered by monolithic

kernels, they do so using the exact same hardware abstractions to differentiate user-

and kernel-space. In contrast, KUCS changes the underlying hardware abstractions

used for coordination and translation between different components of the system,

although any current kernel, whether monolithic or micro, could be implemented

using KUCS. Even cutting edge virtualization-based security efforts use the same

hardware design patterns that operating systems have been using for years [141]. As

argued in [25] this “turtles all the way down” approach to security is not sufficient.

In KUCS, virtualization is used as a tool for the kernel to protect itself, rather than

as an abstraction that replicates the classic kernel design paradigm in a new software

layer.

KUCS explores the idea of using Asymmetric MultiProcessing (ASMP) on mod-

ern x86 hardware. Unlike the prior ASMP efforts described in Section 3.4, the KUCS

design aims explicitly to divorce the virtual address space of the application from that

of the kernel. The distinct, decoupled operating environments provided on each pro-

cessor allow for the kernel and the application to use completely different hardware

resources, rather than sharing a common processor core. Inter-Processor Interrupts

(IPIs) will take the role of the standard INT/SYSENTER and IRET/SYSEXIT function-

ality for system calls. The application and the kernel will no longer share hardware,

120

so the hardware-provided CPU privilege mode switch is not needed, and neither pro-

gram has to relinquish its own resources to message or signal the other. Additionally,

virtualization will be utilized to implement fine-grained security rules on a per-core

basis to further police the software.

This paradigm shift interrupts commonly deployed privilege escalation mecha-

nisms by defining privilege as a physical location on-chip rather than a traditional

processor mode. As a result, more advanced virtualization-based defenses can be

employed because each core is responsible for either the kernel or an application –

never both. Additionally, the fact that the kernel and the application do not share

hardware resources means that they can operate simultaneously. This creates oppor-

tunities for techniques such as using the kernel to perform watchdog state checks on

applications while they run an application utilizing truly asynchronous system calls

by signaling the kernel but continuing to run while the kernel services its request.

Utilizing these techniques in a KUCS operating system will deliver strong separation

between the kernel and an application, fast IPI-based control transfers and messag-

ing, advanced virtualization-based and application-specific security features, and new

opportunities such as asynchronous system calls, kernel-based watchdog processes,

and application-specific hardware-secured subcontexts.

7.1 KUCS: Overview

This section presents an overview of the Kernel and User Core-Based Separation

(KUCS) operating system design. The content of this chapter is based largely on the

design that was proposed in NSPW ’16 [31], which presents a new security paradigm

in operating system design. The implementation details discussed later in this chap-

ter reference this design, but certain features of the design are not present in the

KUCSBear prototype. This is discussed in more detail in the next section.

121

Kernel User Process User Process

IPI

IPI

IPIs

Core 0 Core 1 Core N

Allow HLT instructions? Allow device interrupts?

✓ ✓

Ring -1 (VMM)

Ring 0

Allow HLT instructions? Allow device interrupts?

✗ ✓

(e.g. Network Driver)

Allow HLT instructions? Allow device interrupts?

✗ ✗

Hardware-Enforced Privilege Mode Boundary

INT/SYSENTER

IRET/SYSEXIT

Ring 3

Application-Speci�c Secure Context

(e.g. Web Browser)

Figure 7.1: Operating system design using KUCS. CPU privilege level (ring) 0, tra- ditionally used for the kernel context, contains the main code for each core: either the kernel or a user processes. The virtualization layer, commonly referred to as ring -1, protects the hardware from user processes. With ring 3 no longer being used by the system, individual applications can use the hardware to provide their own secure sub-contexts.

Figure 7.1 illustrates an overview of the KUCS design. Core number, rather

than the traditional CPU privilege level (ring), differentiates kernel- from user-space.

Core-specific security rules in the virtualization layer and virtual memory isolation

for ring 0 processes recover the protection offered by the traditional ring 3 user-space.

Interprocessor interrupts provide a mechanism for implementing system calls across

cores. With ring 3 no longer used for isolating user-space, the application can use the

hardware protection associated with ring 3 to provide its own secure sub-contexts.

This section explores the features of a KUCS design in detail. The main factors

contributing to increased security are divorcing kernel- and user-space, utilizing user-

space drivers and deploying fine-grained per-core virtualization-based security poli-

cies. The potential for an increase in performance will come from fast mechanisms for

signaling from an application to the kernel, less overhead in driver implementations,

and more efficient parallelization of computation.

122

7.1.1 Separating Kernel and User Cores

The primary characteristic of the KUCS design is that the kernel and the user appli-

cation do not share a virtual address space and are each executed at ring 0, but on

independent cores. This arrangement will have several implications on the security

of the system.

Ring 3 (and more) Protection in Ring 0

The weak separation between kernel- and user-space in traditional operating system

design allows for even the most strictly “sandboxed” processes to be used as a stepping

stone for compromising the whole system because privilege escalation compromises

the kernel. Since the kernel needs to manage all of system memory, it maintains

virtual mappings to all memory; a process that shares its virtual address space with

the kernel can perform arbitrary reads and writes with sufficient privilege escalation.

When not shared with the kernel, the application’s virtual address space does not

need to contain mappings to all of system memory. Consequently, the application

cannot inspect or modify memory unless the kernel has given it access, even with

arbitrarily high privilege. Naively, this means that the application can safely be run

in ring 0. However, additional steps are required to recreate the full protections

offered by ring 3 while running in ring 0.

In ring 0, the process is a supervisor on-core and can read from or write to any

memory mapped into its address space. In order to stop a process from modifying

its page tables to create mappings for arbitrary system memory, virtual mappings to

the application’s page tables will be omitted from the application’s virtual address

space. In order to modify its own page tables, the page tables need to contain a

virtual mapping to themselves; without it, the application cannot modify the memory

used by the memory management unit to define its own virtual address space. This

ensures that the process is permanently and completely isolated in virtual memory

123

and enables the process’ page tables to deny access to memory-mapped devices.

The virtualization layer also helps to recreate ring 3 protection by denying inappro-

priate hardware accesses like reading from and writing to processor control registers

or executing typically privileged instructions. The end result is an application exe-

cution context with the same protections offered by ring 3, but without the crown

jewels of the system hiding behind a CPU privilege level bit.

Hardware Control from Applications

User-space drivers [72] minimize the amount of potentially third party and frequently

buggy [37, 66, 129] code that runs with privilege. Unfortunately, this comes at a sig-

nificant performance cost. Device drivers need to interact with hardware, for which

they need privilege that they do not have in ring 3. Therefore, they must frequently

interact with the kernel to accomplish their tasks, significantly degrading perfor-

mance, increasing the kernel’s code base, and widening the interface between user-

and kernel-space.

Although applications can be run in ring 0 with privileges less than or equal to

those of ring 3, security can actually be increased by providing drivers with privileges

greater than those available in ring 3. Setting up the proper operating context for a

driver (as suggested in the virtualization layer on the core being used for a network

driver in Figure 7.1) will allow the driver to be implemented in a single context. This

is an improvement over traditional “user-space” drivers that need the kernel to do

work on their behalf. It keeps the kernel’s code base small and interface narrow.

Application-Specific Secure Contexts

By moving user-space from ring 3 on a shared core to ring 0 on a user-specific core,

there is yet another opportunity to increase security. In particular, ring 3 is no longer

being used by the system. Instead, applications can (with some kernel support) use

124

ring 3 to provide their own secure contexts.

For an example of how this feature might be used, consider a web browser. Al-

though a browser strives to protect web pages from one another and protect itself

from web pages, cross-site scripting [154] is just one example of a common attack

vector in which a single web page can compromise an entire web browser. However,

if the browser is running in ring 0, it could manage individual web pages in ring 3 the

same way that traditional operating systems manage user processes. When the user

opens a new web page, the browser could ask the kernel for a fresh address space that

contains itself in ring 0 and a clean context for the new web page in ring 3. The kernel

needs to handle this task, but the browser could use a local scheduler implementa-

tion (separate from the main scheduler in the kernel) to cycle through these different

contexts without kernel intervention during its own scheduled time quantum.

Watchdog Kernel Processes

In current operating systems, the kernel is invoked strictly when an application needs

work done on its behalf. In other words, the kernel is never running without a specific

task to handle. In contrast, with the asymmetric multiprocessing solution, the kernel

will be running on a dedicated core whether or not it is servicing a specific process’

request. This creates an opportunity: time during which the kernel is executing but

has no specific task to complete. One possible way to utilize this opportunity is to

implement a watchdog routine [44, 112] in the kernel. This routine could monitor

system and application invariants in real time. For example, it could hash a process’

code to check for code patching, monitor the stack looking for ROP payloads, or

profile system components to detect unfamiliar configurations. In the event that an

anomaly is detected, the application can be suspended pending further action.

The watchdog routine in this design is doing its work with CPU cycles that would

otherwise be wasted on the kernel core. These cycles could also be used for other

125

tasks in the interest of either performance or security. One example of the former is

pre-computing values that are likely to be requested by applications in future system

calls. In the case of the latter, the kernel could use these cycles as a third line of

defense (after virtual memory isolation and virtualization) to restrict user processes

being run in ring 0. For example, if the kernel wants the application to alert it in the

event of a processor fault, the watchdog process can verify that the process does not

modify the fault handlers mapped into its context.

7.1.2 Fine-Grained Virtualization

One advantage of the KUCS design is the ability to exercise each core’s virtualization

hardware independently to impose fine-grained security rules on each core based on

the software that that particular core will be executing. ExOShim [26], as described

in Chapter 4 is a thin layer of virtualization underneath the kernel used to mark all

memory associated with kernel code as execute-only using the Extended Page Table

(EPT) functionality provided by modern Intel VT-x hardware [80]. This mitigates

the risk of kernel-level memory disclosures that could facilitate reverse-engineering or

the construction of kernel ROP or JIT-ROP payloads. Additionally, ExOShim does

not accept any inputs or tolerate any permissions violations so that it can provide

this protection for the lifetime of the system, even in the face of kernel compromise.

Figure 4.2 on Page 55 illustrates how the physical frames of memory correspond-

ing to kernel code are marked execute-only in the EPT, denying kernel-level memory

disclosure vulnerabilities. However, all other memory is marked with the most lib-

eral possible permissions in the EPT: read, write, and execute. This is because the

applications running on the processor may need to use this memory. Therefore, the

virtualization layer relies on the kernel to maintain the appropriate memory man-

agement and access control for this memory. Note that this is consistent with most

hypervisors; only the least restrictive security rules can be deployed for the lifetime of

126

a virtual machine. This is an unfortunate byproduct of SMP; the hypervisor cannot

enforce stricter security rules because it cannot predict which core will need which

set of rules.

Unfortunately, the permissive rules applied to this memory allows for the possibil-

ity of a ret2usr [93] or ret2dir [92] style attack. Intuitively, this is because the security

rules that ExOShim applies to the kernel are not complete. It enforces a rule that all

kernel code is execute-only. However, it does not enforce the desirable rule that the

kernel may execute only kernel code. This rule would eliminate the possibility of the

processor executing any non-kernel code with kernel privilege, but the requirement

that application code must be able to utilize this memory from this core denies the

possibility of applying such a strong rule in the virtualization layer. Although Intel’s

Supervisor Mode Execution Prevention (SMEP) [62] feature can address this risk,

SMEP can be disabled in the event of kernel compromise, SMEP bypass techniques

have already been demonstrated [91, 143], and SMEP cannot mitigate the threat of

a ret2dir style attack [92]. For these reasons, this rule would be better implemented

with ExOShim than with SMEP.

In a KUCS operating system, however, the kernel runs on its own core; no ap-

plication code will run on this core. This means that the ret2usr attack is defeated

directly: there is no user-space to return to in the kernel context. Additionally,

the kernel core’s virtualization layer can enforce even stronger security rules than

the ExOShim prototype described in Chapter 4. In addition to marking all mem-

ory corresponding to kernel code as execute-only, it will mark all other memory as

non-executable. This will preserve the protection against memory disclosure vulner-

abilities while also denying ret2dir style attacks by prohibiting the execution of any

non-kernel code from the kernel core.

127

7.1.3 Increasing Performance

The KUCS operating system design offers opportunities to increase system perfor-

mance in ways not possible in a conventional operating system.

Simultaneous Application and Kernel Execution

A KUCS implementation may explore new relationships between the application and

the system call. In particular, the application does not necessarily need to stop ex-

ecution while it makes a syscall. Figure 7.2 contrasts the work that an application

can accomplish with a traditional syscall and with this asynchronous design. It illus-

trates that when the application requests work from the kernel (in this case, setting

an “alarm”) with the traditional interface, it must wait for the kernel to perform the

requested action prior to continuing its work2. In the KUCS design, however, the

application could continue its work immediately because it is not sharing hardware

with the kernel. As a result, the application would be able to complete more work in

a given time quantum.

Note that this truly asynchronous system call interface is only possible if the kernel

is not sharing hardware with the application. As such, many additional research

questions exist before a full implementation of this feature can be realized. For

example, if the kernel fails to perform the requested task, it must alert the process; this

is nontrivial if the process has changed state since the time of the syscall. Additionally,

the process needs a way to know that the kernel is prepared to handle a second

syscall after the first has been dispatched. Despite these unanswered questions, the

possibility for this type of system call mechanism has promising implications for

system performance.

2Note that this is true even in the case of the “asynchronous system calls” [32] used in some current operating systems. These interfaces may provide the capability for the process to work while the kernel is servicing its request, but the process must still relinquish its hardware to the kernel while it is making the request.

128

Time

Application

Kernel

Schedule New

Process

Schedule New

Process

1 2 3 4 alarm

1 2

5 6

(a) With the Traditional Syscall Mechanism

Time

Application

Kernel

Schedule New

Process

Schedule New

Process

1 2 3 4 alarm

1 2

5 6 7 8 9

(b) With the Proposed IPI-Based Mechanism

Figure 7.2: Application’s work completed in a given time quantum. With the pro- posed IPI-based syscall mechanism, the clock cycle following the call to the kernel belongs to the application, not to the kernel. This is not the case with traditional system call interfaces; even syscalls that claim to be “asynchronous” suspend the application while it is delivering its request to the kernel.

Removing Drivers from the Kernel

In a KUCS operating system, a driver running as a ring 0 user process on a remote

core can have both privileged access to hardware and full user-space encapsulation.

It follows that this driver design could reveal faster performance than the equivalent

microkernel driver in user-space (as traditionally defined) because the driver can

access hardware from its own context rather than invoking the kernel.

In addition to the performance increase over traditional user-space driver imple-

mentations, a driver on a KUCS operating system also has a possible performance

advantage over kernel-level drivers employed in monolithic kernels. Without suffi-

ciently fine-grained locking, even a kernel-level driver locks other cores out of (at

least some part of) the kernel while it is servicing interrupts. In contrast, drivers

loaded with privileged access to the hardware but not into the kernel can service

interrupts as quickly as traditional kernel-level drivers, but without blocking other

cores from entering the kernel.

This effect is similar to that reported in [51] when the network driver was isolated

129

to its own virtual machine. In that case, letting a network-specific VM handle all

network interrupts without interrupting the main kernel resulted in a performance

increase, despite the added overhead of inter-VM communication. Analogous results

were also found in [22,121].

7.2 KUCS: Implementation

The Bear microkernel [123] was used as a platform to build a prototype operating

system using the KUCS design paradigm. The prototype, KUCSBear, does not im-

plement all of the features enabled by a KUCS design. However, it provides a case

study for future KUCS implementation efforts and offers a platform to investigate

the design’s potential implications on performance.

This section describes the KUCSBear prototype in detail. It is framed to reflect

the organization of Chapter 2 in order to facilitate the comparison of particular im-

plementation details between a conventional operating system (Bear) and a KUCS

operating system (KUCSBear).

7.2.1 KUCSBear

The prototype implements some but not all of the features described in Section 7.1.

It does:

• Isolate the kernel onto one particular core.

• Load user processes in virtual memory contexts with no mappings to kernel

code or data, or to its own page tables.

• Restrict the privileges of user processes even while running them in ring 0 on

cores without a resident kernel.

• Utilize IPIs to communicate between the user cores and the kernel core.

130

• Run a driver in “user-space” in ring 0 on a user core with direct mappings to

otherwise privileged hardware.

• Virtualize each core independently.

• Enforce strict security policies on the kernel core that extend the protections

otherwise offered by ExOShim.

• Demonstrate the possibility of the kernel doing “watchdog” type tasks while

not servicing user requests.

However, the prototype does not take full advantage of every feature made possible

by a KUCS design. It does not:

• Explore the possibility of system call interfaces that allow the application to

continue its work while the kernel services a syscall request.

• Route hardware interrupts directly to device drivers.

• Implement application-specific secure contexts in ring 3 on user cores.

In order to facilitate implementation, KUCSBear was developed to allow KUCS-

style kernel and user process operation on some subset of cores while conventional

kernel and user processes can run simultaneously on other cores. Even as published,

KUCSBear reserves one core to run the conventional Bear operating system. This

core runs some legacy processes in the standard ring 3 configuration and handles

hardware interrupts.

Finally, the prototype is not as stable as a production operating system, nor as

stable as the Bear operating system from which it was built. Bear, like any stable

system, has benefited from thousands of man-hours of debugging and quality control

to generate a system that can run an arbitrary number of processes over an arbitrarily

long time. Although KUCSBear has not yet been subjected to intense debugging, it

131

can run its one reserved legacy core, one reserved kernel core, and 6 user cores running

for many thousands of processes.

The KUCSBear prototype is available as open source software under the MIT

software license at https://github.com/SCSLaboratory/BearOS.

7.2.2 Interrupts

Interrupt Handling in a Ring 0 Process

Much of the communication between the process and the kernel is done via interrupts.

Therefore, interrupt handling in the process is extremely important for its functional-

ity, security, and performance. The interrupt handling initialization involves setting

up required Intel hardware configuration structures and loading interrupt handling

routines.

Interrupt Handling Routines An abstract interrupt handling scheme uses sev-

eral pieces of code to route interrupts to the proper handling routine. Upon receiving

an interrupt 0xYY, the hardware consults a vector table to find instruction pointer at

which it should execute. In order for software later in the handling control flow to have

knowledge of the vector number (0xYY), this instruction pointer is the start of a unique

routine, vecYY, that pushes 0xYY onto the stack before jumping to a more generic

handler, common handler. This assembly routine does the necessary context saving,

calls a C code generic interrupt dispatch routine, restores context, then returns from

the interrupt. The C function called by common handler is intr invoke handler. It

uses the vector pushed in vecYY to find the registered handler from an array of func-

tion pointers, and calls the proper registered handler. This control flow is summarized

in Snippet 7.1.

In the case of a ring 0 process, interrupt handling routines are one of the program

resources that the KUCS kernel needs to manage, or at least oversee, in order to guar-

132

/* Assembly code */

ENTRY(usr_vecA5)

cli

pushq %rax

movq $0xA5 , %rax

movabs %rax , vec

popq %rax

jmp _common_handler

SET_SIZE(usr_vecA5)

# jump to C from an interrupt

ENTRY(_common_handler)

# save context to the struct to be read by the kernel.

SAVE_CONTEXT_STRUCT

# save context for to return to later

SAVE_CONTEXT_STACK

# pass interrupt vector into handler

movabs vec , %rax

movq %rax , %rdi

# jump to C

RELCALL(_intr_invoke_handler)

# restore regs from stack

RESTORE_CONTEXT_STACK

# return

iretq

SET_SIZE(_common_handler)

/* C code */

void (* _intr_handler_array [256])( uint64_t );

void _intr_invoke_handler(uint64_t vector) {

_intr_handler_array[vector ]( vector );

return;

}

Snippet 7.1: When an interrupt is received, a generic interrupt handling scheme goes through a series of functions in both assembly and C code in order to properly dispatch and handle the interrupt.

133

antee that the process cannot “go rogue” and stop responding to requests/instructions

from the kernel. If the process had arbitrary control of the interrupt handling routines

on its processor core, it could maintain control of the core for an arbitrarily long time

until the processor was rebooted or a virtualization-based interruption was deployed.

With the importance of the integrity of these routines in mind, there are several

options for the mechanism to get interrupt handling, including assembly instructions,

into the process’ address space.

• Compile into the application. The assembly and C general handling rou-

tines could be compiled directly into the process. This facilitates loading the

routines into the address space since the application code is already being

loaded. Unfortunately, it gives a lot of control to the application developer

who can, at development-time or build-time, modify interrupt handling to suit

their (possibly malicious) needs. In theory, the kernel could verify the safety

of these routines at load-time but in practice this is non-trivial. Additionally,

these routines may share memory pages with legitimate user-controlled mem-

ory, which could create a challenge for page-based access right control primitives

that could otherwise be placed on the interrupt routines.

• Compile into the kernel. In order to have these routines be kernel controlled,

the kernel could include them in its own source. Then, at run-time, there would

be an already-loaded set of interrupt handling routines for the process resid-

ing in the kernel’s virtual address space. It could copy these instructions into

the user’s space. This gives the kernel complete control over the instructions.

Unfortunately, when the kernel is loaded the relocation information in these

routines will be populated with respect to the kernel’s virtual memory layout.

When the kernel later tries to copy these instructions into the process, it must

replace these addresses with respect to the user’s virtual memory layout, but

the relocation metadata will have been lost.

134

• Write at run-time. To avoid loading issues, the kernel could simply include

a routine to write the proper instructions into the user’s virtual address space.

By doing this ad-hoc, there would be no issues with relocations. However,

this is only feasible if the generic portions of the interrupt handling routines

are extremely small. This is also the least elegant of the solutions; compilers,

assemblers, linkers, and loaders all exist for a reason.

• Compile into a separate file. A compromise between the security issues of

compiling the instructions into the application and the loading issues of com-

piling the instructions into the kernel is to compromise the instructions into a

separate file of their own. This allows the developer to continue to make use of

the toolchain, the process to have access to kernel-controlled routines, and the

kernel to load the instructions via its normal loading mechanisms into the pro-

cess. The downside of this approach is that the generic portion of the interrupt

handling (up to and including accessing the function pointer array) must be

completely self-contained. In other words, it is difficult to link objects between

the compiled process and the compiled interrupt handling library.

KUCSBear compiles a separate usr intr ELF binary. This binary contains the

generic interrupt handling routines and is loaded into the process after the regular

application binary. It is called the Kernel-Controlled User Interrupt Handling Library

(KCUIHL).

The KCUIHL is loaded into the process’ context on non-writeable pages. The

virtual memory isolation guarantees that the process cannot change these protections,

and consequently the process will execute kernel-controlled code any time there is an

interrupt. However, the process can read and execute this memory. Although a

robust security model in the kernel and hypervisor levels should be able to handle

arbitrary execution in the KCUIHL, system designers may prefer to load the library in

a location that is not known to the application. This is possible using any number of

135

randomization techniques, and there are no direct mechanisms for finding it because

it is only ever entered via an int instruction. If the process guessed or discovered the

address of the KCUIHL it would have read and execute access to it, but guessing the

address would likely generate page faults that could be detected by the kernel.

Intel Hardware Control Structures Intel hardware interrupt control structures

such as the Advanced Programmable Interrupt Controller (APIC), Global Descriptor

Table (GDT), and Interrupt Descriptor Table (IDT) are all needed for interrupt

handling in the user process.

The APIC is memory-mapped so that privileged software can write to specific

memory addresses in order to send different types of interrupts. The GDT contains

state information used during the hardware context switch preformed in response to

a local interrupt. Finally, the IDT contains pointers to the handlers that should be

run in response to each particular interrupt.

A detailed description of these structures and the x86 interrupt subsystems is

available in [52, 79]. These structures should only be used by the KCUIHL, and

are only linked to the KCUIHL. Although the user will have the same permissions

to access these structures as the KCUIHL, the structures will be loaded with the

minimum required permissions. For example, the IDT is marked as non-writeable.

This guarantees that only the kernel can control what code is executed on the user

core in response to a particular interrupt.

Configuration of the virtualization layer also protects the GDT and IDT. The

hypervisor guest state allows for vmexits to occur if the instructions used to load these

structures for inspection (lgdt, lidt) or store new ones (sgdt, sidt) are executed.

This allows the virtualization layer to safeguard the integrity of the kernel-controlled

interrupt handling routines on the remote user core.

136

Special Considerations for Drivers As discussed in Section 7.1, driver processes

can be loaded with special permissions in a KUCS design. The first changes could

exist in the virtualization layer, although this would add complication as the drivers

would have to stay on one particular core or the virtualization configuration would

have to follow the driver around to different cores. KUCSBear does not implement

virtualization protection differences between user cores.

Another technique that is possible in the KUCS design but not implemented

in KUCSBear is routing hardware interrupts directly to the appropriate core to be

handled natively in the driver. This would require the same routing mechanisms as

the virtualization; the kernel would have to monitor what core (if any) contained the

particular driver at a particular time in order to route the interrupt to the correct core.

In this case, the driver process could request that the kernel load a driver-provided

interrupt handler into the IDT so that it could service interrupts directly.

Although it does not implement full support for drivers as enabled by the KUCS

design, KUCSBear does run Bear’s VGA daemon under the KUCS paradigm. A

special mapping in the process’ page tables gives it the ability to write directly to

VGA memory. This special mapping could be done at load-time, but in KUCSBear

it is done in response to an initialization system call from the daemon.

IPI Source Determination

When many processor cores are running user processes and concurrently requesting

kernel intervention, an interrupt is not sufficient to tell the kernel what it needs to

do. More specifically, the kernel must determine the source of the IPI before it can do

anything else. Once it knows which core (and therefore which process) has requested

assistance, it can query that core’s memory interfaces to determine what actions to

take next.

Interrupt controller hardware that reported the source of the interrupt would

137

solve this problem efficiently. Unfortunately, the x86 hardware does not provide a

mechanism for determining the originating core of an IPI. As a result, some soft-

ware mechanism must be provided for determining the source of an inter-processor

interrupt. There are three main ways to do this: polling, interrupt vector assign-

ment, and hypervisor intervention. Each method has advantages and disadvantages,

as described below.

Polling The most obvious method for determining the source of an event is by

polling: querying each core’s shared memory interfaces until an event requiring kernel

intervention is found. In fact, a purely polling based solution could do away with

interrupts entirely if the idle time of the kernel was spent constantly querying different

cores, looking for work to do. The obvious disadvantage to the polling method is that

it is computationally intensive; the kernel is spending a lot of time looking for events

to service rather than simply servicing events.

Polling becomes much more attractive when interrupts are integrated as well. In

this case, the kernel would only begin querying cores looking for an event to service

once it receives an interrupt. This method deeply reduces the amount of overhead

involved with the kernel core churning on message queues looking for messages. The

kernel core would do whatever task it wants until it receives an interrupt then it

would search the cores for work to do and service the work before returning to its

prior task.

Unfortunately, the hardware adds yet another restriction that makes polling less

attractive again. The interrupt delivery hardware can only receive two of the same

interrupt vector before one is handled. Any further interrupts sent to the core are

discarded. The possibility of losing interrupts eliminates the option of waiting for an

interrupt and then poll until an event to service is found and processed. Instead, the

kernel would need to wait for an interrupt and then poll until all events to service are

138

found and processed. This eliminates the possibility of missing requests by missing

an interrupt, but increases the amount of time required for finding and servicing user

messages.

In summary, the main disadvantage of polling is performance. It requires a fair

amount of work for finding the source of a request for service. In addition, while its

ability to scale to any number of cores is theoretically an advantage, having complexity

that is linear in the number of cores in such an important system task may become a

major performance bottleneck as processors with hundreds, thousands, or even more

cores become mainstream.

Interrupt Vector Assignment In order to accommodate more than two inter-

rupts simultaneously, the hardware provides 256 individual interrupt vectors that

can be used. This capability can be leveraged to address the problem of interrupt

source identification if different user cores send different interrupt vectors to the ker-

nel. With this solution, the kernel would know based on which interrupt vector it

receives which core sent the interrupt.

One disadvantage of this mechanism is that the hardware prioritizes interrupt

delivery by vector number. This behavior is not configurable. This means that cores

with high-priority interrupt vectors assigned will always cut ahead of other cores

that send interrupts at the same time. Fortunately, this risk is mitigated by the

fact that the kernel only registers interrupts in the interrupt handler, rather than

servicing the interrupts. This means that the kernel spends most of its time ready

to receive interrupts and the risk of two interrupts occurring at the same time and

being delivered out of order is reduced. Furthermore, although it is not implemented

in KUCSBear, a KUCS operating system could simply randomize the vectors used

by particular cores over time, in effect giving every core “a turn” at being the highest

priority core.

139

Another possible issue with this mechanism is that a process with arbitrary write

access to the APIC could send an interrupt using the vector of another core, essentially

impersonating the other process. Hiding the KCUIHL as discussed previously could

mitigate this threat. However, with a sufficiently discerning kernel implementation,

the process could not gain anything by impersonating another process. The real

mechanism for communicating between the kernel and the user process is the shared

memory interface. The interrupt is just an alert that the kernel should query the

shared memory interface to determine what has happened on the core. In the event

that a process sent a vector other than its own, the kernel should notice the lack of

corresponding communication from the core that owns that vector and take remedial

action.

Finally, this solution may not scale to systems with large numbers of cores. For

example, it certainly will not be sufficient for a system with more than 256 cores.

The advantage of this technique is that it offers simple constant-time determi-

nation of the source of an IPI. In addition, it is reasonably simple to implement; a

simple lookup from IPI vector to core is all that is required. Along with the tech-

niques to address the limitations of the method, this makes this an excellent candidate

mechanism. This is the technique used in KUCSBear.

Hypervisor intervention A third technique for determining the source of an IPI

could be by using the hypervisor. In particular, the hypervisor could intercept APIC

accesses from the user core and utilize its trusted access to all of memory to write the

core number in kernel memory. This technique is the most secure but also likely to be

the slowest, especially if the hypervisor is not already monitoring APIC accesses. If

a particular KUCS implementation had the hypervisor monitoring APIC accesses as

part of is security policy, then this mechanism could be the simplest without adding

much overhead.

140

#de f i n e r emote pdpt v i r t ( pcr3 , pml4t idx ) \ ( ( s t r u c t pdpt ∗) phys2v i r t ( ( s t r u c t pml4t ∗) phys2v i r t ( pcr3)−>e n t r i e s [ pml4t idx ] . addr ) )

#de f i n e r emote pd v i r t ( pcr3 , pml4t idx , pdpt idx ) \ ( ( s t r u c t pd∗) phys2v i r t ( r emote pdpt v i r t ( pcr3 , pml4t idx)−>e n t r i e s [ pdpt idx ] . addr ) )

#de f i n e r emot e p t v i r t ( pcr3 , pml4t idx , pdpt idx , pd idx ) \ ( ( s t r u c t pt ∗) phys2v i r t ( r emote pd v i r t ( pcr3 , pml4t idx , pdpt idx)−>e n t r i e s [ pd idx ] . addr ) )

#de f i n e r emote vaddr v i r t ( pcr3 , pml4t idx , pdpt idx , pd idx , p t idx ) \ ( ( s t r u c t pg∗) phys2v i r t ( r emot e p t v i r t ( pcr3 , pml4t idx , pdpt idx , pd idx)−>e n t r i e s [ p t idx ] . addr ) )

Snippet 7.2: If the kernel were to use arbitrary virtual addresses to map each frame of memory used by the remote process, it would require multiple phys2virt lookups for each attempt to find the memory at a particular virtual address within the remote process.

7.2.3 Virtual Memory

The KUCS operating system design runs processes on cores in virtual memory sand-

boxes. The virtual memory context of the process contains only mappings to the text

and data needed by the process. It does not contain any mappings to the kernel or to

its own page tables. This creates a unique challenge when it comes to manipulating

this address space. In particular, the KUCS kernel needs a method for addressing the

memory that comprises the remote process’ memory.

There are several possible methods for doing this. For instance, the kernel could

use arbitrary virtual addresses for each physical frame of memory used in the in the

remote process. The Bear kernel could use the vk module for this purpose as it

manages a pool of unused virtual addresses. Unfortunately, this is not a particularly

elegant solution and keeping track of these mappings in some type of structure like

a reverse page table would incur a large memory overhead. They could be recovered

from Bear’s frame array with phys2virt as suggested in Snippet 7.2, but the lack

of any linkage from one paging structure level to another leads to a slow process for

looking up virtual address for deeper page tables.

In order to mitigate the performance cost of the multiple lookups needed with

per-frame virtual mappings, the KUCS Bear prototype uses a method to maintain

persistent virtual mappings to all the paging structures in each remote process. To

141

do this, a special pointer called the vmem bridge stores the physical address of the

cr3 target of the remote process into one of the entries in the kernel’s PML4T. This

means that the kernel can create a specially constructed virtual address to force the

MMU translations to resolve to a particular PT of the remote process. Using Bear’s

recursive pointer (as described in Figure 2.2b on Page 16) the MMU can also provide

virtual addresses for any higher level page table.

Unfortunately, the vmem bridge will not allow mappings to the lowest level pages

in the remote process. As such, the KUCS Bear prototype still uses the vk module

to manage arbitrary virtual addresses to maintain the mappings for these lowest level

pages. This hybrid approach is shown in Figure 7.3.

Note that the vmem bridge method will only support as many simultaneously

running processes as there are free entries in the kernel’s PML4T; a maximum of

510. A more complete KUCS implementation could use the vmem bridge to address

the most performance-critical processes while using direct mappings to support an

arbitrarily large number of unique remote processes.

7.2.4 Message Passing

Although the asymmetrical multiprocessing design allows for asynchronous system

calls, there are design challenges associated with the implementation of such a mech-

anism. One such challenge that is beyond the scope of this thesis is delivering a

return value to a process that has continued execution immediately after sending a

message. One that was solved in this thesis is the method of providing the message

in a place where the kernel can access it. The user process in Bear’s original message

passing interface discussed in Section 2.2 simply wrote the message onto its own stack

as shown in Snippet 2.1. When the kernel was invoked, the message could easily be

read because the process would not have continued its execution and therefore the

message was preserved on the user stack. In an asynchronous system, the user pro-

142

!"#$%&'"()*"+,-(%"(.%!'-+$/+,-( !!"

!"#$%& '()* +"%,$-./,0/1234"5-./677%&44/8%-94.-,"09:;09#/<07&

513-200.eps

PML4E PDE

Physical Address

PDPE

PTE

Physical Page O�set

Sign Extension

63 0

Page Directory O�set

Page Map Level-4 O�set

Page Directory Pointer O�set

Page Table O�set

Page Map Base Register CR3

64-Bit Virtual Address

Page Directory Pointer Table

Page Directory Table

Physical Page Frame

Page Table

Page Map Level 4 Table

=</;/:9-)87$6543&!2#1)0/.-,'$#+&+*)('&"%$#

513-200.eps

PML4E PDE

Physical Address

PDPE

PTE

Physical Page O�set

Sign Extension

63 0

Page Directory O�set

Page Map Level-4 O�set

Page Directory Pointer O�set

Page Table O�set

Page Map Base Register CR3

64-Bit Virtual Address

Page Directory Pointer Table

Page Directory Table

Physical Page Frame

Page Table

Page Map Level 4 Table

Kernel Page Tables

Remote Process Page Tables

vm em

b rid

ge

D irect M

apping

Figure 7.3: The KUCS kernel core uses two methods for addressing the memory of a remote process. A pointer in the top level PML4T allows virtual addressing to the paging structures of the remote process, while the vk module allows for direct mappings to the lowest level of pages.

143

cess may return from the msgsend call before the kernel has a chance to access the

message. This would change the user’s stack and thus deny the kernel from accessing

the message.

Naively, the need for a persistent location for storing messages could be solved

simply by the user’s malloc implementation. However, malloc may need to call the

umalloc system call, leading to an recursive problem with no base case. The solution

implemented in KUCSBear is a ring buffer stored in memory shared by the kernel

and the process.

The Message Ring Buffer

A ring buffer is a common data structure for tasks involving asynchronous production

and consumption of data. It is especially convenient with just one producer and one

consumer, because in that case it does not need mutual exclusion or other special

synchonization techniques. That special case suits this application; the process is the

sole producer while the kernel is the sole consumer.

In order to use the ring buffer, the process simply uses four global variables:

sc rbuf, sc rbuf head, sc rbuf tail, and sc rbuf size. The process relies on

the kernel to populate these variables with the appropriate values at its load-time, so

that it may simply use them without allocating any of the underlying memory. When

writing a message, it simply writes to the current head index of the ring buffer, as

long as the buffer is not full. This is shown in Snippet 7.3.

Similarly to Snippet 7.3, the kernel can consume messages by using the tail

index into the ring buffer array. However, the kernel side is slightly more complicated

due to the fact that it is not mapped into the same page tables as the user process.

Therefore, it cannot simply dereference the same addresses that the user process is

dereferencing. The process of initializing and then accessing the ring buffer from

the kernel is described in detail here as an example of the issues created when two

144

Message_t *produce_rbuf_msg(Message_t *msg) {

uint64_t next_head;

Message_t *ret;

/* increment the head */

next_head = (_sc_rbuf_head + 1) % _sc_rbuf_size;

/* make sure there’s room for the new msg */

while ( _sc_rbuf_tail == next_head ) ;

/* store message pointer */

_sc_rbuf[next_head] = *msg;

/* will return the address where the msg was stored */

ret = &_sc_rbuf[next_head ];

/* save the new head */

_sc_rbuf_head = next_head;

return ret;

}

Snippet 7.3: The process needs a persistant memory location to store messages so that it may continue executing while the kernel retrieves, processes, and responds to its message. This is the procedure for adding a message to the ring buffer used to accomplish this persistant and asynchronous memory space.

145

/* Allocate (and record kernel ’s address to) Ring buffer */

proc ->k_sc_rbuf = vmem_alloc_remote(proc , usr_rbuf_addr ,

PAGE_SIZE , PG_NX);

/* tell the user where the ring buffer will be */

*remote2vaddr(proc , proc ->usr_sc_rbuf) = usr_rbuf_addr;

/* tell the process how many messages the ring buffer will hold */

*remote2vaddr(proc , proc ->sc_rbuf_size )= PAGE_SIZE/sizeof(Message_t );

/* find the kernel ’s address for user’s global head and tail vars */

proc ->k_sc_rbuf_tail = remote2vaddr(proc , proc ->usr_sc_rbuf_tail );

proc ->k_sc_rbuf_head = remote2vaddr(proc , proc ->usr_sc_rbuf_head );

/* initialize the user’s head and tail index variables */

*proc ->k_sc_rbuf_tail = 0;

*proc ->k_sc_rbuf_head = 0;

Snippet 7.4: This code demonstrates some of the pointer translation and bookkeeping necessary to create a memory space that is shared between two contexts that do not share a page table.

contexts that do not share page tables attempt to access common memory.

The process of initializing the ring buffer from the kernel is summarized in Fig-

ure 7.4 on Page 148. First, the loader makes a note of the loaded location of the

relevant global variables that will be used by the process: sc rbuf, sc rbuf head,

and sc rbuf tail. The kernel must initialize these values with the address of the

allocated memory to be used for the buffer, 0, and 0, respectively. However, the loader

only knows the address that the user will use to access these variables which, due to

the separate address spaces, is different from the address that the kernel can use to

access them. Thus, the kernel must take care to find (and record) the addresses it can

use to access the global variables and the ring buffer itself, as shown in Snippet 7.4.

After initialization, the kernel can consume messages from the ring buffer by being

careful to use its own addresses to access the shared memory, as shown in Snippet 7.5.

146

/* find and increment the tail (should not be modified by proc) */

tail = (*proc ->k_sc_rbuf_tail + 1) % proc ->sc_rbuf_size;

/* get kernel -local copy of message */

Message_t mp = proc ->k_sc_rbuf[tail];

/* OMITTED - handle the message */

/* ’consume ’ msg; tell the proc about the new tail */

*proc ->k_sc_rbuf_tail = tail;

Snippet 7.5: After initializing the addresses for itself and for the process to access the common memory of the ring buffer, the kernel can consume messages from its tail. This is the procedure it uses to do so.

System Calls

As described in Section 7.1, the system call in an ASMP kernel is an interprocessor

interrupt from the user core to the kernel’s core. This can be accomplished simply

from the process by simply writing to the APIC to generate an IPI. Unfortunately, this

direct method fails to accommodate a situation in which the kernel needs to recover

the process’ state. With the process and kernel sharing a core, this is straightforward

because the privilege level change that happens when changing from the process to

the kernel saves the state onto the stack. No such mechanism exists when invoking

the kernel on a separate core.

Although a scheme could ask the process to save its own state to a shared memory

interface as discussed in the context of message passing, this is extremely complicated.

For example, the instruction pointer changes with each state saving instruction; this

is much like trying to change a bicycle tire without dismounting the bike! In addition,

it relies on cooperation from the application developer to faithfully and accurately

save their own state before issuing the IPI.

These issues can be avoided with an indirect method in which the process invokes

a local interrupt on its own core. This saves its current state automatically during

the hardware routine of switching to an interrupt stack and going through the kernel-

147

addr label value

_sc_rbuf

_sc_rbuf_head

_sc_rbuf_tail

A P

B P

C P

D P

E P

F P

G P

addr label value

usr_sc_rbuf

usr_sc_rbuf_head

usr_sc_rbuf_tail

A K

B K

C K

D K

E K

F K

G K

H K

I K

J K

K K

k_sc_rbuf_tail

k_sc_rbuf_head

k_sc_rbuf

0

0

0

0

0

0

G K

H K

I K

E P

B P

C P

D P

Process Virtual Address Space

Kernel Virtual Address Space

Virtual Memory Abstraction

Virtual Memory Abstraction

Physical Memory

_sc_rbuf[0]

_sc_rbuf[1]

_sc_rbuf[2]

k_sc_rbuf[0]

k_sc_rbuf[1]

k_sc_rbuf[2]

1

2

3

1 Loader tells the kernel where in the process’ address space the important global variables are loaded.

2 The kernel allocates the ring bu�er, maps it into the process’ address space, and populates _sc_rbuf.

2

3 The kernel saves the addresses it will use to access the ring bu�er and head/tail indeces.

Figure 7.4: Initialization of the necessary memory and variables for the shared ring buffer used to pass messages from the user process to the kernel.

controlled interrupt handling routines. In the case of a system call, these routines

then forward an IPI to the kernel core where the kernel will now have access to

the correctly saved state. Figure 7.5 compares the direct and indirect system call

methods.

The extra interrupt and state saving in the indirect system call method adds a

performance cost over the direct IPI method. Section 7.3.2 quantifies this cost against

the direct IPI and conventional system call mechansims.

Additionally, some of the performance cost of the indirect system call method

could be recovered by using the indirect method only where the execution state of

the caller is important. Calls such as fork() require that the kernel have knowledge

of the state of the caller, but simpler system calls such as getpid() can be serviced

148

Kernel CoreUser Core User Core

Handle IPI

Process Code

Process Code

IDTIDT KCUIHL KCUIHL

IPI

INT

IPI

Direct Indirect

Figure 7.5: With the user and kernel residing on different cores, a new mechanism is required to dispatch system calls. Two possible alternatives to the traditional INT/IRET system call mechanism are the direct IPI and the indirect IPI.

faithfully without full knowledge of the caller’s state.

Kernel-Space System Call Servicing

For every system call or other event dispatched from the user process, the kernel

must do some work to handle the event. In a conventional operating system, the

interrupt that triggers a privilege mode switch on the core routes execution to a

kernel event handler which can branch further, routing control to the appropriate

kernel functionality for the particular event received. In a KUCS operating system,

the IPI delivery can be configured the same way.

With the conventional operating system design, this synchronous event handling

is the obvious solution. A process is running on a single core and that core is either

in user mode running code for the process or in kernel mode running code for the

process. This means that when the user process requests work from the kernel, the

whole core will be in kernel mode and might as well finish the kernel-mode work.

With a KUCS design, though, a task can be considered as running on 2 cores

simultaneously; the user mode portion on one core and the kernel mode portion on

149

another. In this case, there can be some more flexibility in when the kernel services the

request from the user core. One asynchronous approach involves having the kernel

core in a loop servicing pending requests for work at idle, and then the interrupt

handler simply registers a new pending event. This method minimizes the time spent

in interrupt handlers; an especially important consideration with a KUCS design in

which one kernel core is handling requests from multiple different user cores.

The KUCSBear prototype uses asynchronous kernel event handling. The interrupt

handler in the kernel writes to a ring buffer of pending events. It is important that

the interrupt handler does no more than this because the kernel may have been doing

other work when the interrupt arrived and a complex workload in the handler could

interfere with the state of the interrupted task. When not in this interrupt handling

routine, the kernel core is continuously reading pending events from the ring buffer

and servicing them.

Note that the KUCSBear prototype has not implemented “truly asynchronous

system calls” as discussed in Section 7.1. The user process still suspends its own

execution until its requested task is serviced by the kernel. The completion of truly

asynchronous system calls is beyond the scope of this work, although it is an exciting

and novel workflow, possible only on a KUCS operating system.

Additionally, note that the asynchronous kernel event handling facilitates “watch-

dog” tasks during free time on the kernel core. This is because any work done in

an interrupt handler needs to be independently locked from any work done outside

of an interrupt handler. If the kernel handles the events in the interrupt handler,

as in a conventionally designed operating system, then fine-grained locking would be

required to separate the watchdog work from the event handling work. Even with

fine-grained locking, some watchdog work would be impossible because it would not

be able to survive being interrupted and having state change. This is not an issue in

conventionally designed operating systems because the kernel is only ever run in an

150

interrupt handler; no work is done outside of an interrupt handling context.

The KUCSBear prototype demonstrates how to do “watchdog” work in the kernel

along with servicing user requests by harvesting the memory of dead processes in

between servicing events from user cores.

7.2.5 Scheduling

Context Switching

Context switching without the kernel on core is difficult because it cannot act as

an intermediate context between two processes. Not only must these two processes

manage to sync in order to change from one to the other, but they must do so without

compromising their privacy. Some possible solutions, loosely ordered from “hardest

to get right” but fastest to “easiest to get right” but slowest include:

1. Direct transfer ( A→ B ): process A is given cr3B, switches to B.

2. Intermediate context ( A → K ′ → B ): Process A is given cr3K′ where K ′ is

a context built by kernel, specifically designed to intermediate between A and

B. In other words, it has landing code at the address of the switching code in

A, switching code at the address of landing code in B, and communicates with

the kernel core in between.

3. Modified Destination ( A → B′ → B ): As above, but kernel embeds K ′ into

B’s address space while B is blocked, and this version of K ′ (B′) deletes itself

from B before jumping to any B controlled code.

4. Hypervisor ( A → H → B ) By doing a vmcall, the hypervisor can do any

manipulation necessary to switch the operating context on the core. As the hy-

pervisor in this design is an extension of the kernel, it is trusted and omniscient.

This makes it a natural entity for mediating the context switch.

151

Fortunately, with just a bit of care taken, the fastest solution can be implemented

safely. The first important technique is that the scheduling function will be loaded at

the same location in every process, solving the issue of differing locations of “switch-

ing code” and “landing code”. This works similarly to scheduling between different

processes’ kernel contexts on conventional operating systems. The actual virtual ad-

dress switch always happens at a common address, regardless of what processes are

being switched to or from. Then, the stack for that particular process can return

execution to the appropriate place in its execution path.

Furthermore, state restoration happens only after the new address space is loaded.

This ensures that, when switching from process A to process B, process B never has

access to any of process A’s general purpose register values.

Finally, the scheduling function is provided as a non-writeable routine into the

process’ address space. This guarantees that the process is forced to execute the

scheduling routine as written. Additionally, it is put into the process’ IDT as an

interrupt handler for the SYSTICK interrupt, ensuring that it must be executed at

the border of a process’ time quantum.

Bootstrapping (and Idling)

Running the first process on a core can be an extra challenge. While each process is

set up to transfer control to another process, the system initialization code running

on the core after boot is not set up for this. Although one could load interrupt

handling and user-space context switching code into the system initialization context

and then switch from it as if it were a regular process, this might inhibit kernel-space

randomization efforts. Furthermore, this method represents a confusion of semantic

differences between system code and system-provided user library interrupt handling

code.

Instead, the prototype uses the idle process as an intermediate between system

152

initialization code and user code on a particular core. Like most operating systems,

there is one idle process per core. These are loaded separately from regular user

processes and serve as a placeholder context to be deployed when there is no useful

work for a particular core to do. The only things present in the idle process are:

1. Idle Function. This extremely simple assembly routine writes the cr3 of the

idle process, then makes sure interrupts are on and halts in a tight loop. This

function is entered only once – the first time the idle process is run – and then

each time the process is running it will continue to loop on the halt.

2. Kernel-Controlled User Interrupt Handling. An instance of the KCUIHL

is copied into the idle process. This includes the scheduling function. These

routines are the mechanism by which the core can switch from the idle process

to another process. The kernel can initiate this switch by sending an interrupt

to the idle process.

After the idle processes are initialized, the kernel context doing the initialization

on each core calls the virtualization initialization function. The kernel context on

the core will become that core’s hypervisor context. The guest virtual machine is

configured, as described in Chapter 4 in the context of ExOShim, so that the guest

that is run on vmlaunch is the ring 0 idle process. This method allows a secure way

to change from the kernel initialization context on a user core to the idle process,

which can then schedule other processes as appropriate. The former kernel context

will never again run on that core unless a vmexit restores the hypervisor.

The kernel core initializes its virtualization layer the same way as ExOShim. In

particular, it sets up a second kernel context which will execute the shim initialization

function, configure the host state to match itself, configure the guest state to match

the original regular kernel context and then issues a vmlaunch to set itself up as the

hypervisor and restore the kernel state prior to its being scheduled.

153

Special consideration is needed during the first context switch to a particular

process. The first time a process is run, it should be executing from a clean state.

However, since the routine that caused it to be scheduled was actually an interrupt

handler in another process, the core will think that it is still in an interrupt handler

when it begins to execute the new process. Therefore, the context switching routine

must build a stack frame such that an iretq instruction points execution towards the

beginning of the process’ instructions and counteracts the interrupt from the previous

process. In future context switches, the process will have triggered a context switch

via interrupt during the last time it ran, so it continues operating without an iretq

in the context switching routine. Upon restoration, the process’ stack will unwind

and bring it back out of the interrupt handler that it triggered a full time-quantum

(or more) ago.

Scheduling Proper

With the mechanism for initialization and context switching described, the main

scheduling algorithms can be discussed. The challenge associated with choosing the

next process to run is not one of how ; like the original Bear kernel, KUCSBear simply

uses a round-robin scheduling scheme. Instead, the challenge is associated with the

when. The solution for when to choose a process to run is trivial in a regular operating

system where the kernel is invoked during context switching. Since it needs to run

to switch from one process to another, the kernel can simply choose which process to

run during that context switch.

Unfortunately, with the processes running on a core remotely, without the kernel,

context switches no longer invoke the kernel. Therefore, a target process must be

chosen sometime before the context switch is to occur. This can be done immediately

before switching or early in its time quantum. The former approach requires a syn-

chronous communication with the kernel; the process must wait for an answer after

154

asking what process to run next before it can continue. The alternative approach can

be accomplished asynchronously instead. In the asynchronous case, the process can

send a message to the kernel as soon as it begins to run and assume that the kernel

will have responded by the time it is next ready to switch contexts.

The benefits of the asynchronous scheme include a reduced overhead time during

context switching. Additionally, it serves as a “check-in” event where the kernel can

check on the new process at the beginning of its time quantum. However, there may

be issues with the asynchronous method. If no process is ready to run at the time

of check-in, an “idle” process will be run. If a more interesting process becomes

available during the time quantum, a careless implementation might waste a time

quantum idling while another process is waiting to run. Salvaging that time is a

difficult implementation task with sensitive timing requirements.

The synchronous scheme may be slower because the kernel cannot do its schedul-

ing work in parallel with process execution. However, its implementation is more

straightforward because it eliminates the complication from the asynchronous case in

which the scheduling algorithm is run early and possibly without complete system

state. Additionally, this method means that the “check-in” is not necessarily the first

event of the process’ time quantum. Although that is an attractive property of the

asynchronous solution, it is not strictly necessary. Any event can serve as a check-in

for the kernel’s purposes, and because the context switching mechanism resides in

kernel-controlled code (in the KCUIHL) the kernel can trust that the context switch

occurred faithfully.

In order to avoid unnecessary TLB flushes, the scheduling function in the KCUIHL

checks whether it is about to schedule a new process or if it will remain on the core

for another time quantum. In the event that it is not leaving the core, it skips the

step of writing to the cr3 target. This avoids a TLB flush on the core, which would

slow the process down in the beginning of the next time quantum by causing many

155

cache misses.

(More) Secure Scheduling

Without the kernel on the core to supervise the user process, special care must be

taken to ensure that the remote core is acting faithfully under the direction of the

kernel. This is accomplished by the KCUIHL: the interrupt handling library mapped

into the user process. As discussed previously, this library cannot be modified by the

user process because it is marked as non-writeable in the page tables. Consequently,

when the timer interrupt is triggered on the remote core the KCUIHL acts as an

agent of the kernel that preforms the scheduling transition.

Although the design ensures that the user core must schedule new processes ac-

cording to the kernel’s scheduling algorithms, a mechanism can be used to police

this transaction further. In order to guarantee that the remote core is behaving as

expected, each process can be loaded with a unique and random identifier. This iden-

tifier will be loaded into read-only memory for the process. When scheduling occurs,

the process will read its identifier and write it to a predefined memory location.

The kernel can read this memory location and verify that the identifier it sees

belongs to the process that it expects to be running on the remote core at a given

time. In order to avoid collusion between processes, the identifier can be written in

the kernel-controlled code.

Note that if the kernel-controlled code in the user process is corrupted, there is

little that this mechanism can do. Without a trusted agent on the core, the kernel

cannot expect to have any control over the remote core. In the meantime, however,

this method can provide an additional verification step to verify that the remote core

is operating the way that the kernel expects it to be.

In the event of the failure of these software mechanisms, the virtualization layer

could be deployed to recover the system. The virtualization layer on each core is a

156

trusted part of the system code that can act as a reliable agent of the kernel on each

remote core.

7.3 KUCS: Evaluation and Analysis

7.3.1 Security

Section 7.1 discussed several ways in which the KUCS operating system design offers

increased security. KUCSBear realizes many of these features, including hard separa-

tion between user- and kernel-contexts, offering resistance to ret2usr and kernel code

injection type attacks, as well as providing protection from Meltdown [109].

As discussed in Section 7.2, KUCSBear loads each user process into a virtual

memory context without any mappings to any kernel memory. This virtual memory

isolation is sufficient to offer protection from Meltdown, which utilizes these mappings

in a process with weak user- and kernel-space separation in order to leak kernel

memory.

The virtualization layer on the kernel core profiles the memory when the kernel

is finished initializing. The same mechanism used by ExOShim, as described in

Chapter 4, is used to ensure that for the lifetime of the kernel, the kernel code pages

loaded at initialization time can only be executed; not read or written. However,

the fact that only kernel code is ever executed on the kernel core can strengthen this

guarantee. All memory that is not marked as kernel code during initialization can

be marked as non-executable for the lifetime of the system, even by the kernel. This

defeats the possibility of executing any non-kernel code on the kernel core, mitigating

the threats of ret2usr and kernel code injection attacks. It even defeats the threat

of ret2dir [92] type attacks, even though they are classified as a kernel design flaw

rather than a conventional privilege escalation mechanism.

However, KUCSBear is designed to be a proof-of-concept and case study for the

157

KUCS operating system design. It is not provided as a secure operating system. As

such, there are some implementation-specific security holes that need to be addressed

for a secure KUCS system to be deployed.

KUCSBear runs user processes in ring 0. Unfortunately, it does not fully repro-

duce ring 3 protections in this context. Some operations, such as loading a new IDT

or GDT, are easily prohibited by simple configuration of the virtualization layer on

the user cores. Unfortunately, other restricted options are not so easily restricted.

Consider the instruction cli which disables interrupts on the core. This is an in-

struction that should not be allowed, because the kernel controls the user core by

sending it interrupts. Unfortunately, the kernel-controlled user interrupt-handling li-

brary needs to call cli in order to reliably process potentially concurrent interrupts.

This means that the user core needs to call cli, but only under certain conditions.

While the KUCSBear prototype does not deal with this issue, there are several

possible solutions. For example, a security policy implemented in the virtualization

layer could be consulted when cli is called and allow or deny the instruction based

on the calling context. Alternatively, the kernel could monitor the behavior of each

user core through its normal interactions with the core, and take corrective action if

any core is misbehaving. Finally, non-maskable interrupts (NMIs) could be used to

force core behavior regardless of the cli.

Giving the process access to the APIC also poses unique security challenges. The

process needs access to the APIC in order to send IPIs to communicate with the ker-

nel. However, with arbitrary access to the APIC, the process could spam cores with

interrupts, falsify interrupt-based messages, or send special signals, such as start-up

interrupts, that could affect other cores. KUCSBear does not protect from this risk.

One method to make this attack much harder for the attacker would be through ran-

domization of the mappings to the APIC. If the APIC is linked only to the KCUIHL,

and the user process uses a local interrupt to access the KCUIHL, there would be no

158

way for the process to know the location of the APIC mapping. However, an infor-

mation leak or a very good guess could give the user direct and unrestricted access

to the APIC. A secure implementation would want to use the virtualization layer to

moderate accesses to the APIC from the user cores.

Another option for implementing a secure KUCS implementation on x86 hardware

would involve loading the user process into ring 3, with the KCUIHL loaded into ring

0. The user process would still not have any mappings to any kernel memory and

would still be run on a core that never runs the kernel. However, application code

would be run in ring 3 and therefore access to privileged instructions or the hardware

would not pose any threat. Instead, a local interrupt would route execution into the

KCUIHL in ring 0 which could then write to the APIC and preform any required

instructions. This design would be more secure, but adding a privilege mode switch

during system calls would incur an additional performance cost.

7.3.2 Performance

System Call Mechanisms

The system call mechanism is one of the biggest differences between KUCS and

conventional operating system design. The conventional method, using local INT or

SYSENTER instructions to change from ring 3 to ring 0 and IRET or SYSEXIT to change

back, is described in Chapter 2. In KUCS, the user can send an IPI to request work

from the kernel and monitor shared memory interfaces for a response.

Section 7.2.4 describes an extension of the IPI mechanism in which the user core

first issues a local interrupt to direct execution into the KCUIHL which actually issues

the IPI. This is referred to as an “indirect IPI” mechanism.

A fourth system call mechanism has been developed in response to the Meltdown

vulnerabilities. Most major operating systems have implemented system calls that

use local interrupts to change privilege from ring 3 to ring 0, but only find a stub in

159

ring 0. This stub issues a page-table swap in order to access the kernel. In order to

verify that the kernel never shares virtual mappings with the process, another page

table swap is required before returning control to the user process. This is actually

a return to a mechanism developed in the 4G/4G Linux Split project [118] to give

32-bit user processes more virtual memory by removing the kernel from their virtual

address space.

Microbenchmarks were developed to measure the relative performance of these

four system call mechanisms. In order to isolate the behavior being measured, the

tests were conducted on a system that did no other work. All peripheral input/output

was suspended, and even the timer interrupt was disabled. Each of the cores either

executed its test program or simply halted.

First, the performance of the system call mechanism itself was measured. This

“one-way” ping test measured the time between the “user” issuing a request for work

and the “kernel” receiving the request. Also of interest is the “round-trip” ping test

time, measuring the time between when the user issues a request for service and when

it can continue its work. Finally, a third test simulated a “real-world” ping in which

the kernel needs to do some work in response to the request from the user. This

test is important because the meltdown system call mechanism impacts the speed of

kernel processing.

In all three of these tests, the built-in Time Stamp Counter (TSC) was used to

measure the mechanisms. A particular core reads its TSC, then indicated that the

test should begin. When it saw an indication that the test was over, it sampled its

TSC again in order to evaluate the number of cycles consumed during the test. In

all tests, commonly mapped memory was used to coordinate between different cores.

This memory was mapped as uncacheable on each core in order to avoid skewing the

results of the test with cache-coherency latency.

For each test, the IPI and Indirect IPI mechanisms used 3 cores. The first read its

160

TSC, gave the signal to start the test, waited for a “done” signal, and then read its

TSC again and printed the result. The second core waited for the “done” signal and

then triggered an interrupt. For the IPI case it sent an IPI to the third core while for

the Indirect IPI case it triggered a local interrupt whose handler sent the IPI to the

third core. In the case of the one-way test, the third core just did a busy-wait until

it received an IPI whose handler indicated “done”. In the other two tests, however,

its IPI handler would just return (for the round-trip test) or do pointer-chasing and

memory manipulation (for the real-world test) before allowing the second core to

continue and indicate “done”.

The conventional INT/IRET mechanism tests also used one core to simply begin

the test, monitor for completion, and measure the difference. However, where the

IPI and Indirect IPI mechanisms needed 2 additional cores (to simulate the user and

kernel cores) the traditional mechanism only needed a single core (because the user

and kernel run on the same core). In the one-way test, the second core waited in ring 3

for the signal to begin the test. It then issued a local interrupt that changed privilege

level to ring 0 where the core declared the end of the test. In the round-trip and

real-world cases, the end of the test was declared back in ring 3 after the kernel had

used the IRET instruction. In the round-trip case the kernel returned immediately,

but in the real-world case it did the same pointer chasing and memory manipulation

tasks as mentioned previously.

Finally, the Meltdown Patch mechanism was tested in the same way as the con-

ventional mechanism except the kernel wrote a new CR3 target to change page tables

as soon as it entered ring 0 and again before it left. For this test, global paging was

disabled because Meltdown patches cannot allow for the TLB to contain mappings

to kernel code while the user is executing; global paging would allow this.

Each of the three test cases was run 100 times with each of the four system call

mechanisms of interest. The results of these 12 tests are summarized in Figure 7.6

161

(on page 163) and Table 7.1 (on page 164) and discussed below.

Figure 7.6a and Table 7.1a show the results of the one-way mechanism for all four

mechanisms. The simple IPI is the fastest, while the Indirect IPI is the slowest. This

is not very surprising since the Indirect IPI, which does both a local interrupt and

a simple IPI, has to do almost the same work as the INT/IRET mechanism and the

IPI mechanism combined. However, it does not need to change privilege levels like

the INT/IRET mechanism does. This explains why the Indirect IPI mechanism is still

faster than the sum of the IPI and INT/IRET mechanisms. Somewhat more interesting

is the fact that the extra page table switch associated with the Meltdown patch

mechanism does not seem to substantially degrade performance over the traditional

INT/IRET mechanism in this test.

More is revealed in the round-trip tests, summarized in Figure 7.6b and Table 7.1b.

In the case of the IPI and Indirect IPI mechanisms, the acknowledgment to complete

the test takes place over a shared memory interface. This is much faster than the

INT/IRET and Meltdown patch mechanisms which need to do another privilege mode

switch to return to ring 3. This is why the round-trip is only an average of 8% slower

than the one-way for the Indirect IPI but 26% slower for the INT/IRET. Additionally,

the results are beginning to indicate that the Meltdown patch impacts performance

compared to the standard INT/IRET mechanism.

The real-world tests illustrated in Figure 7.6c and Table 7.1c show that the Indirect

IPI mechanism used in KUCSBear was 3.48% slower than the INT/IRET mechanism

used in Bear. These tests also reveal the potential cost of the Meltdown patch method.

The need to flush the TLB before doing the kernel work significantly slows down the

kernel’s work to service the user’s request. Even with the small toy tasks completed

during this test, the Meltdown patch slowed down enough that even the Indirect IPI

mechanism was faster. This is consistent with the results of the 4G/4G project [118]

that inspired the Meltdown patches, which saw a performance cost as high as 30%.

162

IPI Indirect IPI INT/IRET Meltdown Patches

10 00

25 00

40 00

C lo

ck C

yc le

s

(a) One-way performance of different system call mechanisms. In other words, this measures the latency from when a user process requests kernel work until the kernel begins its work.

IPI Indirect IPI INT/IRET Meltdown Patches

20 00

40 00

60 00

C lo

ck C

yc le

s

(b) Round-trip performance of different system call mechanisms. In other words, this mea- sures the latency from when a user process requests kernel work until the process can resume after being acknowledged by the kernel.

IPI Indirect IPI INT/IRET Meltdown Patches

90 00

12 00 0

15 00 0

C lo

ck C

yc le

s

(c) Microbenchmarks that simply get an acknowledgement from the kernel do not accurately capture the implications of different system call mechanisms on performance. In particular, the mechanism used by Meltdown Patches slows kernel work dramatically.

Figure 7.6: Microbenchmarks exploring the performance implications of various sys- tem call mechanisms.

163

System Call Mechanism CPU Cycles

Max Min Average IPI 2932 1044 1306.4

Indirect IPI 4520 1764 2486.36 INT/IRET 3004 1096 1756.28

Meltdown Patches 3360 1048 1742.76

(a) In the one-way test, the IPI is the fastest system call mechanism while the Indirect IPI is the slowest. In this narrow test, the Meltdown Patched mechanism is not meaningfully different from the INT/IRET mechanism.

System Call Mechanism CPU Cycles

Max Min Average IPI 2988 1252 1710.72

Indirect IPI 4424 2088 2688.88 INT/IRET 6144 1484 2210

Meltdown Patches 4364 2192 2771.32

(b) The INT/IRET and Meltdown patched mechanisms suffer more than the IPI and Indirect IPI mechanisms in the round-trip test case because the former mechanisms require a second context switch while the latter use a shared memory interface to return control.

System Call Mechanism CPU Cycles

Max Min Average IPI 11396 9236 9753.8

Indirect IPI 12056 9984 10641.04 INT/IRET 13952 9552 10283.84

Meltdown Patches 16184 11036 11689.68

(c) With a test load on the kernel side, the performance cost of changing page tables on every system call is becoming more clear as the Meltdown patched mechanism slows considerably.

Table 7.1: Microbenchmarks exploring the performance implications of various system call mechanisms.

164

Test (CPU Cycles) Multiply (×109) Add (×109) Divide (×1010)

Bear Max 2.2238 1.3599 1.0747 Min 2.2232 1.3311 1.0744 Avg 2.2237 1.3391 1.0746

KUCSBear Max 2.2233 1.3593 1.1976 Min 2.2225 1.2739 1.0741 Avg 2.2228 1.3312 1.09892

Average Performance Cost -0.03% -0.59% 2.26%

Table 7.2: AIM9 Benchmark Suite [6] running on KUCSBear and the conventional Bear operating system. Each test type was run 100 times.

CPU Performance

The Bear operating system runs the industry-standard AIM9 benchmark test suite [6].

This is as a CPU intensive benchmark; it runs on a single core and executes a series of

CPU intensive add, multiply, or divide instructions. These tests are single-threaded

and therefore the number of cores running on the system does not have a meaning-

ful effect on the performance of these tests. As such, it is simple to compare the

performance of the conventional Bear operating system to that of KUCSBear.

The results of 100 iterations of the AIM9 test suite are summarized in Table 7.2.

In the case of the Multiply and Add tests, the performance costs observed in KUCS-

Bear are negligible. In the case of the Divide test, on the other hand, a 2.26% average

performance cost was observed. The explanation for the difference between the Mul-

tiply and Add tests and the Divide test may be that because the Divide test takes an

order of magnitude longer than the others, the small amount of additional overhead

incurred by KUCSBear’s Indirect IPI mechanism during timing interrupts begins to

show an effect.

Despite the small performance cost in the Divide case, these AIM9 tests show that

there is no dramatic difference in CPU performance between Bear and KUCSBear.

This makes sense because each system is simply running a user process on a core. The

only appreciable difference between the operating context of the two user processes is

165

that the KUCSBear process is running in ring 0 while the conventional Bear process

is running in ring 3. During general purpose computation, this should not affect CPU

performance.

Overall Performance

A malloc test [103] is used as an overall performance cost metric. The test is memory-

intensive, but also runs 100 processes and uses many system calls. As such, it exercises

the system call mechanism, scheduling mechanisms, kernel bookkeeping, and memory

systems. In addition, it can demonstrate the effect of multiple cores and multitasking

because it uses so many processes relative to the number of cores.

Figure 7.7 and Table 7.3 show the average cycle count over 100 trials of the

malloc benchmark test on Bear and KUCSBear for several core counts. Note that

this is specifically counting active user cores. In the case of the KUCSBear prototype,

there are n − 2 user cores on a system booted with n cores, because one is reserved

for the kernel core and one is reserved to run the conventional Bear OS to handle

hardware interrupts and unported drivers.

Several interesting trends emerge from Figure 7.7. The first is that additional

user cores after 4 actually slow performance of the benchmark rather than increase

it. This is likely due to contention over the kernel. Although Bear’s “big kernel

lock” and KUCSBear’s corresponding single kernel core may increase the likelihood

of contention, a kernel with fine-grained locking may not perform much better because

all the processes being tested are identical and would therefore fight over the same

locked kernel paths.

It seems that before 4 user cores, KUCSBear is dramatically slower than Bear.

However, an interesting pattern exists in this region of the data. The performance

with two KUCSBear user cores is nearly identical (0.8% faster) to the performance

with one Bear user core. The same is true for the relationship between three KUCS-

166

User Core Bear KUCSBear Performance Count (Cycles ×109) (Cycles ×109) Cost

1 11.2302† Not Measured* N/A 2 5.6779 11.1364† 96.14% 3 3.8218 5.6648 48.22% 4 2.8942 3.1680 9.46% 5 2.9426 3.2258 9.62% 6 2.9948 3.3208 7.88% 7 3.0465 N/A‡ N/A 8 3.1004 N/A‡ N/A

* Stability issues combined with a very long run-time will not allow the full test to run with 1 user core on KUCSBear. † These values are single runs, not the average of 100 trials,

due to long run times of the test. ‡ KUCSBear reserves 2 cores for system use, so trials using

more than 6 user cores are not possible.

Table 7.3: The average cycle count required to run the 100 process malloc test [103] on Bear and KUCSBear with different numbers of active user cores.

1 2 3 4 5 6 7 8

3e +1 0

4e +1 0

5e +1 0

6e +1 0

7e +1 0

User Cores

C lo

ck C

yc le

s

Bear KUCSBear

Figure 7.7: The average cycle count required to run the 100 process malloc test [103] on Bear and KUCSBear with different numbers of active user cores. KUCSBear can only achieve a maximum of 6 user cores because one core is reserved for the kernel and a second is reserved for legacy functions.

167

Bear user cores and two Bear user cores; KUCSBear is 0.2% faster in this case.

Combined with the close relationship and relatively low performance costs seen in

user core counts of four and above, this pattern suggests that there is some other

factor at play.

Although this data leaves some questions that would require further study of

the prototype to understand fully, the behavior at the higher user core ranges is

consistent enough to make a fair estimate of the performance cost of the KUCSBear

prototype. As implemented, KUCSBear imposes less than a 10% performance cost

for a particular user task at scale.

Another important consideration is the fact that any KUCS operating system

will reserve at least one core for the kernel, meaning that there will be one fewer

cores available to run user processes. This could impact performance greatly for a

workload that makes effective use of all cores. The malloc test does not seem like

a good one to estimate this effect, because decreasing performance with more cores

suggests that it cannot make good use of all of the available cores. However, according

to Amdahl’s Law [12], even the most highly optimized and parallelized workload will

experience a “diminishing return” as the available core count increases. Therefore,

in the future (as hardware continues to scale and offer more and more processors)

removing any constant number of cores from those available will not significantly

decrease the performance of any particular application.

7.3.3 Future Work

Increasing the Performance of KUCSBear

The 3.48% performance cost of the system call mechanism cannot explain the ∼10%

overall measured performance cost. It cannot even explain a third of that cost, since

only a small percentage of the running time of the test should be spent actually

exercising that specific mechanism. Further study could identify the source of this

168

performance cost and work to reduce it.

One possible contributing factor for the performance cost of KUCSBear is the

symmetrical nature of hardware resources in the SMP chip being used. In particular,

caches and shared memory or peripheral busses are optimized for symmetric use by

all cores. Using the cores asymmetrically may result in suboptimal performance of

these hardware facilities. Unfortunately, this could only be resolved at the hardware

development stage.

KUCSBear also uses virtualization on all cores. This may pose an additional per-

formance cost compared to Bear which was tested without any virtualization enabled.

However, this cost is likely to be low, as suggested in Section 4.4 where ExOShim was

measured to impose just a 0.86% performance cost.

Hardware is not the only place where future KUCS implementations could find

better performance. There are many places in KUCSBear where future efforts could

replace synchronous tasks with asynchronous ones. For example, the scheduling mech-

anism in KUCSBear is implemented synchronously. When a process receives a timer

interrupt, it must ask the kernel for the next process to run and wait for the answer.

The kernel handles tasks in the order that they are received, so the process may

have to wait some time for the kernel to clear its queue and handle the scheduling

request. This is inconsistent with the behavior of a conventional operating system

such as Bear, in which all cores requesting work fight for the lock and the order in

which they begin trying for the lock is unrelated to the order in which they acquire

it. Still, a more advanced KUCS operating system implementation could have the

kernel pre-load scheduling events while the process is running, greatly reducing the

cost of a timer interrupt on the user core.

Additionally, it is possible that the kernel’s event handling algorithm itself can

be optimized. For example, handling events in some order other that the order that

they are received could avoid the situation in which a process that wants to schedule

169

must wait for other work to be done.

Other than the small costs of the relatively rare system call and the virtualiza-

tion extensions, there is no clear architectural reason for even the relatively small

performance cost measured in KUCSBear. This separates KUCSBear from the di-

versification techniques described in Chapters 5 and 6 where loss of code locality can

be expected to decrease the effectiveness of hardware memory caching mechanisms.

The lack of a clear architectural explanation for the performance cost invites further

research into the design choices mentioned here as well as others, and how a KUCS

operating system can be implemented most efficiently.

Enhancing the Prototype

Beyond increasing the performance of the KUCSBear prototype, future work could

seek to increase the robustness and feature set of KUCSBear. The first task would be

to eliminate the single reserved legacy core running an instance of the conventional

Bear operating system. This core is running unported drivers and handling any

hardware interrupts.

In order to have all cores running KUCSBear, several drivers would need to be

ported to run as ring 0 processes. This is a straightforward task. In fact, nothing

in user-space needs to change. The only changes required in user-space to make a

user process cooperate with KUCSBear happen in the library layer. As implemented

currently, even the compilation is identical for processes that will be run in ring 0

on a remote core and processes that will be run in ring 3 and cohabit a core with

a kernel instance. Only at load-time is any distinction made between the two types

of process. In the system call library, the process checks a variable to see whether it

should use an IPI mechanism or an INT/IRET mechanism to signal the kernel. This

variable is set at load time. This variable also determines whether the process should

write its message to the ring buffer located in its shared memory interface with the

170

kernel. The variable is set by the kernel at load-time.

Interestingly, because the Indirect IPI and INT/IRET system call mechanisms each

begin by executing a local int instruction on core, future work may be able to develop

a loader that can run a particular user process in ring 3 with a kernel mapped into

its context or in ring 0 with only a KCUIHL without any difference in the user code.

The other challenge for this will be accessing the process’ memory without using the

shared memory interface, but this could be overcome since the kernel can manipulate

memory as needed. This possible extension would bring the “red-pill” vs. “blue-

pill” debate from virtualization [144] into the context of the kernel- and user-space

relationship.

Handling interrupts without a legacy Bear core will also require some modifica-

tion to the KUCSBear kernel. On Bear, like most conventional operating systems,

the kernel is only ever run in response to an interrupt. In other words, the entire

kernel is like one large interrupt handler; including the fact that it does not allow

interrupt delivery while it is running. For this reason, hardware interrupt handlers

make assumptions about the state saving and restoration needed to go from the ring

3 context where the interrupt will be delivered to the kernel context where it will be

serviced, and back. With the KUCSBear kernel, the kernel core is always executing

code in the kernel and the interrupt handler only logs an event that the main kernel

code can then service. In order to handle legacy hardware interrupts in KUCSBear,

the interrupt handling routines for these interrupts would need to be rewritten to

change and restore context from a ring 0 kernel context rather than from a ring 3

user context. In addition, the interrupt would have to be logged and later retrieved

and serviced like any other kernel event. This is a departure from the legacy paradigm

in which the interrupt is serviced in the handler. Unless KUCSBear is made com-

pletely reentrant, servicing interrupts in an interrupt handler (rather than simply

logging them) on the kernel core could corrupt kernel state.

171

Finally, the prototype could use more debugging in order to increase its stability.

Although it can run many thousands of processes, there are potential timing and

memory integrity issues in the prototype that keep it from being stable enough to

run indefinitely.

Fine-Grained Locking

The original Bear operating system uses a single kernel lock. This is a mechanism

that ensures that only one core can enter the kernel at any given time [107] and is in

contrast with the most modern operating systems which implement many separately-

locked paths so that multiple cores can do kernel work simultaneously if the tasks do

not compete for resources [155].

KUCSBear uses the same kernel lock to restrict access to the kernel resources

between the KUCS kernel core and the reserved legacy Bear OS core. However,

KUCS operating systems will struggle to take advantage of fine-grained locking. Using

a single core for the kernel implies that only one thread of execution can be running

kernel code at any particular time. This could impose significant performance costs

on certain types of workloads. On a machine with infinite cores, this would not be an

issue because one core could be reserved for each independently locked path through

the kernel. Future work could examine the performance implications of trading user

cores for independent kernel cores. This tradeoff may increase or decrease performance

depending on the type of workload being handled and the number of cores available.

Porting Other Kernels to KUCS

The security benefits of KUCS make it an attractive proposition for real-world operat-

ing systems, especially for hardware that is susceptible to the Meltdown vulnerability

but expensive to replace. In addition, the performance cost is moderate as measured

at less than 10%. When contrasted to the potentially high costs of current Meltdown

172

patches, this 10% seems acceptable. As such, future efforts may be interested in im-

plementing a KUCS design on larger, more stable, and more fully featured operating

systems.

The implementation of a major redesign of an operating system, as would be

required to port a major kernel to a KUCS operating system design, is a complex

endeavor. It is vital to have a plan to approach this huge software engineering task so

that it can be developed in stages that can be parallelized where possible. Fortunately,

the experience of developing KUCSBear has led to a preliminary decomposition of

steps for implementing a KUCS design on an arbitrary operating system.

1. Initial IPI testing The first step consists of an exercise that will guide the

system developers in some of the essential system capabilities that will be used

in the later steps. In particular, the task is to have two cores interact in the

following way:

“A non-locked kernel-level process on core X sends an IPI to core Y , then

monitors a particular memory location, looking for a new value. Core Y receives

the IPI and displays that it has received it. In addition, it writes a confirmation

to the shared memory location, freeing X to continue with its own execution.”

This process can be destructive. It does not need to preserve system state. The

purpose of the task is to get the system developers familiar with the multicore

support on the target operating system, sending and receiving IPIs, and using

shared memory interfaces to communicate between cores. These tasks are the

foundation of any KUCS implementation.

2. Load a page table with the process but no kernel This step involves

generating an address space for a remote process. Such an address space will

contain virtual memory mappings to the process code and data, plus all of the

other mappings it may need, but not to the kernel.

173

Generating this address space may require more or less work depending on the

robustness and flexibility of the existing virtual memory system in the target

operating system. Unfortunately, many operating systems are written so that

their virtual memory operations act on the currently loaded set of page tables.

This makes sense for most tasks because the operating system is loaded into

every context, so any context can be modified by the operating system through

the mappings that it contains. In this case, the operating system needs to

operate on a context where it is not local (unless the implementation goes

through the normal loading process and then later unmaps the kernel).

With a sufficiently flexible virtual memory layer, the loader and linker must be

modified to generate remote processes in addition to standard ones. The best

method for accomplishing this will be dependent on the target system.

3. Pin tasks to cores A foundational idea in the KUCS operating system design

is using the processor’s cores asymmetrically. Assigning specific tasks to specific

cores is functionality that may or may not be present in the target operating

system. As such, this step requires using (or modifying) the target operat-

ing system’s multicore management system in order to assign specific tasks to

specific processors. This task may be trivial or very difficult, depending on the

capability, flexibility, and robustness of the existing multicore management code

in the host operating system.

4. Implement a system call In order to communicate between the kernel and

remote processes for system calls, the implementation needs to make accom-

modations for passing data between the two. This is likely to take the form

of a shared memory interface in which the two entities mutually agree upon a

location at which the relevant arguments will be stored and retrieved.

Interrupts sent from the remote core to the kernel core or kernel polling of the

174

relevant memory can be used to notify the kernel of a new system call from the

remote process. If the former is used, it will require that the interrupt han-

dling mechanisms described in the first step are implemented completely. This

includes an interrupt handling mechanism (KCUIHL) loaded into the remote

process.

As long as the loader is sufficiently flexible, compiling the interrupt handling

library should be done in a separate file and then loaded into the address space

of the remote process.

5. Run a “Hello World” user program Beginning to scale up from the first

system call, the developers should port other system calls until a simple process

can be loaded and run on the remote core. For a particular system call, changes

may need to be made in the user-space libraries or kernel system call handling

code itself in order to accommodate the KUCS design.

Note that even for a simple hello world program, there may be dozens of system

calls necessary.

6. Schedule between user processes With a process that can be observed

and interacted with, the next task would be to schedule between two of these

processes. Any method of accomplishing scheduling will require robust interrupt

handling on the user and kernel cores. Additionally, scheduling will begin to

reveal issues with the design or implementation that were not an issue with

the special case of running a single process. Section 7.2.5 examined multiple

possible methods for scheduling in the context of KUCSBear.

7. Port all system calls With all of the core components implemented, the

system developers have the tools to port all system calls and user processes to

be compatible with the KUCS kernel. Additionally, they can allow arbitrary

175

scheduling with n processes. This will be a step in which major stability issues

are likely to surface, and may take a large debugging effort.

8. Add virtualization Finally, the virtualization layer can be implemented. The

virtualization layer is largely passive and so it can be added after the regular

KUCS system is stable. However, developers may find certain virtualization

features can assist in debugging. For this reason, they may wish to complete

this step early in the process.

Note that throughout the implementation plan, steps are taken to run KUCS type

processes alongside conventional processes that contain operating system mappings.

This is extra work in the beginning of the process, but facilitates debugging at the

end of the process. It is unlikely that even the most skilled system programmers

could manage to implement an entire KUCS operating system without errors on the

first attempt, and it is very helpful to have processes running under the standard

paradigm that the developers can “trust.”

Possible Hardware Extensions

New processor designs could go a long way towards supporting KUCS operating

systems. Possible extensions include:

• Optimizing cache, bus, and interrupt architectures for asymmetrical multipro-

cessing architectures.

• Providing a hardware mechanism to accept interrupts in a FIFO manner rather

than through a vector.

• Implement hardware interrupt source recording.

• Implement non-readable memory in the regular page tables (in order to facilitate

hiding the KCUIHL).

176

Of course, a hardware designer that is looking especially for ways to support

a KUCS operating system could implement any number of creative solutions. For

example, a master-slave paradigm that could apply to kernel and user cores may

enable enhanced security features without requiring software mechanisms such as

hypervisor intervention.

7.4 KUCS: Conclusion

Over the past several decades, few have attempted to change the traditional operating

system design methods. However, the symmetric multiprocessing paradigm that is

used in conventional kernel design may be imposing unnecessary limits on the security

and performance of operating systems. KUCS is an alternative kernel design paradigm

that uses asymmetric multiprocessing to offer possibilities for increased security and

performance.

KUCS abandons the traditional weakly-separated kernel and user virtual address

spaces in favor of a strongly separated kernel- and user-space. In particular, it ex-

ecutes the kernel on one core and applications on other cores, with the two pieces

of software never sharing hardware. System calls and other communication between

the kernel and the application is accomplished with IPIs instead of the traditional

INT/IRET mechanism. Finally, the processor’s virtualization layer is be utilized on a

per-core basis to protect the hardware from malicious processes.

KUCSBear is a prototype operating system that ported the Bear microkernel to

use the KUCS kernel design paradigm. It offers several contributions to the current

state of the art. Divorcing virtual address spaces enables complete sandboxing of

each user application. Redefining “user-space” as a location rather than a privilege

level allows for device drivers with the security of user-space encapsulation and the

performance of kernel modules. Simultaneous operation of the kernel and applications

177

allows the kernel to accomplish its own work in addition to servicing the remaining

user cores. The asymmetric multiprocessing paradigm allows for uniquely fine-grained

security policy enforcement enabled by per-core virtualization.

There still much work to be done in evaluating the full potential of the KUCS

kernel design. While KUCSBear provides a proof-of-concept, future efforts can im-

plement watchdog security monitors in the kernel, make ring 3 hardware protection

mechanisms available for use within applications, and offer more support for device

drivers.

KUCSBear was measured to have a performance overhead of less than 10% com-

pared to the conventional Bear operating system. Although a small percentage of

this cost is due to a the system call mechanism, there is no clear architectural reason

for the bulk of this cost. Future work would be required to understand the role of

various implementation design decisions in this performance cost. In addition, future

KUCS operating systems could utilize novel techniques such as asynchronous system

calls and direct mapping of interrupts to device drivers in order to further increase

performance.

The implications of the prototype and its performance measurements are espe-

cially interesting in the face of the recent Meltdown vulnerability [109]. The patches

for Meltdown increase the overhead of the system call mechanism and only new hard-

ware can mitigate the risk without a meaningful performance cost. Although a full

study of the performance cost of Meltdown patches has not yet been conducted, mi-

crobenchmarks in this work suggest that it could slow certain system events by as

much as 25%. This is consistent with the significant performance costs seen in the

4G/4G Linux Split patch [118] from which the Meltdown patch mechanism was in-

spired. The prospect of an alternative operating system design that is not susceptible

to Meltdown with less than 10% performance cost and room to get faster is very

interesting in the face of millions of processors that need to be replaced or suffer a

178

potentially significant performance cost.

There are many outstanding questions regarding the KUCS kernel design and its

application to modern hardware. Whether or not these questions are researched in the

future, KUCSBear offers an interesting case study in the sparse world of alternative

operating system designs.

179

Chapter 8

Conclusion

The pervasive and diverse problem of privilege escalation motivated the research pre-

sented in this thesis. An analysis of the existing solutions to the various types of

privilege escalation mechanisms (Chapter 3) revealed a host of disparate techniques,

but none has gained traction in commodity software due to performance costs, com-

patibility or usability issues, and/or specialty hardware requirements.

This thesis contributes four research efforts that seek to mitigate privilege es-

calation threats. The KPLT (Chapter 5) and Überdiversity (Chapter 6) use non-

determinism to mitigate the risk of return-oriented programming attacks, especially

at the kernel level. ExOShim (Chapter 4) uses the virtualization features of modern

hardware to provide an operating system protection mechanism that can remove the

threat of kernel-level memory disclosures. Finally, the KUCS operating system de-

sign (Chapter 7) describes a new paradigm for mapping the software components of

a computing system onto existing hardware in order to mitigate the threat of kernel

code injection and ret2usr attacks, and defeat the Meltdown vulnerability.

The KPLT uses virtual memory indirection to map common kernel functionality at

unique and randomized addresses on a per-process basis. This models the behavior of

dynamically-linked libraries for the kernel itself. An operating system using the KPLT

180

inhibits the development of kernel-level return-oriented programming by shuffling the

location of kernel gadgets in each process. Due to the inability to utilize global

paging between processes (since the kernel addresses are no longer global), a moderate

performance cost of 16% was measured.

Überdiversity is a study of load-time randomization. It presents a prototype

load-time diversification scheme that uses more of the address space than previous

implementations, virtualizes the hypervisor code as well as the user and kernel code,

and interleaves user and kernel sections in order to maximize the randomness of the

system. Additionally, the study discusses the algorithms used to generate random

address spaces. In particular, it shows that many algorithms produce some address

spaces with higher probability than others, and presents an algorithm that produces

all address spaces with equal probability. Finally, it examines the quantification of

randomization schemes and attempts to clarify the term “entropy” while contributing

a novel quantification metric: “program variants.” A modest performance cost of

only 6% was measured for the Überdiversity prototype implementation, indicating

that more thorough randomization methods are not unreasonably slow, and could be

used in more dramatic ways in commodity software.

ExOShim is a technique in which the operating system deploys the virtualization

features of its hardware in order to mark its code pages as execute-only, thus defeating

the risk of kernel-code memory disclosures. Although ExOShim was designed to be

deployed by the kernel, it was carefully configured so that it provides protection that

cannot be modified, disabled, or hijacked by an attacker. The measured performance

cost was minuscule, at just 0.86%. Additionally, any operating system that does not

initialize the virtualization capabilities of its hardware is susceptible to virtualization-

based rootkits. With ExOShim, the virtualization features are preemptively deployed

to protect the kernel rather than laying in wait to help an attacker compromise it.

181

KUCS is an operating system design that isolates the kernel from the user processes

by running them on separate CPU cores. The process is loaded in an address space

without the kernel, so the kernel does not reside anywhere in the process’ context,

rather than simply hiding behind a privilege bit. Processes communicate with the

kernel via inter-processor interrupts, and the kernel returns data to the processes

via shared memory interfaces. Although there was a measured performance cost of

∼10% with the prototype implementation, future efforts could take steps to reduce

that cost. In fact, the design offers many opportunities to increase performance over

the status quo operating system design.

In spite of its organization, the thesis contributes more than just four unique ef-

forts. They compliment each other to form a complete picture of a possible future

hardened kernel design. Chapter 7 describes how ExOShim-style virtualization tech-

niques can augment the protections offered by the KUCS operating system design. In

addition, the randomization techniques described in Chapter 5 and Chapter 6 can also

strengthen a KUCS-designed kernel. Überdiversity could be deployed on each core to

randomize each user process and even the kernel-controlled interrupt handling code

and other memory regions on the user cores. Finally, a KPLT could allow for several

diverse kernel images to occupy the kernel core at different times. A KUCS kernel

design utilizing these techniques could offer a performant and secure alternative to

conventional operating systems.

In summary, this thesis shows not only how different research efforts can address

specific privilege escalation threats, but also how they can compliment each other to

offer a hardened kernel design for the future.

182

Bibliography

[1] Building a Secure System using TrustZone Technology. Technical report, ARM.

[2] CVE Details. https://www.cvedetails.com/vulnerabilities-by-types.

php. Accessed: 1/29/2018.

[3] Address Space Layout Randomization. Technical report, PaX Team, 2001.

[4] CWE-639: Authorization Bypass Through User-Controlled Key, September

2008.

[5] Microgadgets: Size Does Matter in Turing-Complete Return-Oriented Program-

ming. In Presented as part of the 6th USENIX Workshop on Offensive Tech-

nologies, Berkeley, CA, 2012. USENIX.

[6] AIM Independent Resource Benchmark, February 2013.

[7] CVE-2013-2094, May 2013.

[8] OS X Mavericks Core Technologies Overview. Technical report, Apple, October

2013.

[9] CVE-2016-0728, January 2016.

[10] Mike Accetta, Robert Baron, William Bolosky, David Golub, Richard Rashid,

Avadis Tevanian, and Michael Young. Mach: A New Kernel Foundation for

UNIX Development. pages 93–112, 1986.

183

[11] AMD. AMD64 Architecture Programmer’s Manual Volume 2: System Program-

ming, October 2013.

[12] Gene M. Amdahl. Validity of the Single Processor Approach to Achieving Large

Scale Computing Capabilities. In Proceedings of the April 18-20, 1967, Spring

Joint Computer Conference, AFIPS ’67 (Spring), pages 483–485, New York,

NY, USA, 1967. ACM.

[13] Algirdas Avizienis and Liming Chen. On the Implementation of N-version Pro-

gramming for Software Fault Tolerance during Execution. In Proc. the First

IEEE-CS International Computer Software and Applications Conference (COM

PSAC 77), Chicago, 1977.

[14] Ahmed M. Azab, Peng Ning, Jitesh Shah, Quan Chen, Rohan Bhutkar, Gu-

ruprasad Ganesh, Jia Ma, and Wenbo Shen. Hypervision Across Worlds: Real-

time Kernel Protection from the ARM TrustZone Secure World. In Proceedings

of the 2014 ACM SIGSAC Conference on Computer and Communications Se-

curity, CCS ’14, pages 90–102, New York, NY, USA, 2014. ACM.

[15] Michael Backes, Thorsten Holz, Benjamin Kollenda, Philipp Koppe, Stefan

Nürnberger, and Jannik Pewny. You Can Run but You Can’T Read: Pre-

venting Disclosure Exploits in Executable Code. In Proceedings of the 2014

ACM SIGSAC Conference on Computer and Communications Security, CCS

’14, pages 1342–1353, New York, NY, USA, 2014. ACM.

[16] Saisanthosh Balakrishnan, Ravi Rajwar, Mike Upton, and Konrad Lai. The

Impact of Performance Asymmetry in Emerging Multicore Architectures.

SIGARCH Comput. Archit. News, 33(2):506–517, May 2005.

[17] Paul Barham, Boris Dragovic, Keir Fraser, Steven Hand, Tim Harris, Alex

Ho, Rolf Neugebauer, Ian Pratt, and Andrew Warfield. Xen and the Art of

184

Virtualization. In Proceedings of the Nineteenth ACM Symposium on Operating

Systems Principles, SOSP ’03, pages 164–177, New York, NY, USA, 2003. ACM.

[18] Christoph Baumann, Bernhard Beckert, Holger Blasum, and Thorsten Bormer.

Formal Verification of a Microkernel Used in Dependable Software Systems. In

Bettina Buth, Gerd Rabe, and Till Seyfarth, editors, Computer Safety, Relia-

bility, and Security, volume 5775 of Lecture Notes in Computer Science, pages

187–200. Springer Berlin Heidelberg, 2009.

[19] Sandeep Bhatkar, R. Sekar, and Daniel C. DuVarney. Efficient techniques for

comprehensive protection from memory error exploits. In Proceedings of the

14th Conference on USENIX Security Symposium - Volume 14, SSYM’05, pages

17–17, Berkeley, CA, USA, 2005. USENIX Association.

[20] Kenneth J Biba. Integrity considerations for secure computer systems. Technical

report, DTIC Document, 1977.

[21] Andrea Bittau, Adam Belay, Ali Mashtizadeh, David Mazières, and Dan Boneh.

Hacking blind. In Proceedings of the 2014 IEEE Symposium on Security and

Privacy, SP ’14, pages 227–242, Washington, DC, USA, 2014. IEEE Computer

Society.

[22] Silas Boyd-Wickizer, Haibo Chen, Rong Chen, Yandong Mao, Frans Kaashoek,

Robert Morris, Aleksey Pesterev, Lex Stein, Ming Wu, Yuehua Dai, Yang

Zhang, and Zheng Zhang. Corey: An Operating System for Many Cores. In

Proceedings of the 8th USENIX Conference on Operating Systems Design and

Implementation, OSDI’08, pages 43–57, Berkeley, CA, USA, 2008. USENIX

Association.

[23] Alfred Bratterud, Alf-Andre Walla, Paal E Engelstad, Kyrre Begnum, et al.

IncludeOS: A minimal, resource efficient unikernel for cloud services. In 2015

185

IEEE 7th International Conference on Cloud Computing Technology and Sci-

ence (CloudCom), pages 250–257. IEEE, 2015.

[24] Sergey Bratus, Nihal DCunha, Evan Sparks, and Sean W Smith. Toctou, traps,

and trusted computing. In International Conference on Trusted Computing,

pages 14–32. Springer, 2008.

[25] Sergey Bratus, Michael E. Locasto, Ashwin Ramaswamy, and Sean W. Smith.

VM-based Security Overkill: A Lament for Applied Systems Security Research.

In Proceedings of the 2010 Workshop on New Security Paradigms, NSPW’10,

pages 51–60, New York, NY, USA, 2010. ACM.

[26] Scott Brookes, Robert Denz, Martin Osterloh, and Stephen Taylor. ExOShim:

Preventing Memory Disclosure using Execute-Only Kernel Code. In Proceed-

ings of the 11th International Conference on Cyber Warfare and Security, IC-

CWS’16, pages 56–66, April 2016.

[27] Scott Brookes, Robert Denz, Martin Osterloh, and Stephen Taylor. ExOShim:

Preventing Memory Disclosure using Execute-Only Kernel Code. International

Journal of Information and Computer Security (IJICS), 2018, In Press.

[28] Scott Brookes, Martin Osterloh, Robert Denz, and Stephen Taylor. The KPLT:

The Kernel as a shared object. In Military Communications Conference, MIL-

COM 2015 - 2015 IEEE, pages 954–959, Oct 2015.

[29] Scott Brookes, Martin Osterloh, Robert Denz, and Stephen Taylor.

Überdiversity: Exploring the Limit of Load-Time Software Diversification.

Technical report, Thayer School of Engineering at Dartmouth College, April

2018.

[30] Scott Brookes and Stephen Taylor. Containing a Confused Deputy on x86: A

Survey of Privilege Escalation Mitigation Techniques. IJACSA, April 2016.

186

[31] Scott Brookes and Stephen Taylor. Rethinking operating system design: Asym-

metric multiprocessing for security and performance. In Proceedings of the 2016

New Security Paradigms Workshop, NSPW ’16, pages 68–79, New York, NY,

USA, 2016. ACM.

[32] Zach Brown. Asynchronous System Calls. In Proceedings of the Ottawa Linux

Symposium (OLS), pages 81–85, 2007.

[33] Erik Buchanan, Ryan Roemer, Stefan Savage, and Hovav Shacham. Return-

Oriented Programming: Exploitation without Code Injection, 2008.

[34] c0ntex. Bypassing non-executable-stack during exploitation using return-to-

libc. http://www.open-security.org/texts/4.

[35] Xi Chen, Asia Slowinska, Dennis Andriesse, Herbert Bos, and Cristiano Giuf-

frida. Stackarmor: Comprehensive protection from stack-based memory error

vulnerabilities for binaries. In NDSS Symposium 2015, San Diego, California,

2015.

[36] Monica Chew and Dawn Song. Mitigating buffer overflows by operating system

randomization. Technical report, 2002.

[37] Andy Chou, Junfeng Yang, Benjamin Chelf, Seth Hallem, and Dawson En-

gler. An Empirical Study of Operating Systems Errors. In Proceedings of the

Eighteenth ACM Symposium on Operating Systems Principles, SOSP ’01, pages

73–88, New York, NY, USA, 2001. ACM.

[38] Frederick B Cohen. Operating system protection through program evolution.

Computers & Security, 12(6):565–584, 1993.

[39] TIS Committee. Tool Interface Stadard (TIS) Executable and Linking Format

(ELF) Specification, May 1995.

187

[40] F. J. Corbató and V. A. Vyssotsky. Introduction and Overview of the Multics

System. In Proceedings of the November 30–December 1, 1965, Fall Joint Com-

puter Conference, Part I, AFIPS ’65 (Fall, part I), pages 185–196, New York,

NY, USA, 1965. ACM.

[41] Crispin Cowan. Non-Executable Stack, 1997.

[42] Crispin Cowan, Calton Pu, Dave Maier, Heather Hintony, Jonathan Walpole,

Peat Bakke, Steve Beattie, Aaron Grier, Perry Wagle, and Qian Zhang. Stack-

Guard: Automatic Adaptive Detection and Prevention of Buffer-overflow At-

tacks. In Proceedings of the 7th Conference on USENIX Security Symposium

- Volume 7, SSYM’98, pages 5–5, Berkeley, CA, USA, 1998. USENIX Associa-

tion.

[43] S. Crane, Ch. Liebchen, A. Homescu, L. Davi, P. Larsen, AR Sadeghi, S. Brun-

thaler, and M. Franz. Readactor: Practical Code Randomization Resilient to

Memory Disclosure. In Proceedings of the 2015 IEEE Symposium on Security

and Privacy, 2015.

[44] Paul Crawford. Linux Watchdog Daemon - Overview. http://www.sat.

dundee.ac.uk/psc/watchdog/watchdog-background.html, January 2016.

[45] J. Criswell, N. Dautenhahn, and V. Adve. KCoFI: Complete Control-Flow

Integrity for Commodity Operating System Kernels. In Proceedings of the 2014

IEEE Symposium on Security and Privacy, SP’14, pages 292–307, May 2014.

[46] John Criswell, Andrew Lenharth, Dinakar Dhurjati, and Vikram Adve. Secure

Virtual Architecture: A Safe Execution Environment for Commodity Operating

Systems. In Proceedings of Twenty-first ACM SIGOPS Symposium on Operat-

ing Systems Principles, SOSP ’07, pages 351–366, New York, NY, USA, 2007.

ACM.

188

[47] Robert C Daley and Jack B Dennis. Virtual memory, processes, and sharing in

Multics. Communications of the ACM, 11(5):306–312, 1968.

[48] Lucas Davi, Alexandra Dmitrienko, Ahmad-Reza Sadeghi, and Marcel

Winandy. Privilege Escalation Attacks on Android. In Mike Burmester, Gene

Tsudik, Spyros Magliveras, and Ivana Ili, editors, Information Security, volume

6531 of Lecture Notes in Computer Science, pages 346–360. Springer Berlin

Heidelberg, 2011.

[49] Lucas Davi, Christopher Liebchen, Ahmad-Reza Sadeghi, Kevin Z Snow, and

Fabian Monrose. Isomeron: Code randomization resilient to (just-in-time)

return-oriented programming. Proc. 22nd Network and Distributed Systems

Security Sym.(NDSS), 2015.

[50] Lucas Vincenzo Davi, Alexandra Dmitrienko, Stefan Nürnberger, and Ahmad-

Reza Sadeghi. Gadge me if you can: Secure and efficient ad-hoc instruction-

level randomization for x86 and arm. In Proceedings of the 8th ACM SIGSAC

Symposium on Information, Computer and Communications Security, ASIA

CCS ’13, pages 299–310, New York, NY, USA, 2013. ACM.

[51] Robert Denz. Securing the Cloud with Utility Virtual Machines. PhD thesis,

Thayer School of Engineering at Dartmouth College, July 2016.

[52] Robert Denz, Scott Brookes, Martin Osterloh, Stephen Kuhn, and Stephen Tay-

lor. Symmetric multiprocessing from boot to virtualization. Software: Practice

and Experience, 48(3):681–718.

[53] Richard Durstenfeld. Algorithm 235: Random permutation. Commun. ACM,

7(7):420–, July 1964.

[54] Jake Edge. Kernel address space randomization, October 2013.

189

[55] Shawn Embleton, Sherri Sparks, and Cliff Zou. Smm rootkits: A new breed

of os independent malware. In Proceedings of the 4th International Conference

on Security and Privacy in Communication Netowrks, SecureComm ’08, pages

11:1–11:12, New York, NY, USA, 2008. ACM.

[56] D. R. Engler, M. F. Kaashoek, and J. O’Toole, Jr. Exokernel: An Operating

System Architecture for Application-level Resource Management. In Proceed-

ings of the Fifteenth ACM Symposium on Operating Systems Principles, SOSP

’95, pages 251–266, New York, NY, USA, 1995. ACM.

[57] Philip Enslow, Jr. Multiprocessor Organization – a Survey. ACM Comput.

Surv., 9(1):103–129, March 1977.

[58] Úlfar Erlingsson and Fred B. Schneider. SASI Enforcement of Security Poli-

cies: A Retrospective. In Proceedings of the 1999 Workshop on New Security

Paradigms, NSPW ’99, pages 87–95, New York, NY, USA, 2000. ACM.

[59] Isaac Evans, Sam Fingeret, Julián González, Ulziibayar Otgonbaatar, Tiffany

Tang, Howard Shrobe, Stelios Sidiroglou-Douskos, Martin Rinard, and Hamed

Okhravi. Missing the point (er): On the effectiveness of code pointer integrity1.

2015.

[60] William Feller. An Introduction to Probability Theory and Its Applications,

volume 1. Wiley, January 1968.

[61] Peter Ferrie. Attacks on more virtual machine emulators. Symantec Technology

Exchange, 2007.

[62] Stephen Fischer. Supervisor Mode Execution Protection. NSA Trusted Com-

puting Conference and Exposition, 2011.

190

[63] Ronald Aylmer Fisher, Frank Yates, et al. Statistical tables for biological,

agricultural and medical research. Statistical tables for biological, agricultural

and medical research., (Ed. 3.), 1949.

[64] S. Forrest, A. Somayaji, and D. Ackley. Building Diverse Computer Systems. In

Proceedings of the 6th Workshop on Hot Topics in Operating Systems (HotOS-

VI), HOTOS ’97, pages 67–, Washington, DC, USA, 1997. IEEE Computer

Society.

[65] Jason Franklin, Arvind Seshadri, Ning Qu, Sagar Chaki, and Anupam Datta.

Attacking, repairing, and verifying SecVisor: A retrospective on the security of

a hypervisor. Technical report, Carnegie Mellon University, 2008.

[66] Archana Ganapathi, Viji Ganapathi, and David Patterson. Windows XP Kernel

Crash Analysis. In Proceedings of the 20th Conference on Large Installation Sys-

tem Administration, LISA ’06, pages 12–12, Berkeley, CA, USA, 2006. USENIX

Association.

[67] Xinyang Ge, Hayawardh Vijayakumar, and Trent Jaeger. Sprobes: En-

forcing kernel code integrity on the trustzone architecture. arXiv preprint

arXiv:1410.7747, 2014.

[68] Jason Gionta, William Enck, and Peng Ning. HideM: Protecting the Contents

of Userspace Memory in the Face of Disclosure Vulnerabilities. In Proceedings

of the 5th ACM Conference on Data and Application Security and Privacy,

CODASPY ’15, pages 325–336, New York, NY, USA, 2015. ACM.

[69] Cristiano Giuffrida, Anton Kuijsten, and Andrew S. Tanenbaum. Enhanced

Operating System Security Through Efficient and Fine-grained Address Space

Randomization. In Presented as part of the 21st USENIX Security Symposium

(USENIX Security 12), pages 475–490, Bellevue, WA, 2012. USENIX.

191

[70] Robert P. Goldberg. Survey of Virtual Machine Research. Computer, 7(9):34–

45, September 1974.

[71] Norm Hardy. The confused deputy: (or why capabilities might have been in-

vented). SIGOPS Oper. Syst. Rev., 22(4):36–38, October 1988.

[72] Jorrit N Herder. Towards a true microkernel operating system. PhD thesis,

Vrije Universiteit Amsterdam, February 2005.

[73] Jorrit N. Herder, Herbert Bos, Ben Gras, Philip Homburg, and Andrew S.

Tanenbaum. MINIX 3: A Highly Reliable, Self-repairing Operating System.

SIGOPS Oper. Syst. Rev., 40(3):80–89, July 2006.

[74] Dan Hildebrand. An Architectural Overview of QNX. In Proceedings of the

Workshop on Micro-kernels and Other Kernel Architectures, pages 113–126,

Berkeley, CA, USA, 1992. USENIX Association.

[75] J. Hiser, A. Nguyen-Tuong, M. Co, M. Hall, and J. W. Davidson. Ilr: Where’d

my gadgets go? In 2012 IEEE Symposium on Security and Privacy, pages

571–585, May 2012.

[76] Andrei Homescu, Steven Neisius, Per Larsen, Stefan Brunthaler, and Michael

Franz. Profile-guided Automated Software Diversity. In Proceedings of the 2013

IEEE/ACM International Symposium on Code Generation and Optimization

(CGO), CGO ’13, pages 1–11, Washington, DC, USA, 2013. IEEE Computer

Society.

[77] R. Hund, C. Willems, and T. Holz. Practical timing side channel attacks against

kernel space aslr. In Security and Privacy (SP), 2013 IEEE Symposium on,

pages 191–205, May 2013.

192

[78] Ralf Hund, Thorsten Holz, and Felix C. Freiling. Return-oriented Rootkits:

Bypassing Kernel Code Integrity Protection Mechanisms. In Proceedings of the

18th Conference on USENIX Security Symposium, SSYM’09, pages 383–398,

Berkeley, CA, USA, 2009. USENIX Association.

[79] Intel. Intel 64 and IA-32 Architectures Software Developer’s Manual, June 2014.

[80] Intel. Intel 64 and IA-32 Architectures Software Developer’s Manual Combined

Volumes: 1, 2A, 2B, 2C, 3A, 3B, and 3C, June 2014.

[81] Todd Jackson, Andrei Homescu, Stephen Crane, Per Larsen, Stefan Brunthaler,

and Michael Franz. Diversifying the software stack using randomized nop in-

sertion. In Sushil Jajodia, Anup K. Ghosh, V.S. Subrahmanian, Vipin Swarup,

Cliff Wang, and X. Sean Wang, editors, Moving Target Defense II, volume 100

of Advances in Information Security, pages 151–173. Springer New York, 2013.

[82] Todd Jackson, Babak Salamat, Andrei Homescu, Karthikeyan Manivannan,

Gregor Wagner, Andreas Gal, Stefan Brunthaler, Christian Wimmer, and

Michael Franz. Compiler-Generated Software Diversity. In Sushil Jajodia,

Anup K. Ghosh, Vipin Swarup, Cliff Wang, and X. Sean Wang, editors, Moving

Target Defense, volume 54 of Advances in Information Security, pages 77–98.

Springer New York, 2011.

[83] Adhiraj Joshi, Swapnil Pimpale, Mandar Naik, Swapnil Rathi, and Kiran

Pawar. Twin-Linux: Running independent Linux Kernels simultaneously on

separate cores of a multicore system. In Proceedings of the Ottawa Linux Sym-

posium, 2010.

[84] Mateusz Jurczyk and Gynvael Coldwind. Identifying and exploiting windows

kernel race conditions via memory access patterns. 2013.

193

[85] S. Kagstrom, L. Lundberg, and H. Grahn. A novel method for adding multi-

processor support to a large and complex uniprocessor kernel. In Parallel and

Distributed Processing Symposium, 2004. Proceedings. 18th International, pages

60–, April 2004.

[86] Morgon Kanter. Enhancing Non-determinism in Operating Systems. PhD the-

sis, Thayer School of Engineering at Dartmouth College, October 2013.

[87] Morgon Kanter and Stephen Taylor. Attack Mitigation through Diversity.

In Military Communications Conference, MILCOM 2013 - 2013 IEEE, pages

1410–1415, Nov 2013.

[88] Morgon Kanter and Stephen Taylor. Attack Mitigation through Diversity.

In Military Communications Conference, MILCOM 2013 - 2013 IEEE, pages

1410–1415, Nov 2013.

[89] Morgon Kanter and Stephen Taylor. Diversity in Cloud Systems Through Run-

time and Compile-time Relocation. In Proceedings of the 13th IEEE Conference

on Technologies for Homeland Security, pages 396–402, 2013.

[90] Kdm. NTIllusion: A portable Win32 userland rootkit, 62.

[91] keegan. Attacking Hardened Linux Systems with Kernel JIT Spraying, June

2011.

[92] Vasileios P. Kemerlis, Michalis Polychronakis, and Angelos D. Keromytis.

Ret2Dir: Rethinking Kernel Isolation. In Proceedings of the 23rd USENIX Con-

ference on Security Symposium, SEC’14, pages 957–972, Berkeley, CA, USA,

2014. USENIX Association.

[93] Vasileios P. Kemerlis, Georgios Portokalidis, and Angelos D. Keromytis.

kGuard: Lightweight Kernel Protection Against Return-to-user Attacks. In

194

Proceedings of the 21st USENIX Conference on Security Symposium, Secu-

rity’12, pages 39–39, Berkeley, CA, USA, 2012. USENIX Association.

[94] D. Keuper. XNU: a security evaluation, December 2012.

[95] Jean-Jamil Khalife. MS15-010/CVE-2015-0057 win32k Local Privilege Escala-

tion, December 2015.

[96] Chongkyung Kil, Jinsuk Jun, Christopher Bookholt, Jun Xu, and Peng Ning.

Address Space Layout Permutation (ASLP): Towards Fine-Grained Random-

ization of Commodity Software. In Proceedings of the 22Nd Annual Computer

Security Applications Conference, ACSAC ’06, pages 339–348, Washington, DC,

USA, 2006. IEEE Computer Society.

[97] Avi Kivity, Dor Laor, Glauber Costa, Pekka Enberg, Nadav Har’El, Don Marti,

and Vlad Zolotarov. OSv-optimizing the operating system for virtual machines.

In 2014 usenix annual technical conference (usenix atc 14), pages 61–72, 2014.

[98] Gerwin Klein, June Andronick, Kevin Elphinstone, Toby Murray, Thomas

Sewell, Rafal Kolanski, and Gernot Heiser. Comprehensive Formal Verification

of an OS Microkernel. ACM Trans. Comput. Syst., 32(1):2:1–2:70, February

2014.

[99] P. Larsen, A. Homescu, S. Brunthaler, and M. Franz. SoK: Automated Software

Diversity. In Security and Privacy (SP), 2014 IEEE Symposium on, pages 276–

291, May 2014.

[100] Per Larsen, Stefan Brunthaler, Lucas Davi, and Ahmad-Reza Sadeghi. Auto-

mated Software Diversity. Morgan & Claypool, 2015.

195

[101] Per Larsen, Andrei Homescu, Stefan Brunthaler, and Michael Franz. Sok: Auto-

mated software diversity. In Security and Privacy (SP), 2014 IEEE Symposium

on, pages 276–291. IEEE, 2014.

[102] Rick Lehtinen, Deborah Russell, and G. T. Gangemi. Computer Security Basics.

O’Reilly Media, Inc., 2006.

[103] Chuck Lever and Chuck Boreham. malloc() Performance in a Multithreaded

Linux Environment. In Proceedings of USENIX Annual Technical Conference,

pages 55–56, 2000.

[104] John R. Levine. Linkers and Loaders. Morgan Kaufmann Publishers Inc., San

Francisco, CA, USA, 1st edition, 1999.

[105] Kyung-Suk Lhee and Steve J Chapin. Buffer overflow and format string overflow

vulnerabilities. Software: Practice and Experience, 33(5):423–460, 2003.

[106] Jinku Li, Zhi Wang, Xuxian Jiang, Michael Grace, and Sina Bahram. Defeating

Return-oriented Rootkits with ”Return-Less” Kernels. In Proceedings of the 5th

European Conference on Computer Systems, EuroSys ’10, pages 195–208, New

York, NY, USA, 2010. ACM.

[107] Rick Lindsley and Dave Hansen. Bkl: One lock to bind them all. In Ottawa

Linux Symposium, page 301, 2002.

[108] Anthony Lineberry. Malicious Code Injection via/dev/mem. 2009.

[109] Moritz Lipp, Michael Schwarz, Daniel Gruss, Thomas Prescher, Werner Haas,

Stefan Mangard, Paul Kocher, Daniel Genkin, Yuval Yarom, and Mike Ham-

burg. Meltdown. ArXiv e-prints, January 2018.

[110] Thomas Lovén. Recursive page directory, June 2012.

196

[111] Anil Madhavapeddy and David J Scott. Unikernels: Rise of the virtual library

operating system. Queue, 11(11):30, 2013.

[112] A. Mahmood and E. J. McCluskey. Concurrent error detection using watchdog

processors-a survey. IEEE Transactions on Computers, 37(2):160–174, Feb

1988.

[113] Hector Marco-Gisbert and Ismael Ripoll-Ripoll. Exploiting Linux and PaX

ASLR’s weaknesses on 32- and 64-bit systems. In Blackhat Asia 16, April 2016.

[114] Joao Martins, Mohamed Ahmed, Costin Raiciu, and Felipe Huici. Enabling

fast, dynamic network processing with clickos. In Proceedings of the second

ACM SIGCOMM workshop on Hot topics in software defined networking, pages

67–72. ACM, 2013.

[115] Richard McDougall and Jim Mauro. Solaris internals: Solaris 10 and OpenSo-

laris kernel architecture. Pearson Education, 2006.

[116] metasploit. Chkroot Local Privilege Escalation, November 2015.

[117] Jeffrey C. Mogul, Jayaram Mudigonda, Nathan Binkert, Parthasarathy Ran-

ganathan, and Vanish Talwar. Using Asymmetric Single-ISA CMPs to Save

Energy on Operating Systems. IEEE Micro, 28(3):26–41, 2008.

[118] Ingo Molnar. 4G/4G split on x86, 64 GB RAM (and more) support, July 2003.

[119] T.Y. Morad, U.C. Weiser, A. Kolodny, M. Valero, and E. Ayguade. Perfor-

mance, power efficiency and scalability of asymmetric cluster chip multiproces-

sors. Computer Architecture Letters, 5(1):14–17, Jan 2006.

[120] Timothy Prickett Morgan. x86 servers dominate the datacenter - for now, June

2015.

197

[121] S. Muir and J. Smith. AsyMOS-an asymmetric multiprocessor operating sys-

tem. In Open Architectures and Network Programming, 1998 IEEE, pages 25–

34, Apr 1998.

[122] Tilo Müller. Aslr smack & laugh reference. In Seminar on Advanced Exploitation

Techniques, 2008.

[123] Colin Nichols. Bear - a Resilient Core for Distributed Systems. Master’s thesis,

Thayer School of Engineering at Dartmouth College, 2013.

[124] Jeff Larson Nicole Perlroth and Scott Shane. N.s.a. able to foil basic safeguards

of privacy on web, September 2013.

[125] Gene Novark and Emery D Berger. Dieharder: securing the heap. In Proceedings

of the 17th ACM conference on Computer and communications security, pages

573–584. ACM, 2010.

[126] Angelos Oikonomopoulos, Elias Athanasopoulos, Herbert Bos, and Cristiano

Giuffrida. Poking holes in information hiding. In 25th USENIX Security Sym-

posium (USENIX Security 16), pages 121–138, Austin, TX, 2016. USENIX

Association.

[127] Aleph One. Smashing the Stack for Fun and Profit. Phrack, 7(49), November

1996.

[128] Christian Otterstad. Brute force bypassing of aslr on linux. Norsk informasjon-

ssikkerhetskonferanse (NISK), 2012, 2012.

[129] Yoann Padioleau, Julia L. Lawall, and Gilles Muller. Understanding Collateral

Evolution in Linux Device Drivers. In Proceedings of the 1st ACM SIGOPS/Eu-

roSys European Conference on Computer Systems 2006, EuroSys ’06, pages

59–71, New York, NY, USA, 2006. ACM.

198

[130] R. K. Pandey and Vinay Tiwari. Article: Reliability Issues in Open Source Soft-

ware. International Journal of Computer Applications, 34(1):34–38, November

2011. Full text available.

[131] RK Pandey and Vinay Tiwari. Reliability issues in open source software. 2011.

[132] Vasilis Pappas, Michalis Polychronakis, and Angelos D. Keromytis. Smashing

the Gadgets: Hindering Return-Oriented Programming Using In-place Code

Randomization. In Proceedings of the 2012 IEEE Symposium on Security and

Privacy, SP ’12, pages 601–615, Washington, DC, USA, 2012. IEEE Computer

Society.

[133] PaX. Homepage of the PaX Team, 2013.

[134] Nick L. Petroni, Jr. and Michael Hicks. Automated Detection of Persistent

Kernel Control-flow Attacks. In Proceedings of the 14th ACM Conference on

Computer and Communications Security, CCS ’07, pages 103–115, New York,

NY, USA, 2007. ACM.

[135] Daniel Potts, Simon Winwood, and Gernot Heiser. Design and Implementation

of the L4 Microkernel for Alpha Multiprocessors, 2002.

[136] William H. Press. Numerical Recipes 3rd Edition: The Art of Scientific Com-

puting. Cambridge University Press, 2007.

[137] Brian Randell. System structure for software fault tolerance. In ACM SIGPLAN

Notices, volume 10, pages 437–449. ACM, 1975.

[138] rebel. issetugid() + rsh + libmalloc osx local root, July 2015.

[139] Ryan Riley, Xuxian Jiang, and Dongyan Xu. Guest-Transparent Prevention

of Kernel Rootkits with VMM-Based Memory Shadowing. In Proceedings of

199

the 11th International Symposium on Recent Advances in Intrusion Detection,

RAID ’08, pages 1–20, Berlin, Heidelberg, 2008. Springer-Verlag.

[140] Ryan Roemer, Erik Buchanan, Hovav Shacham, and Stefan Savage. Return-

Oriented Programming: Systems, Languages, and Applications. ACM Trans.

Inf. Syst. Secur., 15(1):2:1–2:34, March 2012.

[141] Timothy Roscoe, Kevin Elphinstone, and Gernot Heiser. Hype and Virtue. In

Proceedings of the 11th USENIX Workshop on Hot Topics in Operating Systems,

HOTOS’07, pages 4:1–4:6, Berkeley, CA, USA, 2007. USENIX Association.

[142] Dan Rosenberg. Qsee trustzone kernel integer over flow vulnerability. In Black-

hat USA 2014, pages 1–5, 2014.

[143] Dan Rosenburg. SMEP: What is it, and How to Beat it on Linux., June 2011.

[144] Joanna Rutkowska and Alexander Tereshkin. Bluepilling the Xen Hypervisor.

Black Hat USA, 2008.

[145] Jerome H Saltzer and Michael D Schroeder. The protection of information in

computer systems. Proceedings of the IEEE, 63(9):1278–1308, 1975.

[146] Ravi S Sandhu, Edward J Coyne, Hal L Feinstein, and Charles E Youman.

Role-based access control models. Computer, (2):38–47, 1996.

[147] Arvind Seshadri, Mark Luk, Ning Qu, and Adrian Perrig. SecVisor: A Tiny

Hypervisor to Provide Lifetime Kernel Code Integrity for Commodity OSes.

SIGOPS Oper. Syst. Rev., 41(6):335–350, October 2007.

[148] Hovav Shacham. The Geometry of Innocent Flesh on the Bone: Return-into-

libc Without Function Calls (on the x86). In Proceedings of the 14th ACM

Conference on Computer and Communications Security, CCS ’07, pages 552–

561, New York, NY, USA, 2007. ACM.

200

[149] Hovav Shacham, Eu jin Goh, Nagendra Modadugu, Ben Pfaff, and Dan Boneh.

On the Effectiveness of Address-Space Randomization. In In CCS 04: Proceed-

ings of the 11th ACM Conference on Computer and Communications Security,

pages 298–307. ACM Press, 2004.

[150] Eitaro Shioji, Yuhei Kawakoya, Makoto Iwamura, and Takeo Hariu. Code

shredding: Byte-granular randomization of program layout for detecting code-

reuse attacks. In Proceedings of the 28th Annual Computer Security Applications

Conference, ACSAC ’12, pages 309–318, New York, NY, USA, 2012. ACM.

[151] Snow, Kevin Z. and Monrose, Fabian and Davi, Lucas and Dmitrienko, Alexan-

dra and Liebchen, Christopher and Sadeghi, Ahmad-Reza. Just-in-time code

reuse: On the effectiveness of fine-grained address space layout randomization.

In Proceedings of the 2013 IEEE Symposium on Security and Privacy, SP ’13,

pages 574–588, Washington, DC, USA, 2013. IEEE Computer Society.

[152] Matt Solnik. Who needs decrypted kernels anyways?, August 2016.

[153] Brad Spengler. uderef, 2007.

[154] Kevin Spett. Cross-site scripting. Technical report, SPI Labs, 2005.

[155] Arwed Starke. Locking in os kernels for smp systems. Citeseer, 2006.

[156] PaX Team. Address Space Layout Randomization.

https://pax.grsecurity.net/docs/aslr.txt, 2001.

[157] PaX Team. Address Space Layout Randomization. https://pax.grsecurity.

net/docs/aslr.txt, 2001.

[158] Ken Thompson. Reflections on Trusting Trust. Commun. ACM, 27(8):761–763,

August 1984.

201

[159] Jim Turley. Taming the x86 beast, 2004.

[160] Richard Wartell, Vishwath Mohan, Kevin W. Hamlen, and Zhiqiang Lin. Binary

stirring: Self-randomizing instruction addresses of legacy x86 binary code. In

Proceedings of the 2012 ACM Conference on Computer and Communications

Security, CCS ’12, pages 157–168, New York, NY, USA, 2012. ACM.

[161] King’s Way. Lastore-Daemon in Deepin 15 results in privilege escalation, Febru-

ary 2016.

[162] Ollie Whitehouse. An Analysis of Address Space Layout Randomization on

Windows Vista. Technical report, Symantec, 2007.

[163] Haizhi Xu and Steve J. Chapin. Address-space Layout Randomization Using

Code Islands. J. Comput. Secur., 17(3):331–362, August 2009.

202

  • Abstract
  • Contents
  • List of Tables
  • List of Figures
  • List of Code Snippets
  • List of Algorithms
  • Introduction
    • Approach
    • Metrics of Success
    • Publications and Contributions
    • Thesis Organization
  • Background and State of the Art
    • Virtual Memory
      • Case Study: Recursive Paging
      • The Virtual Address Space
    • Message Passing
    • Scheduling
  • Related Work
    • Privilege Escalation Mitigation
      • Techniques based on Virtualization
      • Other Techniques
      • Comparison
      • Privilege Escalation Mitigation: Conclusion
    • Execute-Only Memory
    • Code Diversification
    • Asymmetrical Multiprocessing
    • Operating System Design Paradigm Shifts
      • Microkernels
      • ExoKernel
      • Unikernels
  • ExOShim
    • ExOShim: Background
      • The Kernel and Virtual Memory
      • Virtualization and the EPT
    • ExOShim: Overview
      • Assumptions
      • ExOShim
    • ExOShim: Implementation
      • Providing ExOShim a Context
      • Building the EPT
      • Starting Virtualization
    • ExOShim: Evaluation and Analysis
      • Prototype Complexity
      • Performance
      • Security Implications
    • ExOShim: Conclusion
      • Future Work
      • Final Thoughts
  • Diversification: KPLT
    • KPLT: Overview
      • Using the Virtual Memory Abstraction
      • The Contents of a KPLT
    • KPLT: Implementation
      • KPLT Creation at Load-Time
      • Run-time KPLT Maintenance and Manipulation
    • KPLT: Evaluation and Analysis
      • Security Implications
      • Increase in Diversity
      • Performance Cost
      • Remaining Work and Challenges
    • KPLT: Conclusion
  • Diversification: Überdiversity
    • Diversification Algorithms
      • Run-time Complexity and Termination Analysis
      • Proof of Uniformly Distributed Variants
    • Überdiversity: Implementation
      • ELF & the Diversity Loader
      • The Virtual Memory Abstraction
      • Diversifying the Entire Software Stack
    • Überdiversity: Evaluation and Analysis
      • Quantification of Diversity Achieved
      • Performance Cost
      • Security Implications
    • Überdiversity: Conclusion
  • KUCS
    • KUCS: Overview
      • Separating Kernel and User Cores
      • Fine-Grained Virtualization
      • Increasing Performance
    • KUCS: Implementation
      • KUCSBear
      • Interrupts
      • Virtual Memory
      • Message Passing
      • Scheduling
    • KUCS: Evaluation and Analysis
      • Security
      • Performance
      • Future Work
    • KUCS: Conclusion
  • Conclusion
  • Bibliography