We will use this lab archive throughout the lab.
Please download the lab archive an then unpack it using the commands below:
student@asgard:~$ wget http://elf.cs.pub.ro/oss/res/labs/lab-12.tar.gz student@asgard:~$ tar xzf lab-12.tar.gz
After unpacking we will get the lab-12/ folder that we will use for the lab:
student@asgard:~$ cd lab-12/ student@asgard:~/lab-12$ ls -F 1-overflow/ 2-comparison/ 3-sts7/ 4-sts2/ 5-sts2_alt/
In order to improve the Computer Network Security course, your opinions and suggestions are important to us. The feedback is anonymous and the results will only become visible after the final exam. You will find the link to the feedback form on the main page of the curs.pub.ro instance for your master's program CNS class (either PDCS or SRIC). It's not in the meta-course for all students.
This lab illustrates how improper handling of integers can lead to bugs, and worse, to vulnerabilities and security issues. By integers we mean numeric datatypes without a fractional component, on which we perform arithmetic operations (addition, subtraction, division etc.). Some examples of integer types defined by the C90 standard are int, long, short and char. The following aspects are of interest to us when it comes to correct C programming using integers:
sizeof(char) is different from sizeof(short). Furthermore, sizeof(long) == sizeof(int) might or might not be true depending on the architecture. For example this does not hold on x86_64. The C99 standard introduces fixed-length types such as int8_t or uint32_t, whose widths are architecture-independent.signed or unsigned keywords, denoting the possibility of having negative numbers (types such as int, char etc. are considered signed by default). Note that since numbers are represented in two's complement on x86, negative numbers have the most significant bit 1. That is, the most significant bit is used as a sign bit, therefore, even though sizeof(int) == sizeof(unsigned int), the representation for the two types isn't the same. For example, 0 <= unsigned char x1 <= 255, while -128 <= signed char x2 <= 127.unsigned long i = -1; is a valid C statement! This is especially important for library functions such as atoi, which returns an int, i.e. a signed number.
Note: object sizes such as sizeof(int) have the type size_t, which is an unsigned integral type. It's recommended to use size_t for all operations involving object sizes.
Correct handling of integers is defined in chapter 4 of the CERT C Coding Standard. Some examples of real-life integer vulnerabilities were discovered using the KINT static analyzer.
For our first task, let's investigate how improper handling of integers can lead to undesired program behavior. We will see what happens when an implicit data type conversion takes place.
Please access the 1-overflow/ subfolder in the lab archive. It consists of three files: a Makefile, a C source code file dubbed overflow.c and the resulting executable (ELF) file on a 32-bit system overflow.
First take a look at the source code in overflow.c. The program receives two arguments num1 and num2 that are then intepreted to integers using atoi. Use a different terminal tab to try some sample runs of the program:
student@asgard:~/lab-12/1-overflow$ ./overflow 10 50 student@asgard:~/lab-12/1-overflow$ ./overflow 100 200 Input number too small. student@asgard:~/lab-12/1-overflow$ ./overflow 1500 1500 Input number too small.
Notice the output of the above runs. Inside the source code.and pay attention to the comparison lines (line 16, 22 and 29). Your first goal is to print the last string (on line 30)
if (x < 0 || y < 0) printf("Oh no, I feel exploited!\n");
That is, you want the same comparison happening on line 22 to be false, but it would be true on line 29, right after y has been added to x.
[1p] Find two arguments that result in the overflow program printing Oh no, I feel exploited!.
x and y are of signed char type, and their maximum value is 127. The Input number too large. message will never get printed.
x and y must be positive numbers. However, after the x += y; line, one of them turns negative, and it can only be x.
Think how you can turn a signed positive integer to a signed negative one by adding another positive integer. Think, integer overflow.
Your second task is to fix the program. Make sure that whatever input you feed to it, there won't be a situation where a signed positive integer turns into a negative integer simply by adding another positive integer to it. Any solution is OK as long as the program behaves correctly.
[1.5p] Update the overflow.c file accordigly, and then recompile it using
student@asgard:~/lab-12/1-overflow$ make
Then feed it different arguments and prove that now it behaves correctly.
char and returning a message when it doesn't. Note that overflows cause side effects on x86 (i.e. setting the overflow flag in the CPU), so your solution must avoid any overflows occuring in the code. Look through the CERT C Coding Standard for a compliant solution.
For our second task, let's investigate how comparison between signed and unsigned values results in unwelcomed program behavior.
Please access the 2-comparison/ subfolder in the lab archive. It consists of three files: a Makefile, a C source code file dubbed comparison.c and the resulting executable (ELF) file on a 32-bit system comparison.
First take a look at the source code in comparison.c. The program receives an argument x1 that is then intepreted as an integer using atoi. Use a different terminal tab to try some sample runs of the program:
student@asgard:~/lab-12/2-comparison$ ./comparison 10 0x0000000a student@asgard:~/lab-12/2-comparison$ ./comparison 100 0x00000064 student@asgard:~/lab-12/2-comparison$ ./comparison 1000 0x000003e8 student@asgard:~/lab-12/2-comparison$ ./comparison -10 0xfffffff6 student@asgard:~/lab-12/2-comparison$ ./comparison -100 0xffffff9c student@asgard:~/lab-12/2-comparison$ ./comparison -1000 0xfffffc18
Notice that even though we provide a negative argument the comparison on line 16 in the comparison.c source code file is false. Browse the program carefully and see why that happens.
x1 (a (signed) int type). When compared to x2 (an unsigned int type) an implicit conversion to the unsigned int type occurs, messing everything up.
The hexadecimal representation of x2 may be shown using a Python script:
student@asgard:~/lab-12/2-comparison$ python -c 'print hex(2147483649)' 0x80000001
[1p] Using this, provide the proper argument to the program such that the program will find the condition on line 16 to be true and print the message on line 17: That looks quite odd, doesn't it?.
x1 that would result in a true evaluation of the x1 < x2 comparison. Then create the signed integer in decimal.
[1.5p] Now, let's fix this. Without changing the data types of x1 and x2 (x1 is int and x2 is unsigned int), make sure the program behaves correctly, i.e. it will print out the message on line 17 whenever x1 is less than x2. The program semantics have to be the same.
student@asgard:~/lab-12/2-comparison$ ./comparison -10 0xfffffff6 That looks quite odd, doesn't it? student@asgard:~/lab-12/2-comparison$ ./comparison -100 0xffffff9c That looks quite odd, doesn't it? student@asgard:~/lab-12/2-comparison$ ./comparison -1000 0xfffffc18 That looks quite odd, doesn't it?
First, let's take a quick look through the source code:
int count = atoi(argv[1]); int buf[10]; if(count >= 10 ) return 1; memcpy(buf, argv[2], count * sizeof(int)); if(count == 0x574f4c46) { ... } ... return 0;
The program receives two arguments: a number which is stored into count as an integer and a string which is stored into buf, on the stack. Note that exactly count ints are stored into buf. At the same time, count must be less than 10. How can we exploit this?
First, notice that count is an int. What is the signature of memcpy? Let's have a look:
void * memcpy ( void * destination, const void * source, size_t num );
It seems that we're lucky, since the compiler will implicitly cast int to size_t and our value is multiplied by sizeof(int), which may result in an overflow. At the same time, we notice that the program doesn't check for negative values. The following short program adds a sign bit to the number 42 and prints it in hexadecimal and signed decimal:
#include <stdio.h> int main(void) { int x = 42 | 0x80000000; printf("%%x = %x\n%%d = %d\n", x, x); return 0; }
This way we can generate the offset of count and feed it to the program as argv[1].
To find out where count resides on the stack, we'll look at the objdump of level07. First, we'll look at the assembly generated by if (count >= 10) and by if(count == 0x574f4c46):
8048434: 89 45 f4 mov %eax,-0xc(%ebp) 8048437: 83 7d f4 09 cmpl $0x9,-0xc(%ebp) 804843b: 7e 09 jle 8048446 <main+0x32> ... 804845c: 8d 45 b8 lea -0x48(%ebp),%eax 804845f: 89 04 24 mov %eax,(%esp) 8048462: e8 cd fe ff ff call 8048334 <memcpy@plt> 8048467: 81 7d f4 46 4c 4f 57 cmpl $0x574f4c46,-0xc(%ebp)
We observe that count sits at $ebp - 0xc. We also see that buf is located at $ebp - 0x48. We also know that buf occupies 4 * 10 = 40 = 0x28 bytes on the stack. Therefore the layout of the stack looks something like the following:
$ebp - 0x00: oldebp (4 bytes) $ebp - 0x04: ... $ebp - 0x08: ... $ebp - 0x0c: count (4 bytes) $ebp - 0x10: ... ... $ebp - 0x20: buf[9] ... $ebp - 0x48: buf (40 bytes)
We can now compute the offset of count relative to buf:
$ echo $((0x48 - 0x0c)) 60
The program wants ints, not bytes, so we divide that by 4:
$ echo $(((0x48 - 0x0c) / 4)) 15
Using the info from the previous sections, we can pass a negative number that encodes a malformed size (size | sign bit) and overflow the buffer into count and write the value 0x574f4c46 into it.
level07
The main function is really short:
int main(int argc, char **argv) { puts("source code is available in level02.c\n"); if (argc != 3 || !atoi(argv[2])) return 1; signal(SIGFPE, catcher); return abs(atoi(argv[1])) / atoi(argv[2]); }
The program receives two parameters, argv[1] and argv[2], converted to numbers using atoi. It also sets the catcher function as a signal handler for SIGFPE. Our intention is thus causing a SIGFPE to enter catcher and obtain a shell.
The question becomes the following: how do we cause an erroneous arithmetic operation? Notice that we cannot cause a division by zero, due to the !atoi(argv[2]) sanity check.
Hint: this is more or less an architecture-dependent quirk. Take a second look on two's complement representation on x86 and a good look on how the x86 DIV instruction is specified. It's tricky, but not difficult!
level02.Similarly to the previous level, this is also more or less architecture dependent, or rather dependent on how floating-point numbers are specified. Since it's a bonus, you won't get any hints other than the fact that it's just plain tricky and it also relies on quirks.
Note: the program tries to execute /bin/sh with the -p flag (which would prevent bash from dropping privileges). If you get the following message:
sh: 0: Illegal option -p
then it's ok to recompile the program to run execve without passing -p.
level02_alt.