static

fuzzing/symbolic exec imprecision

  • symbolic execution had some nice properties:

    • could reliably enumerate possible paths
    • could figure out inputs
    • could prove paths are impossible
  • but had huge practical problems:

    • not enough time/space to explore all those paths
    • too complicated to actually solve equations to find inputs

complete versus sound

  • complete (all true positives found)

    • if way to find problem, analysis finds it
  • sound (no false positives found)

    • if analysis finds way problem, it’s a real problem

  • greybox fuzzing, symbolic exec without approximations: always sound

    • because they actually run the program
  • symbolic execution without approximations: complete if all paths are solved

    • but that isn’t practical for a large program

other program analysis designs

  • other design points than symbolic execution:


  • not tracking all the variable values

  • alternative: just track properties of interest


  • compute precisely what paths through code are possible

  • alternative: track sub/superset of possible paths

    • superset = find false positives; subset = false negatives

model for use-after-free

  • model for use-after-free, pointer is:

    • allocated
    • freed
    • (other states?)
  • just track this logical state for each pointer

  • ignore everything else

  • assume all if statements/loop conditions can be true or false

checking use-after-free (1)

void someFunction(int foo, int bar) {
    int *quux = malloc(sizeof(int));
    // A
    ... /* omitted code that doesn't use quux */
    free(quux);
    // B
    ... /* omitted code that doesn't use quux */
    // C
    *quux = bar;
    ...
}

  • can give warning — probably bad
  • exercise: how could this be a false positive?

result from clang’s scan-build

Screenshot of scan-build report showing 'Use of memory after it is freed'. An exercept of the source code indicates memory is allocated in the malloc() in someFunction(), released by the free, then used by the line '*quux = bar;'

checking use-after-free (2)

int *someFunction(int foo, int bar) {
    int *quux = malloc(sizeof(int));
    // A
    if (Complex(foo)) {
        free(quux);
        // B
    }
    ... /* omitted code that doesn't use quux */
    if (Complex(bar)) {
        // C
        *quux = bar;
    }
    ... /* omitted code that doesn't use quux */
    // D
}

similar to symbolic execution
form graph of states of quux

three ways to reach D (ABD, ACD, AD)
but only care if quux freed/allocated
so only two cases to consider

  • can find possible use-after-free path, but whether it’s possible depends on what Complex() does

result from clang’s scan-build

Screenshot of scan-build output showing annoytated source code. 7 steps in the code are annotated: 1. memory is allocated (at malloc()); 2. Assuming the Complex(foo) condition is true; 3. Taking true branch of  the if statement; 4. Memory is released (at free()); 5. Assuming the Complex(bar) condition is true; 6. Taking true branch of if statement; 7. Use of memory after it is freed (at *quux).

exercise: holes in the model?

void example(int a) {
    int *p;
    int *q;
    q = malloc(...);
    p = malloc(...);
    // (A)
    if (a > 0) {
        // (A1)
        p = q;
    }
    // (B)
    free(p);
    // (C)
    ...
}

exercise: what should state of pointer q be at C?

A. allocated B. freed
C. allocated if+only if reached via path with A1
D. freed if+only if reached via path with A1
E. something else?

clang-analyzer output

clang-analyzer screenshot finding an error when '*q = 1;' is placed at (C). The sequence of events identified is: memory is allocated at 'q = malloc(...)'; assuming a > 0, the true branch of the if is taken; memory is released at 'free(p)'; then memory is used after freed at '*q = 1'

analysis building blocks

  • needed to track that p and q could point to same thing
  • common prerequisite for all sorts of program analysis

overly simple algorithm for points-to analysis

  • for each pointer/reference track which objects it can refer to
  • if multiple paths: take union of all possible

simple points-to analysis

void example(int a) {
    int *p;
    int *q;
    q = malloc(...); // ID=1
    p = malloc(...); // ID=2
    // (A)
    if (a > 0) {
        p = q;
        // (B)
    }
    // (C)
    ...
}

mark different versions of of p, q
track as if separate varaibles

shown above: track each path separately
but means we’ll have too many nodes in big program

alternate idea: avoid path explosion by merging possible sets

complicating points-to analysis

  • would like to analyze program function-at-a-time, but…

    • functions can change values shared by other functions
  • what about computed array indices?

  • what about pointers to pointers?


  • high false-positive solution:

    • when incomplete info: assume value points to anything of right type
  • high false-negative solution:

    • when incomplete info: assume value points to nothing

loops

  • real code has loops
  • seems like a problem if we have to go through all the paths?

checking use-after-free (3)

void someFunction() {
    int *quux = malloc(sizeof(int));
    ...
    // A
    do {
        // B
        ...
        if (anotherFunction()) {
            free(quux);
            // C
        }
        ...
        // D
    } while (complexFunction());
    ...
    // E
    *quux++;
    ...
}

result from clang’s scan-build

static analysis limits

  • need to avoid exploring way too many paths
    • clang-analyzer: only a procedure at a time
    • in general: some way(s) of pruning paths
  • need to avoid false positives
    • probably can’t always assume every if can be true/false
    • one idea: apply symbolic-execution like techniques to prune
    • clang-analyzer: limited by being procedure-at-a-time
  • likely to end up with false positives + false negatives

common bug patterns

  • effectively detecting things like “values aren’t used after being freed” is not very reliable for large programs
    • (but analysis tools are getting better)
  • but static analysis tools shine for common bug patterns
    • … that don’t need a lot of context from surrounding code

patterns clang’s analyzer knows

struct foo *p = malloc(sizeof(struct foo*)); // meant struct foo?
long *p = malloc(16 * sizeof(int)); // meant sizeof(long)?

strncat(foo, bar, sizeof(foo));

int *global;
int *foo() {
    int x;
    int *p = &x;
    ...
    global = p; // putting pointer to stack in global
    return p;    // returning pointer to stack
}

more suspect patterns

  • SpotBugs: Java static analysis tool; example patterns:
// pattern: connecting to database with empty password:
connection = DriverManager.getConnection(
    "jdbc:hsqldb:hsql://db.example.com/xdb" /* database ID */, 
    "sa" /* username */, "" /* password */);

// pattern: Sql.hasResult()'s second argument isn't a constant
Sql.hasResult(c, "SELECT 1 FROM myTable WHERE code='"+code+"'");

// pattern: new FileReader's argument comes from request
HttpRequest request = ...;
String path = request.getParameter("path");
BufferedReader r = new BufferedReader(
    new FileReader("data/" + path));

Backup slides

static analysis practicality

  • good at finding some kinds of bugs

    • array out-of-bounds probably not one — complicated tracking needed
  • excellent for ‘‘bug patterns’’ like:

struct Foo* foo;
...
foo = malloc(sizeof(struct Bar));
  • false positive rates are often 20+% or more
  • some tools assume lots of annotations
  • not limited to C-like languages

static analysis tools

  • Coverity, Fortify — commerical static analysis tools

  • Splint — unmaintained?

    • written by David Evans and his research group in the late 90s/early 00s
  • FindBugs (Java)

  • clang-analyzer — part of Clang compiler

  • Microsoft’s Static Driver Verifier — required for Windows drivers:

    • mostly checks correct usage of Windows APIs

checking for array bounds

  • can try to apply same technique to array bounds

  • but much more complicated/more likely to have false positives/negatives


  • for each array or pointer track:

    • minimum number of elements before/after what it points to
  • for each integer track:

    • minimum bound
    • maximum bound
  • similar analysis looking at paths?

checking array bounds (1)

checking array bounds (2)

preview: information flow

  • really common pattern we want to find:
    data from somewhere gets to dangerous place

    • pointer to stack escapes function
    • input makes it to SQL query, file name
  • we’ll talk about it specially next

information flow

  • so far: static analysis concerned with control flow

  • often, we’re really worried about how data moves


  • many applications:

    • does an array index depend on user input?
    • does an SQL query depend on user input?
    • does data sent over network depend on phone number?

  • can do this statically (potential dependencies)
    or dynamically (actual dependencies as program runs)

information flow graph (1a)

def f(a, b, c):
    desc = 'a={},b={}'.format(a, b)
    if b > 10:
        y = a
    else:
        y = c
    w = y + a
    pair = (w, c)
    desc = desc + \
         ',pair={}'.format(pair)
    print(desc)
    return y

information flow graph (1b)

  • ex: does returned value depend on a, b, c?
  • ex: does value of pair depend on a, b, c?
  • ex: does printed value depend on a, b, c?

information flow and control flow

def f(a, b, c):
    if b > 10:
        y = a
    else:
        y = c
    return y
  • Q: which is better …

    • if we’re trying to see if user input makes it to SQL query?
    • if we’re trying to determine if private info goes out over network?

static info flow challenges (1)

# Python example
def stash(a):
    global y
    y = a
x = [0,1,2,3]
stash(x)
x[2] = input()
print(y[2])
// C example
int *y;
void stash(int *a) {
    y = a;
}
int main() {
    int x[3];
    stash(x);
    y[2] = GetInput();
    printf("%d\n",x[2]);
}
  • same points-to problem with static analysis

  • need to realize that x[2] and y[2] are the same!

    • even if assignment to/usage of y is more cleverly hidden
  • can fix this with dynamic approach: monitor running program

static info flow challenges (2)

def retrieve(flag):
    global the_default
    if flag:
        value = input()
    else:
        value = the_default
    value = process(value)
    if not flag:
        print("base on default: ",value)
    return value
retrieve(True)
retrieve(False)
  • input can’t make it to print here
  • … but need path-sensitive analysis to tell
  • can fix this we dynamic approach: monitor running program

static info flow challenges (3)

x = int(input())
if x == 0:
    print(0)
elif x == 1:
    print(1)
elif ...
  • does input make it to output?

  • should we try to detect this?

    • probably depends on intended use of analysis
  • harder to fix this issue

sources and sinks

  • needed choose sources (so far: function arguments)
    and sinks (so far: print, return)

  • choice depends on application

  • SQL injection:

    • sources: input from network
    • sinks: SQL query functions
  • private info leak:

    • sources: private data: phone number, message history, email, …
    • sinks: network output