testing

crashes and exploits

  • many of the exploits we’ve found are crashes/“obvious” errors when executed incorrectly
    • out-of-bounds overwriting return address (segfault when returning)
    • out-of-bounds overwriting pointer (segfault using pointer)
    • use-after-free bugs causing data corruption
    • SQL injection trigger SQL syntax errors
  • crashes \(\sim\) incomplete/broken exploits

on testing

  • challenges with testing for security:
  • security bugs errors may not trigger error/crash
    • tools tool help with this for some types of bugs: AddressSanitizer, -fsanitize=undefined, taint tracking, …
    • can enable expensive checks when testing that we would not do otherwise
  • security bugs use ‘‘unrealistic’’ inputs — e.g. \(>8000\) character name

automatic testing tools

  • basic idea: generate lots of random inputs — ‘‘fuzzing’’

    • easy to generate weird inputs
  • look for memory errors

    • segfaults, or
    • use memory error detector, or
    • add (slow) ‘assertions’ or other checks to code

  • one of the most common ways to find security bugs

‘blackbox’ fuzzing

fn fuzz_test_image_parser(original_image: Vec<u8>) {
    let rng = rand::rng();
    for _ in 0..NUM_TRIES {
        let mut test_image = original_image.clone();
        let number_of_changes = rng.random_range(0..MAX_CHANGES);
        for _ in 0..number_of_changes {
            test_image[rng.random_range(0..test_image.len())] ^= rng.random::<u8>();
        }
        let result = try_to_parse_image(&test_image);
        if result == CRASH {
            // report that test_image triggers a bug
        }
    }
}

blackbox fuzzing pros

  • works with unmodified software

    • even with embedded assembly, etc.
  • works with many kinds of input

    • don’t need to understand input format
  • easy to parallelize


  • has actually found lots of bugs

‘blackbox’?

  • the program is a ‘‘black box’’ — can’t look inside
  • we only run it, see if it works
  • for memory errors — works \(\approx\) doesn’t crash

what can fuzzing find

  • easiest to find crashes

    • intuition: segfault could be security problem
  • otherwise: how do we know if test cases are useful?


  • need some way to know if test result is correct

  • example: fuzz-testing of C compilers versus other C compilers

    • Yang et al, ‘‘Finding and Understanding Bugs in C compilers’’, 2011
    • 79 GCC, 209 Clang bugs
    • about one third ‘‘wrong generated code’’
    • but using smarter fuzzing strategy (we’ll talk about it later)

testing for non-memory flaws?

  • fuzzing for cross-site scripting bugs?

    • run on web application
    • assert that HTML is well-formed?
  • fuzzing for SQL injection?

    • assert that no malformed SQL gets executed?
  • operating system?

    • input = requests (system calls) to make to the OS

  • (less likely) fuzzing for permissions issues?

    • assert that admin. data doesn’t change?

fuzzing challenges

  • isolation — reproducibility, handling crashes/etc.
  • speed
  • completeness — finding useful tests

fuzzing challenges

  • isolation — reproducibility, handling crashes/etc.
  • speed
  • completeness — finding useful tests

determinism

  • results only useful if they consistently trigger bug
    • fuzzer will run enough tests to trigger “rare” events
  • fully reset all state between each test case
  • avoid use of randomness, current time, etc.
  • debugging tools like AddressSanitizer may help

weird things the tested program does

  • need to detect crashes, ideally automatically
    • likely to find multiple crashes
  • need to detect hangs
    • usually: set threshold amount of time, but…
    • … only useful if program consistent/fast enough
    • won’t find many bugs if we spend our time running hanging test cases
  • need to handle resource exhaustion
    • stop tested program before it uses all the system’s memory/etc.

fuzzing challenges

  • isolation — reproducibility, handling crashes/etc.
  • speed
  • completeness — finding useful tests

how many tests do we want?

  • let’s say we’re making random changes to 512 byte image to find bugs
    • or even systematic changes (like trying every bit/byte/etc.)
  • if our bug involves changing a particular byte
    • need 100s of tests to find that
  • if it also needs changing to a particular value
    • need 100s of tests with the right byte to find that
    • or 10000s of tests
  • if it needs changing another byte too
    • need 10000000s of tests?

preview: improving test cases

  • we’ll talk later about choosing better tests
  • this will make these numbers smaller
    • avoid testing bytes that don’t do anything in image
  • but even with better ways of choosing tests…
  • often want to run many millions+ of tests

some speeding up fuzzing ideas

  • prefer shorter/lower-resource test cases
  • test components separately more
    • custom “harness” that just runs one part of program at a time
  • avoid restarting program for each test (needs way to reset state)
    • make version of program that reads many test cases per run
    • (forward reference: AFL++’s “persistent” mode)
  • run test in way that avoids I/O
    • produce output, but don’t write it anywhere
    • preload/hardcode any configuration files
  • do lots of tests in parallel

fuzzing challenges

  • isolation — reproducibility, handling crashes/etc.
  • speed
  • finding useful tests

completeness problem

  • let’s say we’re testing an HTML parser
  • what code is usually going to run when we flip random bits?
    • (or remove/add random bytes)
  • how often are we going to generate tags not in starting document?
  • how often are we going to generate new almost-valid documents?

HTML with changes

<html><head><title>A</title></head><body>B</body></html>
<html*<head><title>A</title></head><body>B</body></html>
<html><iead><title>C</title></head><body>B</body></html>

grammar-based generation

  • <html*<head and <head><iead> worth testing…
  • …but not that important
    • want more than “syntax error” handling bugs
  • alternate: “grammar” of what the input should look like
    • use that grammar to generate random examples

example: Google Project Zero’s domato

“… Like most DOM fuzzers, Domato is generative, meaning that the fuzzer generates a sample from scratch given a set of grammars that describes HTML/CSS structure as well as various JavaScript objects, properties and functions…”

example: domato grammar excerpt

<html root=true> = <lt>!-- saved from url=(0014)about:internet --<gt><lf><lt>html<gt><head><body><newline><lt>/html<gt>
...
<body> = <newline><lt>body onload=jsfuzzer()<gt><newline><lt>!--beginhtml--<gt><bodyelements><lt>!--endhtml--<gt><newline><lt>/body<gt>
...
<bodyelements> = <newline><element><newline><element><newline><element><newline><element><newline><element><newline><element><newline><element><newline><element><newline><element><newline><element><newline>
...
<element> = <HTMLAnchorElement>
<element> = <HTMLBaseElement>
<element> = <HTMLBaseFontElement>
...
<element> = <HTMLHeadingElement>
...
<HTMLAnchorElement> = <lt>a <a_attributes> <attributes><gt><innerelements><lt>/a<gt>
...
<HTMLHeadingElement> = <lt>h1 <h1_attributes> <attributes><gt><innerelements><lt>/h1<gt>
<HTMLHeadingElement> = <lt>h2 <h2_attributes> <attributes><gt><innerelements><lt>/h2<gt>
<HTMLHeadingElement> = <lt>h3 <h3_attributes> <attributes><gt><innerelements><lt>/h3<gt>
...

CSmith

  • Yang et al wrote a random C program generator
    • “Finding and Understanding Bugs in C compilers” (PLDI 2011)

  • carefully avoided code with unspecified effects
    • most of the work was about doing this
  • don’t need to know what program does: comparing two compilers
    • or one compiler with different settings
  • random selection of types, operators, etc.
  • … instead of just random bytes

fuzzing so far

  • “black box” approaches
    • “blind” changes to input
    • using documentaiton of input format
    • don’t require us to understand anything about how program works
  • alternate idea: take advantage of having the code

thinking about testing

void expand(char *arg) {
    if (arg[0] == '[') {
        putchar('[');
        if (arg[2] != '-' || arg[4] != ']') {
            expand(&arg[1]);
        } else {
            for (int i = arg[1]; i <= arg[3]; ++i) {
                putchar(i);
            }
            putchar(']')
            expand(&arg[5]);
        }
    } else if (arg[0] != '\0') {
        putchar(arg[0]);
        expand(&arg[1]);
    }
}

coverage

  • ‘‘coverage’’: metric for how good tests are


  • % of code reached

  • easy to measure

  • correlates with bugs found

    • but not the same thing as finding all bugs

automated test generation

  • conceptual idea: look at code, go down all paths

  • seems automatable?

    • just need to identify conditions for each path

coverage-guided fuzzing

  • observation: easy to measure which paths a test case uses
  • try to choose random tests biased towards finding new paths
  • generate new random tests by varying “seed” tests
  • check if new random test explores new path
    • if so, add to seed test
  • should gradually make our way down a new path

coverage-guided example (1a)

void foo(int a, int b) {
    if (a != 0) {
        // W
        b -= 2;
        a += b;
    } else {
        // X
    }
    if (b < 5) {
        // Y
        b += 4;
        if (a + b > 50) {
            // Q
            ...
        }
    } else {
        // Z
    }
}

initial test case A:
a = 0x17, b = 0x08; covers: WZ

generate random test cases based on A:

a = 0x37, b = 0x08; covers: WZ
a = 0x15, b = 0x08; covers: WZ
a = 0x17, b = 0x0c; covers: WZ
a = 0x13, b = 0x08; covers: WZ
a = 0x17, b = 0x08; covers: WZ

a = 0x17, b = 0x00; covers: WY

found new test case with different coverage

coverage-guided example (1b)

void foo(int a, int b) {
    if (a != 0) {
        // W
        b -= 2;
        a += b;
    } else {
        // X
    }
    if (b < 5) {
        // Y
        b += 4;
        if (a + b > 50) {
            // Q
            ...
        }
    } else {
        // Z
    }
}

initial test case A:
a = 0x17, b = 0x08; covers: WZ

found test case B (derived from A):
a = 0x17, b = 0x00; covers: WY

generate random test cases based on A and B:

a = 0x37, b = 0x08; covers: WZ
a = 0x04, b = 0x00; covers: WY
a = 0x17, b = 0x01; covers: WZ
a = 0x16, b = 0x00; covers: WY

a = 0x97, b = 0x00; covers: WYQ

a = 0x00, b = 0x08; covers: XY

coverage-guided example (1c)

void foo(int a, int b) {
    if (a != 0) {
        // W
        b -= 2;
        a += b;
    } else {
        // X
    }
    if (b < 5) {
        // Y
        b += 4;
        if (a + b > 50) {
            // Q
            ...
        }
    } else {
        // Z
    }
}

initial test case A:
a = 0x17, b = 0x08; covers: WZ

found test case B (derived from A):
a = 0x17, b = 0x00; covers: WY

found test case C (derived from B):
a = 0x97, b = 0x00; covers: WYQ

found test case D (derived from A):
a = 0x00, b = 0x08; covers: XY

generate random test cases based on A and B and C and D:

coverage-guided example (2a)

void foo(unsigned a,
         unsigned b,
         unsigned c) {
    if (a != 0) {
        b -= c; // W
    }
    if (b < 5) {
        if (a > c) {
            a += b; // X
        }
        b += 4; // Y
    } else {
        a += 1; // Z
    }
    assert(a + b != 7); 
}

initial test case A:
a = 0x17, b = 0x08, c = 0x00; covers: WZ

generate random test cases based on A:

a = 0x37, b = 0x08, c = 0x00; covers: WZ
a = 0x15, b = 0x08, c = 0x02; covers: WZ
a = 0x17, b = 0x0c, c = 0x00; covers: WZ
a = 0x13, b = 0x08, c = 0x40; covers: WZ
a = 0x17, b = 0x08, c = 0x10; covers: WZ

a = 0x17, b = 0x00, c = 0x01; covers: WXY

coverage-guided example (2a)

void foo(unsigned a,
         unsigned b,
         unsigned c) {
    if (a != 0) {
        b -= c; // W
    }
    if (b < 5) {
        if (a > c) {
            a += b; // X
        }
        b += 4; // Y
    } else {
        a += 1; // Z
    }
    assert(a + b != 7); 
}

initial test case A:
a = 0x17, b = 0x08, c = 0x00; covers: WZ

found test case B (based on A):
a = 0x17, b = 0x00, c = 0x01; covers: WXY

generate random test cases based on A and B:

a = 0x37, b = 0x08, c = 0x00; covers: WZ
a = 0x17, b = 0x00, c = 0x03; covers: WXY
a = 0x17, b = 0x0c, c = 0x00; covers: WZ
a = 0x37, b = 0x00, c = 0x03; covers: WXY
a = 0x17, b = 0x08, c = 0x10; covers: WZ

a = 0x17, b = 0x00, c = 0x81; covers: WY

exercise: coverage guidance good for?

void example1(int a, int b) {
    if (a < 4 && b < 4 && a == b) {
        assert(a + b != 6);
    }
}
void example2(int a, int b) {
    assert(a != 10325);
}
void example3(int a, int b) {
    assert(a != 10325 && b != 10543);
}
  • exercise: for which of these functions would coverage guided fuzzing be most/least better than random testing for making the assertion fail?

american fuzzy lop / AFL++

  • most well-known fuzzer using this strategy
    • american fuzzy lop (2013-2016; Michal “lcamtuf” Zalewski)
    • AFL++ (later; community-maintained updated version)
  • compiler “plugins” to record computed/conditional jumps:
  • CoverageMap[EdgeID]++;
    • EdgeID = Hash(Source, Dest) or (more recently) unique conditional jump ID
    • “coverage map” = big array of bytes
    • counter so we can handle loops that run different amounts
    • usually saturating add (so once it gets to 256, it stays there)
  • use values from coverage map to distinguish cases
  • outputs only unique (coverage map) test cases

american fuzzy lop / AFL++ heuristics

  • deterministic tests based on base inputs
    • try flipping every bit, every 2 bits, etc. of base input
    • overwrite bytes with 0xFF, 0x00, etc.
    • removing each byte/blocks of bytes of input
    • etc.
  • multiple strategies for producing new inputs
    • bit-flipping
    • duplicating important-looking keywords
    • combining existing inputs
  • base case selection
    • prefer cases that run faster/are shorter
    • see if removing/zeroing bytes/etc. can simplify an input without changing coverage
    • select set of “favored” cases covering each edge (source+dest of branch)

simplifying testing cases

int array[10];
void vulnerable(char *input) {
    char *p;
    int count = 0;
    p = input;
    if (*p == 'A') { 
        p += 1;
        while (*p == '0') {p += 1; count -= 1;}
        while (*p >= 'A' && *p < 'E') p += 1;
        while (*p == '0') {p += 1; count += 1;}
    }
    if (*p == 'B')
        array[count] += 1;
}
  • example crash: A00ABDBBBDEEDDDCCCBBBDDDAAAA00000000000000000000000B

    • might be what coverage-guided fuzzing finds
  • would really prefer minimal example: AA00000000000B

automatically simplifying test cases

  • but look for same result and/or coverage

  • systematic simplifications:

    • try removing every character (one-by-one)
    • try decrementing every byte
  • keep simplifications that don’t change result

  • AFL uses some of this strategy to help get better ‘base’ tests

    • also has tool to do this on a found test
    • prefers simpler ‘base’ tests

CReduce

  • Regher et al, “Test-Case Reduction for C Compiler Bugs” (PLDI 2012)

  • take a C program that triggers bug…

  • try removing things to make it smaller

  • needed: automated way of checking “is bug still there”

    • compilers: took advantage of multiple implementations
  • same idea applies to security bugs

    • remove as much as possible and get it to still segfault, etc.

AFL: manual keywords

  • AFL supports a dictionary
    • list of things to add to create test cases
    • example: all possible HTML tags
  • compiler plugin to generate automatic dictionary
    • outputs constants/strings compared with
  • AFL’s random mutations try inserting values from dictionary

  • other strategy: test-case template
  • other strategy: test postprocessing (fix checksums, etc.)

cmplog

efficiency of finding new coverage

  • PNG files have chunks with 4-byte type labels
  • parser code something like:
uint32_t image_chunk_type;
fread(&image_chunk_type, 4, 1, image_file);
...
if (image_chunk_type == CHUNK_TYPE_PALETTE) {
    ...
}
  • if we make random changes to 512-byte PNG image, how long till we enter that if statement?
    • need to randomly chnage 4 bytes containing “chunk type”
    • need to randomly choose CHUNK_TYPE_PALETTE
    • (or have CHUNK_TYPE_PALETTE in “dictionary”)

comparison logging idea

  • instead of just looking at coverage, look at what comparisons run
  • figure out which input matters for comparison this way

comparison log example

void foo(unsigned a, unsigned b, unsigned c) {
    if (a != 0) { // (1)
        b -= c; // W
    }
    if (b < 5) { // (2)
        if (a > c) { // (3)
            a += b; // X
        }
        b += 4; // Y
    } else {
        a += 1; // Z
    }
    assert(a + b != 7);  // (4)
}
input comparisons
a=0, b=0, c=0 0 != 0, 0 < 5, 0 > 0, 4 != 7
a=1, b=0, c=0 1 != 0, 0 < 5, 1 > 0, 5 != 0
a=0, b=1, c=0 0 != 0, 1 < 5, 0 > 0, 5 != 0
a=0, b=0, c=1 0 != 0, 0 < 5, 0 > 1, 4 != 0

using comparison logging: exercise (1)

input comparisons
a=0, b=0, c=0 0 < 100, 0 == 0x1234
a=1, b=0, c=0 1 < 100, 0 == 0x1235
a=2, b=0, c=0 2 < 100, 0 == 0x1236
a=0, b=1, c=0 0 < 100, 0 == 0x1244
a=0, b=2, c=0 0 < 100, 0 == 0x1254
a=0, b=0, c=1 0 < 100, 1 == 0x1334
a=0, b=0, c=2 0 < 100, 2 == 0x1334
  • exercise 1: If I want to make the first comparison true, what should I change from a=0,b=0,c=0?
  • exercise 2: If I want to make the second comparison true, what should I change from a=0,b=0,c=0?

using comparison logging: exercise (2)

input comparisons
aaaaaaaa 0x61616161 == 0xDEADFF, 0x61616161 == 0x12345678, 'a' == '#'
baaaaaaa 0x62616161 == 0xDEADFF, 0x61616162 == 0x12345678, 'b' == '#'
caaaaaaa 0x63616161 == 0xDEADFF, 0x61616163 == 0x12345678, 'c' == '#'
abaaaaaa 0x61626161 == 0xDEADFF, 0x61616261 == 0x12345678, 'a' == '#'
acaaaaaa 0x61636161 == 0xDEADFF, 0x61616361 == 0x12345678, 'a' == '#'
aabaaaaa 0x61616261 == 0xDEADFF, 0x61626161 == 0x12345678, 'a' == '#'
aacaaaaa 0x61616361 == 0xDEADFF, 0x61636161 == 0x12345678, 'a' == '#'
aaabaaaa 0x61616162 == 0xDEADFF, 0x62616161 == 0x12345678, 'a' == '#'
aaacaaaa 0x61616163 == 0xDEADFF, 0x63616161 == 0x12345678, 'a' == '#'
  • exercise: good new test cases?

“colorization”

  • try changing all parts of input
  • identify which parts affect which comparisons
    • called “colorization”
    • color bytes of input based on which comparisoins they correspond tog
  • more sophisticated ideas:
    • figure out where number in comparison comes from
    • figure out likely encoding of number in input

still guessing

  • so far: guessing based on observed/coverage comparisons
  • experimental observations based on observing inputs
  • not taking full advantage of having program available
  • alternate idea: read code to find compute how to make comparison true
    • closer to what humans testing would do
    • but other trade-offs