3. Buffer Overflow Exploits and Defenses

3. Buffer Overflow Exploits and Defenses

Lecture on Exploiting Buffer Overflow

In this lecture, the professor discusses exploiting buffer overflow. The lecture covers baggy bounds and blind return-oriented programming.

Baggy Bounds

  • Baggy bounds is a system that pads an allocation out to the next power of two.
  • An example is given where a pointer called P is defined with an allocation size of 44 and slot size of 16 bytes. This results in allocating 64 bytes for the pointer. Four different bounds table entries are created, each set to the log of the allocation size.
  • Another pointer called Q is defined and set equal to P plus 60. This access is technically out of bounds but allowed by baggy bounds since it stays within that boundary.
  • A pointer called S is declared and set equal to Q plus 8, which is only four bytes beyond that baggy bound. The high order bit on the pointer will be set so that if anyone subsequently tries to dereference this thing, it's going to cause a hard fault at that point.
  • Another pointer T is declared which equals S minus 32. Essentially we brought this pointer T back in bounds.

Blind Return-Oriented Programming

  • Blind return-oriented programming (BROP) paper will be discussed in detail.
  • The professor will step through how some of these gadgets work so students can understand all this sort of high tech chicanery that they're doing in the paper.

Introduction to Baggy Bounds

In this section, the professor introduces the concept of baggy bounds and how it works. He explains that when creating r, we get an instrumented code that's going to be working at all of these pointer operations. The professor also discusses how baggy bounds adds instrumentation and helps us determine if a pointer is out of bounds.

How Baggy Bounds Works

  • As we do these pointer operations, we look for whether they are going out of bounds or not.
  • When a pointer operation involves a pointer that is either in bounds within the baggy bounds and then something over here makes it go out of bounds, that's how we know that something chicanerous has arisen.
  • Baggy Bounds ensures compatibility with preexisting non-instrumented libraries by initializing all entries in the bound table to 31. This allows us to assume that each pointer from [INAUDIBLE] the code is going to have the largest bound possible.

Examples of Baggy Bounds

  • The professor gives an example where a character pointer malloc had 256 bytes to it. Then he declares another character pointer q equal to that pointer plus 256 and tries to dereference this pointer.
  • Another example given by the professor shows how this line is actually fine but will cause the high bit to be set in q. When you come down here and reference it, everything blows up.

Static Code Analysis

In this section, the professor talks about static code analysis and its role in determining safe sets of pointers without having to use bitwise arithmetic.

Static Code Analysis

  • If you can use static code analysis to figure out the particular set of pointer operations that is safe, you don't have to instrument every pointer operation.
  • The professor defers further discussion of some of the static analysis [INAUDIBLE], but suffice it to say that you don't always have to have all this bitwise arithmetic that you have in some of the cases that we've examined before.

Compatibility with Non-Instrumented Libraries

In this section, the professor explains how baggy bounds ensures compatibility with non-instrumented libraries.

Ensuring Compatibility

  • Baggy Bounds initializes all entries in the bound table to 31. This allows us to assume that each pointer from [INAUDIBLE] the code is going to have the largest bound possible.
  • If instrumented code gets a pointer that comes from here, then those baggy bounds with each particular pointer will always be set to the largest possible value, 2 raised to 31.

Baggy Bounds

This section explains how baggy bounds works on a 32-bit system and how it provides memory safety.

Interoperability between Instrumented and Uninstrumented Code

  • Baggy bounds allows for interoperability between instrumented and uninstrumented code without causing the uninstrumented libraries to blow up.
  • However, out of bounds pointers generated in the uninstrumented code cannot be detected because the high bit is never set.

Limitations of Baggy Bounds

  • Memory safety cannot be provided for operations that take place in uninstrumented code.
  • Passing an out of bounds pointer from instrumented code to uninstrumented code can cause unexpected behavior.

Implementation on a 32-bit System

  • A regular pointer has all bits set to 0 except for the remaining 38 bits representing the address.
  • An out of bounds pointer has its high order bit set to 1, but this only works if it never gets beyond half a slot away from its base.

Baggy Bounds on a 64-bit System

This section explains how baggy bounds works on a 64-bit system.

Storing Information about Bounds in Pointers

  • On a 64-bit system, information about bounds can be stored directly in the pointer itself instead of using a separate table.
  • If the pointer is in bounds, we can set the first 21 bits to 0, put the size in these five bits, and use the remaining 38 bits for address representation.

Handling Out of Bounds Pointers

  • An out of bounds pointer can have its offset directly stored in the pointer using 13 bits, with the remaining bits used for address representation.

Baggy Bounds System

In this section, the professor discusses the baggy bounds system and its benefits. He also talks about some of the limitations and costs associated with using this system.

Benefits of Baggy Bounds System

  • The baggy bounds system uses fat pointers to store both a pointer and its bounds in a single value.
  • These fat pointers are the same size as regular pointers and can be used in structs without changing their size.
  • This system allows for more efficient representation of pointers compared to traditional methods that use multiple words to represent a pointer.

Limitations of Baggy Bounds System

  • There may be certain line issues that require 0-bits in the pointer, which limits the number of bits available for storing offset information.
  • Uninstrumented code cannot be detected by this system, so memory vulnerabilities may still exist.
  • False alarms can occur when out-of-bounds pointers are created but not dereferenced, reducing the effectiveness of security tools.
  • Compiler support is required for implementing this system.

Costs Associated with Baggy Bounds System

  • The space required for storing fat pointers and bounds tables can increase memory usage.
  • CPU overhead is incurred due to checking bounds on almost every pointer operation.
  • False alarms can cause irritation to developers or users who prioritize speed over security.

Non-Executable Memory

This section discusses the concept of non-executable memory as a strategy for preventing buffer overflows.

Non-Executable Memory

  • Non-executable memory is an approach to prevent buffer overflows by making the stack non-executable.
  • The paging hardware specifies three bits for each page in memory: read, write, and execute.
  • By setting the exclusive or x policy, a page can either be written to or treated as executable code but not both. This prevents attackers from running code by creating shell code and jumping to someplace in that buffer.
  • Non-executable memory works at the hardware level and does not require changes to the application. The OS ensures that pages are protected with these bits.
  • One disadvantage of this system is that it makes it harder for an application to dynamically generate code, such as just-in-time compilers used in web browsers.

Randomized Address Spaces

This section discusses randomized address spaces as another strategy for preventing buffer overflows.

Randomized Address Spaces

  • Many attacks use hard-coded addresses, so randomized address spaces make it difficult for attackers to guess addresses.
  • Stack randomization is one way of implementing randomized address spaces. It involves randomly arranging the stack's location in virtual memory space.
  • Another way is heap randomization where heap objects are allocated at random locations in virtual memory space.
  • ASLR (Address Space Layout Randomization) is a technique used by modern operating systems to randomly arrange program elements' locations in virtual memory space.

Randomized Address Spaces

This section discusses the concept of randomized address spaces and how they can be used to prevent attackers from exploiting vulnerabilities in a program's memory.

How Randomized Address Spaces Work

  • The stack and program code can be randomly located in memory, making it difficult for an attacker to exploit vulnerabilities.
  • Attacker-controlled binaries cannot determine the actual location of offsets in the real code running on the server.
  • Relocatable code is not loaded into a specific place in memory, making it easier to randomize its location.

Exploiting Randomized Address Spaces

  • Attackers can extract randomness or find out the random seed that was used to defeat randomized approaches.
  • Attackers can leverage information about randomized locations of objects.
  • Heap attacks involve dynamically allocating shell code and stuffing it randomly in memory. If an attacker controls one of these ret pointers, they may land in shell code. NOP sleds are used to make codes more resilient against randomization.

Wacky Ideas for Randomization

  • Dynamic assignment of syscall numbers to actual syscalls could be created each time a program runs.

Buffer Overflow and Randomization Attacks

In this section, the professor discusses buffer overflow attacks and how they can be prevented using randomization techniques.

Address Space Randomization

  • The key is put into that hardware register when you initially load the program.
  • The attacker doesn't know that key, making it difficult for them to figure out what exactly to put into memory.
  • Attackers can do probes to figure out what's going on in a BROP attack.
  • An attacker could try to xor the binary with known code to extract the key.

Popular Defenses Against Buffer Overflow Attacks

  • GCC and Visual Studio enable stack canaries by default.
  • Linux and Windows can also do things like non-executable memory and randomize the address space.
  • Baggy bounds stuff is not as popular due to memory overhead, CPU usage, false alarms, etc.

Return Oriented Programming (ROP)

In this section, the professor explains return-oriented programming (ROP), which allows attackers to string together preexisting pieces of code in deviant ways.

Insight Behind ROP

  • Instead of generating new code at attack time, attackers can string together preexisting pieces of code.
  • If enough interesting code snippets are found, they can be strung together to form a Turing complete language where attackers can essentially do whatever they want.

Example of ROP

  • A function called "run shell" calls system and executes bin slash bash.
  • A buffer overflow function is declared, but it doesn't seem to access "run shell" in a direct way.

Buffer Overflow Attack

In this section, the speaker explains how an attacker can use a buffer overflow attack to take control of a program's execution flow and run arbitrary code.

Setting up the Attack

  • The attacker overflows the buffer with malicious code.
  • The return address for process message is overwritten by the attacker.
  • The new stack pointer is set to the beginning of the buffer.

Running Arbitrary Code

  • The attacker uses GDB to find the address of run shell.
  • The address of run shell is placed in the overflow array.
  • If there is no command that runs a shell, then we can leverage system and bash path to call system with our argument.

Faking a Calling Frame for System

  • We fake a calling frame for system on the stack.
  • We use GDB to find where system and bash path are located in memory.
  • We ensure that this fake calling frame is in that overflow code that we create.

Creating a Fake Stack Frame

In this section, the speaker explains how to create a fake stack frame and execute any function that is already linked and defined in the application.

Creating a Fake Stack Frame

  • The return address is where system's going to return after it's finished. For the purposes of the discussion now, we don't care what this does, we'll just make this be just some random set of bytes.
  • We're actually going to put the address of bash path up here.
  • Using fake calling frames, we can actually execute any function that is already linked and defined in the application.

Getting Around Missing Strings

  • If the string wasn't actually in the program, you could have the address of bash path point somewhere else and then put slash B-I-N slash P-A-T slash 0 up here.

Chaining Functions Together

In this section, the speaker explains how to chain together several functions by setting useful return addresses.

Setting Useful Return Addresses

  • You could actually set this return address to something that's actually useful.
  • You can start chaining some of these things to do interesting stuff.
  • To call system an arbitrary number of times, you need to use gadgets - small sets of assembly instructions that attackers use to build larger attacks.

Finding Gadgets

  • Pop in EAX takes the top of the stack and puts it into EAX register. Ret instruction pops top of stack and puts it into EIP (instruction pointer).
  • There are off the shelf tools that hackers use to find these gadgets.

Introduction to Return Oriented Programming

In this section, the speaker introduces the concept of return-oriented programming and explains how it can be used for malicious purposes.

How Return-Oriented Programming Works

  • The speaker explains how an exploit works by changing the stack and putting in the address of system, pop ret gadget, and bash path.
  • The speaker describes what happens when we execute this sequence. We start at system, then move to pop ret gadget, then back to system again. This allows us to chain together multiple sequences to execute arbitrary commands.
  • The speaker notes that nothing has been executed on the stack yet, which is how we get around data execution prevention. Instead, we are jumping to different blocks of code in unexpected ways.
  • The speaker explains that return-oriented programming uses the stack pointer as the instruction pointer. As we move the stack pointer, we point to other blocks of code that we want to execute.

Defeating Stack Canaries

In this section, the speaker discusses how canaries work and explores ways to defeat them.

How Canaries Work

  • The speaker explains that a canary is a value placed on the stack that prevents someone from overriding the return address without also overwriting the canary.
  • Before jumping to the return address, the system checks if the canary has been changed in a way that's incorrect.

Defeating Canaries

  • No bullet points with timestamps available for this section.

Defeating Canaries

In this section, the professor discusses how to defeat canaries in a server. The assumptions made are that the server will crash and restart if we set the canary value to a bad one, and after the restart, the canary and any address space randomization that you're doing is not rerandomized.

Guessing Canary Values

  • The attack involves probing each byte of the canary one by one and guessing values for those bytes.
  • If you guess correctly, you move on to guessing the next byte until all values for the canary are found.
  • This process takes advantage of crashes as a signal that something is wrong and staying up as a signal that something is right.

Why Canaries Are Not Always Effective

  • Servers use fork to create new processes which inherit their parent's address space layout. This means that children will have the same values of the canary in their address base.
  • If there's some level of randomization or if you don't quite know where the stack is, it's tricky to directly write to return addresses instead of doing buffer overflow attacks.

Defenses Against Canary Attacks

  • Signal handlers can catch segfaults when programs crash and keep processes alive for a bit. This tricks attackers into thinking they won't get signals back.

Blind Return Oriented Programming

In this section, the speaker discusses blind return-oriented programming (BROP) and how to use it to defeat data execution prevention, address-based randomization, and canaries on a production system. The speaker also explains how to find stop gadgets and stack popping gadgets.

Finding Stop Gadgets

  • A stop gadget is a return address that pauses the program but does not crash it.
  • To engage in BROP, you need to find a stop gadget.
  • Most random addresses will crash the server if used as a stop gadget.
  • Once you find a stop gadget, your socket connection will stay open for a while.

Finding Stack Popping Gadgets

  • You need to use carefully crafted instructions to figure out when you have found one of these stack popping gadgets.
  • The sequence consists of a probe address, a stop address, and a crash address.
  • The probe address is the potential stack popping gadget's address that we put in the stack.
  • The stop gadget is what we found in step one - an address of the stop gadget.
  • The crash gadget is just going to be the address of non-executable code.

Probing for Stack Popping Gadgets

In this section, the speaker discusses how to probe for stack popping gadgets and how to determine which registers these gadgets use.

Probing for Stack Popping Gadgets

  • Hypothesis: One of two addresses is a stack popping gadget.
  • Push sequences onto the stack to test what happens when jumping to an address.
  • If a gadget pops something off the stack and returns, it's a stack popping gadget.
  • If a gadget doesn't pop anything off the stack, it will crash and not be a stack popping gadget.

Determining Which Registers Pop Gadgets Use

  • Need to know which register pop gadgets are popping values into because registers control where arguments are on 64-bit architecture.
  • Use pause system call that takes no arguments to find pause instruction.

Setting up the Attack

In this section, the professor explains how to set up an attack by finding gadgets and system call numbers.

Finding Gadgets and System Call Numbers

  • One gadget pops RDI register, then does a ret.
  • Another gadget pops into a different register, let's say RSI.
  • Put the system call number for pause up here again.
  • Invoke libc function syscall which is then going to execute the requested system call.
  • Jump to the address of this gadget. It will pop something off the stack. We don't know what the register is yet.
  • Each one of these gadgets will hopefully pop the system call number into appropriate RAX register.
  • Once we know for sure where syscall is located, we can try each gadget one by one to see which one induces pause.

Controlling Registers

In this section, the professor explains how attackers can control registers using gadgets and system calls.

Testing Gadgets

  • Try each gadget one by one to see which one induces pause.
  • If it crashed, we know that pop gadget must pop into RDI for example. Get rid of that one and try next gadget.
  • Blindly transfer some address up here and see if you pause.

Leveraging System Calls

  • Leverage fact that for any particular set of registers that you want to be able to control, there's some system call that will give you a signal as an attacker, that allow you to figure out whether you successfully broke it or not.
  • At the end of this phase, you basically have the address of syscall and the address of a bunch of gadgets which allow you to pop into arbitrary registers.

Invoking Write

In this section, the professor explains how to invoke write.

Invoking Write

  • We need to set up RDI register with file descriptor for stdout.
  • We need to set up RSI register with pointer to string we want to print.
  • We need to set up RDX register with length of string we want to print.
  • Once all registers are set up correctly, we can call write system call.

BROP Attack

This section covers the basics of how to launch a BROP attack and how to defend against it.

Invoking Write

  • To invoke write, we need to have the following gadgets: pop RDI, pop RSI, pop RDX, pop racks, and then invoke syscall.
  • The registers being used for system call are: socket/file descriptor, buffer, and length of that buffer.

Controlling Register Values

  • Once we find all these gadgets, we can control the values that are put into those arguments by pushing them on the stack.
  • We use guessing to figure out what the socket is going to be since it's supposed to be the lowest one available.
  • We pass in the pointer to somewhere in the code of the program as a buff pointer.

Exfiltrating Binary

  • Passing in that pointer allows us to read binary out of memory using right call.
  • The attacker can then take that binary and analyze it offline using GDB or other tools.
  • Once they find addresses and offsets for stuff, they can directly attack those gadgets or other vulnerabilities.

Defending Against BROP

  • The most obvious defense is rerandomizing so crashed servers respawn with new randomized versions of themselves.
  • Another defense is doing exec when you spawn your process instead of fork because exec creates totally new randomized layout space on Linux.
  • Windows does not have a fork equivalent so every time you spawn a new server it will have a new randomized address space.
  • Catching faults and keeping connections open for a little while after crashes can confuse attackers and remove signals that something has gone wrong.
  • Bounds checking is another potential defense.

Using Hash of Current Time to Prevent Attacks

In this section, the speaker discusses whether using a hash of the current time is sufficient to prevent attacks.

Hashing Does Not Provide Sufficient Entropy

  • Hashing does not provide sufficient entropy if the input to the hash is easily guessable.
  • If an attacker knows that you're only going to hash one or two things, it doesn't matter how many bits your hash has. They can just guess one of those values and see what it is.

Get Time of Day Has Less Entropy Than You Might Think

  • Get time of day actually has much less entropy than you might think, particularly because the attacker can check what time they are launching the attack.
  • The server skew in terms of clock or client can also remove some entropy from that calculation.

Guessable Base Value Is Useful for Attackers

  • Using a guessable base value, even with guessable just inside a range, is super useful for attackers.
  • Attackers can start subverting a bunch of servers in parallel and know that all of them should have fairly similar values. This is a high order of bits.

Timing Issues in Canary Values

In this section, the speaker discusses timing issues in canary values.

Getting Timing Right Depends on Several Factors

  • Getting timing right depends on several factors such as server skew in terms of clock or client and so on.
  • An attacker can take advantage of opening up multiple connections in parallel and leverage the fact that even if their initial guess is slightly off, they can still launch multiple guesses on what should be very similar canary values and do that attack in parallel.

Turn any video into a summary like this

YouTube links, meetings, lectures — with transcripts, search, and chat.

Video description

MIT 6.858 Computer Systems Security, Fall 2014 View the complete course: http://ocw.mit.edu/6-858F14 Instructor: James Mickens In this lecture, Professor Mickens discusses topics related to buffer overflow exploits, including baggy bounds handling, mitigation approaches, and return-oriented programming. License: Creative Commons BY-NC-SA More information at http://ocw.mit.edu/terms More courses at http://ocw.mit.edu

3. Buffer Overflow Exploits and Defenses | YouTube Video Summary | Video Highlight