Main navigation | Main content
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
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.
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?
"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.
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:
(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.
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:
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.