Buffer overflows, baggy bounds

Note: These lecture notes were slightly modified from the ones posted on the 6.858 course website from 2014.

Review of buffer overflow attacks

Last lecture, we looked at the basics of performing a buffer overflow attack. That attack leveraged several observations:

read_req.c:

    void read_req() {
      char buf[128];
      int i;
      gets(buf);
      //. . . do stuff w/buf . . .
    }

What does the compiler generate in terms of memory layout? x86 stack looks like this:

  %esp points to the last (bottom-most) valid thing on
  the stack.

  %ebp points to the caller's %esp value.

                     +------------------+
    entry %ebp ----> | .. prev frame .. |
                     |                  |  |
                     |                  |  | stack grows down
                     +------------------+  |
    entry %esp ----> |  return address  |  v
                     +------------------+
    new %ebp ------> |    saved %ebp    |
                     +------------------+
                     |     buf[127]     |
                     |       ...        |
                     |      buf[0]      |
                     +------------------+
    new %esp ------> |        i         |
                     +------------------+

How does the adversary take advantage of this code?

What can the attackers do once they are executing code?

Fixing buffer overflows

Approach #1: Avoid bugs in C code.

Approach #2: Build tools to help programmers find bugs.

foo.c:

    void foo(int *p) {
        int offset;
        int *z = p + offset;
        if(offset > 7){
            bar(offset);
        }
    }

Approach #3: Use a memory-safe language (JavaScript, C#, Python).

All 3 above approaches are effective and widely used, but buffer overflows are still a problem in practice.

How can we mitigate buffer overflows despite buggy code?

Mitigation approach 1: canaries (e.g., StackGuard, gcc's SSP)

stack layout:

                     |                  |
                     +------------------+
    entry %esp ----> |  return address  |    ^
                     +------------------+    |
    new %ebp ------> |    saved %ebp    |    |
                     +------------------+    |
                     |     CANARY       |    | Overflow goes
                     +------------------+    | this way.
                     |     buf[127]     |    |
                     |       ...        |    |
                     |      buf[0]      |    |
                     +------------------+
                     |                  |

What kinds of vulnerabilities will a stack canary not catch?

data pointer example:

    int *ptr = ...;
    char buf[128];
    gets(buf);  // Buffer is overflowed, and overwrites ptr.
    *ptr = 5;   // Writes to an attacker-controlled address!
              // Canaries can't stop this kind of thing.

malloc example:

    int main(int argc, char **argv) {
        char *p, *q;

        p = malloc(1024);
        q = malloc(1024);
        if(argc >= 2)
            strcpy(p, argv[1]);
        free(q);
        free(p);
        return 0;
    }

malloc memory blocks:

        +----------------+  
        |                |     
        |   App data     |     
        |                |      Allocated memory block
        +----------------+     
        |   size         |     
        +----------------+  


        +----------------+
        |   size         |
        +----------------+
        |  ...empty...   |
        +----------------+  
        |   bkwd ptr     |     
        +----------------+          
        |   fwd ptr      |      Free memory block
        +----------------+     
        |   size         |     
        +----------------+

free() internals:

    p = get_free_block_struct(size);
    bck = p->bk;
    fwd = p->fd;
    fwd->bk = bck;  // Writes memory!
    bck->fd = fwd;  // Writes memory!

Mitigation approach 2: bounds checking

union example:

    union u{
        int i;
        struct s{
            int j;
            int k;
        };
    };

    int *ptr = &(u.s.k); // Does this point to valid data?

What are some approaches for implementing bounds checking?

Bounds checking approach #1: Electric fences

electric fence:

    +---------+
    | Guard   |
    |         |  ^
    +---------+  | Overflows cause a page exception
    |  Heap   |  |
    |  obj    |  |
    +---------+

Bounds checking approach #2: Fat pointer

example:

    Regular 32-bit pointer  
     +-----------------+
     | 4-byte address  |
     +-----------------+

    Fat pointer (96 bits)
     +-----------------+----------------+---------------------+
     | 4-byte obj_base | 4-byte obj_end | 4-byte curr_address |
     +-----------------+----------------+---------------------+

example:

        int *ptr = malloc(sizeof(int) * 2);
        while(1){
            *ptr = 42;       <----------|
            ptr++;                      |
        }                               |
          ______________________________|
         |

    This line checks the current address of the pointer and
    ensures that it's in-bounds. Thus, this line will fail
    during the third iteration of the loop.

Bounds checking approach #3: Shadown data structures

Idea: Use shadow data structures to keep track of bounds information (Jones and Kelly, Baggy bounds).

The baggy bounds approach: 5 tricks

example:

    slot_size = 16

    p = malloc(16);  -->  table[p/slot_size] = 4;
    p = malloc(32);  -->  table[p/slot_size] = 5;
                     \->  table[(p/slot_size) + 1] = 5;

example:

    C code
    ------ 
    p' = p + i;

    Bounds check
    ------------
    size = 1 << table[p >> log_of_slot_size];
    base = p & ~(size - 1);
    (p' >= base) && ((p' - base) < size)

    Optimized bounds check
    ----------------------
    (p^p') >> table[p >> log_of_slot_size] == 0

Example code (assume that slot_size=16):

    char *p = malloc(44);     //Note that the nearest power of 2 (i.e.,
                              //64 bytes) are allocated. So, there are
                              //64/(slot_size) = 4 bounds table entries
                              //that are set to log_2(64) = 6.

    char *q = p + 60;         //This access is ok: It's past p's object
                              //size of 44, but still within the baggy
                              //bounds of 64.
    char *r = q + 16;         //r is now at an offset of 60+16=76 from
                              //p. This means that r is (76-64)=12 bytes
                              //beyond the end of p. This is more than
                              //half a slot away, so baggy bounds will
                              //raise an error.

    char *s = q + 8;          //s is now at an offset of 60+8=68 from p.
                              //So, s is only 4 bytes beyond the baggy
                              //bounds, which is les than half a slot
                              //away. No error is raised, but the OOB
                              //high-order bit is set in s, so that s
                              //cannot be dereferenced.
    char *t = s - 32;         //t is now back inside the bounds, so
                              //the OOB bit is cleared.

For OOB pointers, the high bit is set (if OOB within half a slot). - Typically, OS kernel lives in upper half, protects itself via paging hardware. - Q: Why half a slot for out-of-bounds?

So what's the answer to the homework problem?

    char *p = malloc(256);
    char *q = p + 256;
    char ch = *q;  // Does this raise an exception?
                   // Hint: How big is the baggy bound for p?

Baggy bounds paper errata

Some bugs in the baggy bounds paper: