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
int
s 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 int
s, 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
.