XFI

Required reading: XFI: software guards for system address spaces.

Introduction

Problem: how to use untrusted code (an "extension") in a trusted program?

What are the dangers?

Possible solutions approaches:

Software-based sandboxing

Sandboxer. A compiler or binary-rewriter sandboxes all unsafe instructions in an extension by inserting additional instructions. For example, every indirect store is preceded by a few instructions that compute and check the target of the store at runtime.

Verifier. When the extension is loaded in the trusted program, the verifier checks if the extension is appropriately sandboxed (e.g., are all indirect stores sandboxed? does it call any privileged instructions?). If not, the extension is rejected. If yes, the extension is loaded, and can run. If the extension runs, the instruction that sandbox unsafe instructions check if the unsafe instruction is used in a safe way.

The verifier must be trusted, but the sandboxer doesn't. We can do without the verifier, if the trusted program can establish that the extension has been sandboxed by a trusted sandboxer.

The paper refers to this setup as instance of proof-carrying code.

Software fault isolation

SFI by Wahbe et al. explored out to use sandboxing for fault isolation extensions; that is, use sandboxing to control that stores and jump stay within a specified memory range (i.e., they don't overwrite and jump into addresses in the trusted program unchecked). They implemented SFI for a RISC processor, which simplify things since memory can be written only by store instructions (other instructions modify registers). In addition, they assumed that there were plenty of registers, so that they can dedicate a few for sandboxing code.

The extension is loaded into a specific range (called a segment) within the trusted application's address space. The segment is identified by the upper bits of the addresses in the segment. Separate code and data segments are necessary to prevent an extension overwriting its code.

An unsafe instruction on the MIPS is an instruction that jumps or stores to an address that cannot be statically verified to be within the correct segment. Most control transfer operations, such program-counter relative can be statically verified. Stores to static variables often use an immediate addressing mode and can be statically verified. Indirect jumps and indirect stores are unsafe.

To sandbox those instructions the sandboxer could generate the following code for each unsafe instruction:

  DR0 <- target address
  R0 <- DR0 >> shift-register;  // load in R0 segment id of target
  CMP R0, segment-register;     // compare to segment id to segment's ID
  BNE fault-isolation-error     // if not equal, branch to trusted error code
  STORE using DR0
In this code, DR0, shift-register, and segment register are dedicated: they cannot be used by the extension code. The verifier must check if the extension doesn't use they registers. R0 is a scratch register, but doesn't have to be dedicated. The dedicated registers are necessary, because otherwise extension could load DR0 and jump to the STORE instruction directly, skipping the check.

This implementation costs 4 registers, and 4 additional instructions for each unsafe instruction. One could do better, however:

  DR0 <- target address & and-mask-register // mask segment ID from target
  DR0 <- DR0 | segment register // insert this segment's ID
  STORE using DR0
This code just sets the write segment ID bits. It doesn't catch illegal addresses; it just ensures that illegal addresses are within the segment, harming the extension but no other code. Even if the extension jumps to the second instruction of this sandbox sequence, nothing bad will happen (because DR0 will already contain the correct segment ID).

Optimizations include:

XFI

XFI extends SFI in several ways:

x86 is challenging, because limited registers and variable length of instructions. SFI technique won't work with x86 instruction set. For example if the binary contains:

  25 CD 80 00 00   # AND eax, 0x80CD
and an adversary can arrange to jump to the second byte, then the adversary calls system call on Linux, which has binary the binary representation CD 80. Thus, XFI must control execution flow.

XFI policy goals:

The binary rewriter inserts guards to ensure these properties. The verifier check if the appropriate guards in place. The primary mechanisms used are:

Can XFI protect against the attack discussed in last lecture?

  unsigned int j;
  p=(unsigned char *)s->init_buf->data;
  j= *(p++);
  s->session->session_id_length=j;
  memcpy(s->session->session_id,p,j);
Where will j be located?

How about the following one from the paper Beyond stack smashing: recent advances in exploiting buffer overruns?

void f2b(void * arg, size_t len) {
  char buf[100];
  long val = ..;
  long *ptr = ..;
  extern void (*f)();
  
  memcopy(buff, arg, len);
  *ptr = val;
  f();
  ...
  return;
}
What code can (*f)() call? Code that the attacker inserted? Code in libc?

How about an attack that use ptr in the above code to overwrite a method's address in a class's dispatch table with an address of support function?

How about data-only attacks? For example, attacker overwrites pw_uid in the heap with 0 before the following code executes (when downloading /etc/passwd and then uploading it with a modified entry).

FILE *getdatasock( ... ) {
  seteuid(0);
  setsockeope ( ...);
  ...
  seteuid(pw->pw_uid);
  ...
}

How much does XFI slow down applications? How many more instructions are executed? (see Tables 1-4)