University of Minnesota
Program Analysis for Security
index.php

Hands-on Assignment 1: symbolic execution and (separately) binary code

Questions 1-3 on symbolic execution use the KLEE tool, whose web page is klee.llvm.org. They have links to a number of tutorials, manuals, and academic papers. KLEE is open source, though because of its LLVM dependencies the process of compiling it from scratch is a bit involved. For the purposes of this assignment it should be sufficient to use their prepackaged "CDE" binary distribution, which should work on any modern x86/Linux system, such as one of your own or a department-administered machine. As a further convenience I've already downloaded and unpacked a copy of this on the compute server dio.cs; if you SSH there you can get started by looking at the README file at:

/export/scratch/csci-8980-pas-sp2013/klee-cde-package/README

As a further convenience I've compiled a copy of KLEE and put it on a local drive of the compute server dio.cs (for reasons I don't understand the CDE package didn't work reliably on this machine). It's in the directory:

/export/scratch/csci-8980-pas-sp2013/klee-precise

with programs under bin, include files under include/klee, etc.

Question 4 just requires a 32-bit-capable x86 version of GCC.

Remember that this is an individual assignment. It's permitted to discuss it at a high level with other students, but each student must submit their own answer, having written all the code and prose in it themselves. Provide proper attribution to any people (other than the instructor) or other resources (books, web sites), that you got ideas from.

1. Magic squares with KLEE (20 pts)

An order-3 normal magic square is a 3x3 matrix containing a permutation of the integers from 1 to 9, such that the sum of the entries in each row, each column, and both of the diagonals is the same value. (Since the total of all the entries is 45, it follows specifically that the sum of each of the rows, columns, and diagonals must be 15.) By applying KLEE to a function that tests whether a matrix is a magic square, we'll get KLEE to create a magic square for us.

a. (10 pts) Your classmate Alyssa started implementing the checking function, but didn't have time to finish it. Fill in the remaining checks (here's the whole file):

/* Check whether the integer array pointed to by A is an order-ORDER
   normal magic square. I.e., A should contain ORDER**2 elements, a
   permutation of the integers from 1 to ORDER**2, such that when they
   are viewed as an ORDERxORDER square array, each row, column, and
   both main diagonals have the same sum. Returns normally if the
   array is a magic square, otherwise aborts. */
void check_square(int order, int *a) {
    int max = order * order;

    /* Value that each row, column, and diagonal must sum to: */
    int target_sum = (order * (order * order + 1)) / 2;

    int i, j, n;

    /* For each integer from 1 to ORDER**2, */
    for (n = 1; n <= max; n++) {
        /* Check that it appears at least once. */
        int product = 1;
        for (i = 0; i < max; i++) {
            product &= (a[i] != n);
        }
        assert(product == 0);
    }
    /* If each of the numers appears at least once, it follows that
       they all also appear at most once, so we don't have to check
       that separately. */

    /* Check that each row has the correct sum. */
    /* For you to fill in. Use assert() to check. */

    /* Check that each column has the correct sum. */
    /* For you to fill in. */

    /* Check that both diagonals have the correct sum. */
    /* For you to fill in. */

    printf("Check succeeded\n");
}

Then compile the code with LLVM and run KLEE on it. You should see the output from some executions that ended with assertion failures, and one that ended with "Check succeeded". Then look through the test cases KLEE generated to find which test corresponds to the successful square.

Hints: The assertion failure messages are saved in ".assert.err" files, but you want the test without any assertion failure. The ".ktest" files are binary, but you can print them with the "ktest-tool" program. To convert that tool's backslash-escaped hex strings back into plain strings, you might find perl and its "unpack" function useful, as in:

perl -le 'print join(" ", unpack("I*", "\x01\x00\x00\x00\t\x00\x00\x00"))'

b. (10 pts) The code Alyssa wrote for checking whether all the numbers appear in the matrix, shown above, looks rather weird. Why do you think she implemented the check this way?

Hint: Try rewriting the check in a more normal way and repeating the KLEE run. How are things different?

2. Counting paths through rot13 (25 pts)

"Rot13" is a not-very-secure encryption system for English text, which can be implemented as follows (here's the complete program):

static inline char rot13_char(char c) {
    if (c >= 'A' && c <= 'Z') {
        return 'A' + ((c - 'A') + 13) % 26;
    } else if (c >= 'a' && c <= 'z') {
        return 'a' + ((c - 'a') + 13) % 26;
    } else {
        return c;
    }
}

int rot13(char *in, char *out, int n) {
    int i;
    for (i = 0; i < n && in[i] != '\0'; i++) {
        out[i] = rot13_char(in[i]);
    }
    out[i] = '\0';
    return i;
}

Your classmate Ben was using this code along with KLEE to search for strings that rot13-decrypted to his name. For instance he found that "Ora" decrypts to "Ben". However when he tried again with his full name "Benjamin B. Bitdiddle III", he didn't have so much luck.

To help Ben understand what's going wrong, explain to him how many execution paths KLEE would have to explore if you ran rot13 on a 25-character symbolic input. It won't be feasible to determine this number of paths directly by experiment, so instead, run KLEE on some short strings to see what the pattern is, and then use math to extrapolate. Be sure to explain where your number comes from: for instance if you use a formula that has a constant in it, you should explain why that constant has the value it does.

Math hint: You might want to review the formula for the sum of a finite geometric series. And/or you can "cheat" by using the Online Encyclopedia of Integer Sequences: it's not actually cheating if you acknowledge it properly.

3. Implementation limitations (25 pts)

In an idealized sense, the algorithms used in KLEE satisfy the following nice properties, for instance in the context checking whether an "assert()" statement in a program could ever be violated:

  • No false positives: if KLEE produces a program input that it claims triggers an assertion, then if you run the original program with that same input, the same assertion will trigger.
  • No false negatives: if KLEE's exploration of a program terminates, and it finds no way to trigger an assertion, than in fact that assertion could never trigger when running the original program.

(Recall that the "if exploration terminates" condition in the later is why these don't violate the usual halting-problem impossibility results.)

However neither of these properties really holds strictly over all possible uses of the real implemented system, as the KLEE authors themselves acknowledge. In addition to outright bugs, exceptions can also arise from the various ways KLEE reimplements or models aspects of programs and their operating environments.

Give a worked-out example of a violation of one of these properties, demonstrated with an experiment using the standard KLEE implementation. In other words, construct a program that can fail an assertion when run directly, but not when explored by KLEE, or conversely a program for which KLEE proposes an assertion failure that does not occur in regular execution.

Hint (inspired by an exchange with David Gloe): one rich source of differing behaviors is KLEE's system call model, like the model for read() sketched in Figure 3 of the paper. For instance this model embodies an assumption that if you read twice from the offset in a file, you'll get the same value back. (Since if the file is symbolic, you get the same symbolic bytes.) If you're having trouble imagining how this assumption could ever fail in a real system, you might want to review the concepts of race conditions and time-of-check vs. time-of-use vulnerabilities.

4. Reading binary code (30 pts)

Here's the disassembled code for a small function:

00000000 <mystery>:
   0:   55                      push   %ebp               push   ebp
   1:   57                      push   %edi		  push   edi
   2:   56                      push   %esi		  push   esi
   3:   53                      push   %ebx		  push   ebx
   4:   83 ec 04                sub    $0x4,%esp	  sub    esp,0x4
   7:   8b 44 24 18             mov    0x18(%esp),%eax	  mov    eax,DWORD PTR [esp+0x18]
   b:   c7 04 24 00 a3 e1 11    movl   $0x11e1a300,(%esp) mov    DWORD PTR [esp],0x11e1a300
  12:   85 c0                   test   %eax,%eax	  test   eax,eax
  14:   7e 36                   jle    4c <mystery+0x4c>  jle    4c <mystery+0x4c>
  16:   be 03 00 00 00          mov    $0x3,%esi	  mov    esi,0x3
  1b:   bb 00 e1 f5 05          mov    $0x5f5e100,%ebx	  mov    ebx,0x5f5e100
  20:   b9 01 00 00 00          mov    $0x1,%ecx	  mov    ecx,0x1
  25:   8d 76 00                lea    0x0(%esi),%esi	  lea    esi,[esi+0x0]
  28:   89 cf                   mov    %ecx,%edi	  mov    edi,ecx
  2a:   89 cd                   mov    %ecx,%ebp	  mov    ebp,ecx
  2c:   0f af fe                imul   %esi,%edi	  imul   edi,esi
  2f:   83 c1 01                add    $0x1,%ecx	  add    ecx,0x1
  32:   89 da                   mov    %ebx,%edx	  mov    edx,ebx
  34:   89 d8                   mov    %ebx,%eax	  mov    eax,ebx
  36:   c1 fa 1f                sar    $0x1f,%edx	  sar    edx,0x1f
  39:   f7 db                   neg    %ebx		  neg    ebx
  3b:   83 c6 02                add    $0x2,%esi	  add    esi,0x2
  3e:   0f af f9                imul   %ecx,%edi	  imul   edi,ecx
  41:   f7 ff                   idiv   %edi		  idiv   edi
  43:   01 04 24                add    %eax,(%esp)	  add    DWORD PTR [esp],eax
  46:   3b 6c 24 18             cmp    0x18(%esp),%ebp	  cmp    ebp,DWORD PTR [esp+0x18]
  4a:   75 dc                   jne    28 <mystery+0x28>  jne    28 <mystery+0x28>
  4c:   8b 04 24                mov    (%esp),%eax	  mov    eax,DWORD PTR [esp]
  4f:   83 c4 04                add    $0x4,%esp	  add    esp,0x4
  52:   5b                      pop    %ebx		  pop    ebx
  53:   5e                      pop    %esi		  pop    esi
  54:   5f                      pop    %edi		  pop    edi
  55:   5d                      pop    %ebp		  pop    ebp
  56:   c3                      ret    			  ret

(The version on the left is the AT&T syntax that's the default format used by GNU tools; the version on the right is Intel syntax that is more common on Windows and is used in Intel's documentation. You can get the latter format out of objdump by using the "-Mintel" option.)

Your mission is to decompile it: i.e., translate it back into C code, which when recompiled, has the same behavior. It's not required that your C code compiles into exactly the same binary instructions, though if you can accomplish that, it's a good way to prove you've done the decompilation correctly. (In attempting this you'll want to know that the compiler I used was GCC version 4.6.3-1ubuntu5 from Ubuntu 12.04, with the options "-m32 -O2".) You can also test your version versus the original by writing code that calls them both: the mystery function takes one integer and returns an integer as an argument, as if it has the C declaration "int mystery(int x)". Here's the compiled but unlinked object file mystery.o.

Hints:

  • The ultimate resource for understanding what each instruction does is the programming manuals that Intel publishes, which you can download in PDF format from their web site. The length of the whole manuals is intimidating, but the reference section that's split up by instruction mnemonic is the best place to start. You can also find a number of shorter summaries elsewhere on the web.
  • The easiest way to get started might be to translate the instructions one at a time, and introduce on C variable for each machine level location (like a register). However this doesn't produce very idiomatic C code, so you should probably then go back and introduce nice control-flow structures and give your variables reasonable names.
  • One operation that will look tricky is division. When you use the "/" operator on C "int"s, you're dividing a 32-bit dividend by a 32-bit divisor, but the x86 "idiv" instruction operates on a 64-bit dividend. You may want to review the concept of "sign extension" if you're rusty on it.
  • The letter "a" in the mnemonic "lea" stands for "address", but at the machine level there's no distinction between integers and pointers, so there's no requirement that the instruction only be used on addresses.

What to turn in

  • A PDF or plain-text document containing the magic square from 1(a), and your prose answers to 1(b), 2, and 3.
  • A .tar or .zip archive file containing the code you modified or wrote: a completed version of magic-square.c, the program that demonstrates the false positive or negative for question 3, and the C source for mystery.c.

Send your answers to the instructor, using the address mccamant@cs.umn.edu, before midnight on Monday, February 25th.

The solutions are now available on another page.