Main navigation | Main content
a. (10 pts) Your classmate Alyssa started implementing the checking function, but didn't have time to finish it. Fill in the remaining checks:
/* Check that each row has the correct sum. */ for (i = 0; i < order; i++) { int sum = 0; for (j = 0; j < order; j++) { sum += a[order*i + j]; } assert(sum == target_sum); } /* Check that each column has the correct sum. */ for (j = 0; j < order; j++) { int sum = 0; for (i = 0; i < order; i++) { sum += a[order*i + j]; } assert(sum == target_sum); } /* Check that both diagonals have the correct sum. */ { int sum = 0; for (i = 0; i < order; i++) { sum += a[order*i + i]; } assert(sum == target_sum); } { int sum = 0; for (i = 0; i < order; i++) { sum += a[order*i + (order - i - 1)]; } assert(sum == target_sum); } printf("Check succeeded\n");
Look through the test cases KLEE generated to find which test corresponds to the successful square.
% ktest-tool klee-out-0/test000001.ktest ktest file : 'klee-out-0/test000001.ktest' args : ['magic-square.o'] num objects: 1 object 0: name: 'square' object 0: size: 36 object 0: data: '\x08\x00\x00\x00\x01\x00\x00\x00\x06\x00\x00\x00 \x03\x00\x00\x00\x05\x00\x00\x00\x07\x00\x00\x00 \x04\x00\x00\x00\t \x00\x00\x00\x02\x00\x00\x00'
8 | 1 | 6 |
3 | 5 | 7 |
4 | 9 | 2 |
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?
The way Alyssa wrote the checking code reduces the number of branches. The more usual way of writing a check like this would break out of the inner loop as soon as it found an occurrence of the target number, which has the advantage of reducing the number of iterations of the inner loop required. But if the code were written that way, every possible combination of locations of the numbers would be a different control-flow path, greatly increasing the number of paths symbolic execution would have to explore. By contrast Alyssa's use of the &= operator to update a boolean can be compiled without a branch. There's still a branch for the assert at the end of each loop iteration, but since the program will terminate if the assertion fails, this branch also doesn't cause a multiplicative increase in the number of paths.
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.
If we run the example to completion with symbolic string inputs of length l equal to 1, 2, 3, 4, and 5, KLEE reports the number of paths as 6, 31, 156, 781, and 3906:
% klee -libc=uclibc --posix-runtime rot13.bc -sym-arg 5 [...] KLEE: done: completed paths = 3906
The formula for this sequence is (5(l+1) - 1)/4, which is the formula for the sum of a geometric series with ratio 5 (6 = 1 + 5, 31 = 1 + 5 + 25, etc.). The reason for this summation is that a symbolic string of length l can hold a string of length l or of a shorter length, if one of the characters is a null. For instance for a three character symbolic input, it could represent the unique empty string if the first character is \0, or it could represent a one-character string if the second character is \0, or a two-character string if the third character is \0, or a three-character string if none of the symbolic bytes is null.
The ratio 5 is the branching factor: the factor by which the number of paths increases for each additional symbolic byte. If we were just exploring concretely, the branching factor would be 256, but for symbolic execution it's determined by the number of possible paths through the body of the loop. For instance all upper-case characters take the same path. The case of a character being null is counted separately (as discussed in the previous paragraph), so the factor of 5 comes from the remaining possible results of the comparisons with the letter ranges. A character can be either (1) less than A, (2) an upper-case letter between A and Z, (3) greater than Z but less than a, (4) a lower-case letter between a and z, or (5), greater than z. This corresponds to treating the two parts of the AND conditions in the rot13_char function as separate branches. Note that the number of paths is still less than the 7 you would expect by looking at the control-flow graph, because some of those paths are infeasible: if a character is less than A, it's guaranteed to be less than a.
Depending on the optimization you use, you might also get results that are the same as described above, except with a ratio and branching factor of 3. This corresponds to a way of compiling the rot13_char function in which the ANDed conditions are each a single branch, so that the three paths through the function correspond to the three return statements.
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.
Let's take the hint. Here's a program that reads four bytes from the beginning of a file, waits ten seconds, reads those same bytes again, and asserts that the two values are equal:
#include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> int main(int argc, char **argv) { int fd, x1, x2; if (argc != 2) { fprintf(stderr, "Usage: reread <input>\n"); exit(1); } fd = open(argv[1], O_RDONLY); if (fd == -1) { fprintf(stderr, "Open failed: %s\n", strerror(errno)); exit(1); } read(fd, &x1, sizeof(int)); sleep(10); lseek(fd, 0, SEEK_SET); read(fd, &x2, sizeof(int)); assert(x1 == x2); exit(0); }
On Unix there is no guarantee that the two reads will read the same value. Files aren't automatically locked when a program opens them (and even the locks that Unix does support are usually only advisory), so another program might write to the file in between the reads. Waiting for ten seconds between the reads isn't necessary: it just makes the possibility easier to demonstrate. This sort of unexpected interleaving of operations is a significant source of security vulnerabilities, though the most common instances tend to involve operations on files in directories (which can often be exploited using symbolic links) rather than the contents of files.
KLEE's symbolic model of system calls uses a single symbolic variable for any read of a given byte from a file, so it implicitly assumes that file changes like this can't happen. As such, KLEE will conclude that the assertion cannot be triggered. But you can see that this is a false negative by running the real program and writing to the file during the 10-second wait.
Note that writing to the file from the same program (i.e., in place of the sleep system call) doesn't trigger the same problem, because KLEE's symbolic system call layer updates its model of the file's contents in that case.
Trying to do the write in a separate process with fork(2) is another idea that does expose a limitation of KLEE's system call model, but it's not the reuse of symbolic values. Rather, it exposes that KLEE doesn't implement fork. If you have the POSIX model turned on, fork will always return an error. If you don't have it turned on, the entire KLEE process will fork, which also doesn't have the right behavior because it makes an independent copy of the symbolic state.
/* 00000000 <mystery>: */ /* 0: 55 push %ebp */ /* 1: 57 push %edi */ /* 2: 56 push %esi */ /* 3: 53 push %ebx */ /* 4: 83 ec 04 sub $0x4,%esp */ /* These instructions save the callee-saved registers and set up the stack frame; we don't need to translate them. */ int mystery1(int arg) { /* 7: 8b 44 24 18 mov 0x18(%esp),%eax */ /* Because we pushed/reserved 20 bytes of stack in the prolog, 0x18(%esp) corresponds to the first (and only) argument. */ int eax = arg; /* b: c7 04 24 00 a3 e1 11 movl $0x11e1a300,(%esp) */ /* A local variable on the stack */ int temp = 0x11e1a300; /* 12: 85 c0 test %eax,%eax */ /* This tests sets ZF iff %eax is 0, SF if it's negative, and clears OF. */ /* 14: 7e 36 jle 4c <mystery+0x4c> */ /* Thus, the condition "le" corresponds to %eax being signed less than or equal to zero. The branch is used to skip over code, so we turn it into an "if" with the opposite condition. */ if (eax > 0) { /* 16: be 03 00 00 00 mov $0x3,%esi */ int esi = 3; /* 1b: bb 00 e1 f5 05 mov $0x5f5e100,%ebx */ int ebx = 0x5f5e100; /* 20: b9 01 00 00 00 mov $0x1,%ecx */ int ecx = 1; int edx, ebp; /* 25: 8d 76 00 lea 0x0(%esi),%esi */ /* This instruction has no effect, it's just used for padding because the next instruction is the target of a loop back edge. */ do { /* 28: 89 cf mov %ecx,%edi */ int edi = ecx; /* 2a: 89 cd mov %ecx,%ebp */ ebp = ecx; /* 2c: 0f af fe imul %esi,%edi */ edi *= esi; /* 2f: 83 c1 01 add $0x1,%ecx */ ecx++; /* 32: 89 da mov %ebx,%edx */ edx = ebx; /* 34: 89 d8 mov %ebx,%eax */ eax = ebx; /* 36: c1 fa 1f sar $0x1f,%edx */ /* This is a sign-extending shift that throws away all but the sign bit, so it sets edx to -1 if it was negative and to 0 otherwise. */ edx >>= 31; /* 39: f7 db neg %ebx */ ebx = -ebx; /* 3b: 83 c6 02 add $0x2,%esi */ esi += 2; /* 3e: 0f af f9 imul %ecx,%edi */ edi *= ecx; /* 41: f7 ff idiv %edi */ { long long dividend = (long long)edx << 32 | eax; eax = dividend / edi; edx = dividend % edi; } /* 43: 01 04 24 add %eax,(%esp) */ temp += eax; /* 46: 3b 6c 24 18 cmp 0x18(%esp),%ebp */ /* 4a: 75 dc jne 28 <mystery+0x28> */ } while (ebp != arg); } /* 4c: 8b 04 24 mov (%esp),%eax */ eax = temp; /* 4f: 83 c4 04 add $0x4,%esp */ /* 52: 5b pop %ebx */ /* 53: 5e pop %esi */ /* 54: 5f pop %edi */ /* 55: 5d pop %ebp */ /* Matching the pushes and sub at the beginning */ /* 56: c3 ret */ /* The calling convention uses eax for the return value. */ return eax; }And here's just the C code:
int mystery1(int arg) { int eax = arg; int temp = 0x11e1a300; if (eax > 0) { int esi = 3; int ebx = 0x5f5e100; int ecx = 1; int edx, ebp; do { int edi = ecx; ebp = ecx; edi *= esi; ecx++; edx = ebx; eax = ebx; edx >>= 31; ebx = -ebx; esi += 2; edi *= ecx; { long long dividend = (long long)edx << 32 | eax; eax = dividend / edi; edx = dividend % edi; } temp += eax; } while (ebp != arg); } eax = temp; return eax; }This still looks rather complicated, so let's try cleaning up and simplifying it in various ways:
@@ -8,23 +8,17 @@ int ebx = 0x5f5e100; int ecx = 1; - int edx, ebp; + int ebp; do { int edi = ecx; ebp = ecx; edi *= esi; ecx++; - edx = ebx; eax = ebx; - edx >>= 31; ebx = -ebx; esi += 2; edi *= ecx; - { - long long dividend = (long long)edx << 32 | eax; - eax = dividend / edi; - edx = dividend % edi; - } + eax /= edi; temp += eax; } while (ebp != arg); }
@@ -1,11 +1,11 @@ int mystery1(int arg) { int eax = arg; - int temp = 0x11e1a300; + int ebx = 100000000; + int temp = 3 * ebx; if (eax > 0) { int esi = 3; - int ebx = 0x5f5e100; int ecx = 1; int ebp;
@@ -1,15 +1,14 @@ int mystery1(int arg) { - int eax = arg; - int ebx = 100000000; - int temp = 3 * ebx; + int total = 3 * ebx; - if (eax > 0) { + if (arg > 0) { int esi = 3; int ecx = 1; int ebp; do { + int eax; int edi = ecx; ebp = ecx; edi *= esi; @@ -19,9 +18,8 @@ esi += 2; edi *= ecx; eax /= edi; - temp += eax; + total += eax; } while (ebp != arg); } - eax = temp; - return eax; + return total; }
@@ -4,19 +4,19 @@ if (arg > 0) { int esi = 3; - int ecx = 1; + int i = 1; int ebp; do { int eax; - int edi = ecx; - ebp = ecx; + int edi = i; + ebp = i; edi *= esi; - ecx++; + i++; eax = ebx; ebx = -ebx; esi += 2; - edi *= ecx; + edi *= i; eax /= edi; total += eax; } while (ebp != arg);
@@ -9,16 +9,14 @@ int ebp; do { int eax; - int edi = i; + int divisor = i * esi * (i + 1); ebp = i; - edi *= esi; - i++; eax = ebx; ebx = -ebx; esi += 2; - edi *= i; - eax /= edi; + eax /= divisor; total += eax; + i++; } while (ebp != arg); } return total;
@@ -8,14 +8,11 @@ int ebp; do { - int eax; int divisor = i * esi * (i + 1); ebp = i; - eax = ebx; - ebx = -ebx; esi += 2; - eax /= divisor; - total += eax; + total += ebx / divisor; + ebx = -ebx; i++; } while (ebp != arg); }
@@ -6,15 +6,13 @@ int esi = 3; int i = 1; - int ebp; do { int divisor = i * esi * (i + 1); - ebp = i; esi += 2; total += ebx / divisor; ebx = -ebx; i++; - } while (ebp != arg); + } while (i - 1 != arg); } return total; }
@@ -3,12 +3,10 @@ int total = 3 * ebx; if (arg > 0) { - int esi = 3; int i = 1; do { - int divisor = i * esi * (i + 1); - esi += 2; + int divisor = i * (2*i + 1) * (i + 1); total += ebx / divisor; ebx = -ebx; i++;
if (cond) { do { ... } while (cond) }would be equivalent to:
while (cond) { ... }
The problem is that the two conditions arg > 0 and i - 1 != arg look rather different. But maybe they both came from the same condition that the compiler optimized. For the if condition, the compiler knows that the initial value of the loop counter is 1, so 0 might be the optimized version of i - 1. Similarly when we're inside the loop the compiler knows that i-1 can never be greater than arg, because it was initialized to be less than or equal and we stop after the iteration when they're equal. So it looks like both conditions are equivalent to i - 1 < arg, or more idiomatically i <= arg.
However there's a subtle problem lurking here, which you would probably only notice if you were paranoid about integer overflow attacks, or if you were testing the function with the largest possible positive integer (INT_MAX) as an argument. If we write the condition as i <= arg, then when arg is equal to INT_MAX, the loop's exit condition will never be satisfied. (The program won't actually loop forever, because when i loops around back near zero, you'll get a divide by zero crash.) You might think you would be safe if you kept the condition as the less natural-looking i - 1 < arg, since that condition looks like it should still be false when arg is INT_MAX and i is INT_MAX + 1. (2's complement arithmetic is associative, so (x + 1) - 1 = x for all values of x.) Unfortunately, the C compiler is still allowed to "optimize" the condition i - 1 < arg into the condition i <= arg, even though they have the differing behavior we just described. The reason is that the C standard says that overflow of a signed integer causes undefined behavior. This means that a program is entitled to do whatever it wants if an overflow might occur, or equivalently, optimize as if the situation triggering undefined behavior could never occur. It's the programmer's responsibility to ensure that the undefined behavior can never occur.
Undefined behavior has turned out to be a ongoing source of friction between C programmers and compiler makers, since programmers' intuitions about how the compiler works often differ from what the standard allows. In particular problems tend to arise when compilers become more sophisticated at optimization: optimizations that take better advantage of the undefined-behavior rules can make some previously-working programs run faster, and other previously-working programs crash in hard-to-debug ways. John Regehr's blog has some lucid discussions of these issues.
For now, we can work around this behavior by adding an extra condition on i to prevent overflow.
@@ -2,15 +2,12 @@ int ebx = 100000000; int total = 3 * ebx; - if (arg > 0) { int i = 1; - - do { + while (i < 0x7fffffff && i <= arg) { int divisor = i * (2*i + 1) * (i + 1); total += ebx / divisor; ebx = -ebx; i++; - } while (i - 1 != arg); } return total; }
@@ -2,12 +2,11 @@ int ebx = 100000000; int total = 3 * ebx; - int i = 1; - while (i < 0x7fffffff && i - 1 < arg) { + int i; + for (i = 1; i < 0x7fffffff && i - 1 < arg; i++) { int divisor = i * (2*i + 1) * (i + 1); total += ebx / divisor; ebx = -ebx; - i++; } return total; }
@@ -2,9 +2,9 @@ int ebx = 100000000; int total = 3 * ebx; - int i; - for (i = 1; i < 0x7fffffff && i - 1 < arg; i++) { - int divisor = i * (2*i + 1) * (i + 1); + int j; + for (j = 0; j < arg; j++) { + int divisor = (j + 1) * (2*j + 3) * (j + 2); total += ebx / divisor; ebx = -ebx; }
int mystery1(int terms) { int unit = 100000000; int total = 3 * unit; int j; for (j = 0; j < terms; j++) { int divisor = (j + 1) * (2*j + 3) * (j + 2); total += unit / divisor; unit = -unit; } return total; }For comparison, here's the code as the instructor originally wrote it:
int mystery(int steps) { int m = 100000000; int sum = 3*m; int i; for (i = 0; i < steps; i++) { int denom = (i+1) * (2*i+3) * (i+2); int change = m / denom; sum += change; m = -m; } return sum; }