M2G Posted June 20, 2012 Report Posted June 20, 2012 1: Introduction 1.1 What is an integer? 1.2 What is an integer overflow? 1.3 Why can they be dangerous?2: Integer overflows 2.1 Widthness overflows 2.1.1 Exploiting 2.2 Arithmetic overflows 2.2.1 Exploiting3: Signedness bugs 3.1 What do they look like? 3.1.1 Exploiting 3.2 Signedness bugs caused by integer overflows4: Real world examples 4.1 Integer overflows 4.2 Signedness bugs--[ 1.0 IntroductionIn this paper I'm going to describe two classes of programming bugs whichcan sometimes allow a malicious user to modify the execution path of anaffected process. Both of these classes of bug work by causing variablesto contain unexpected values, and so are not as "direct" as classes whichoverwrite memory, e.g. buffer overflows or format strings. All theexamples given in the paper are in C, so a basic familiarity with C isassumed. A knowledge of how integers are stored in memory is also useful,but not essential.----[ 1.1 What is an integer?An integer, in the context of computing, is a variable capable ofrepresenting a real number with no fractional part. Integers are typicallythe same size as a pointer on the system they are compiled on (i.e. on a 32bit system, such as i386, an integer is 32 bits long, on a 64 bit system,such as SPARC, an integer is 64 bits long). Some compilers don't useintegers and pointers of the same size however, so for the sake ofsimplicity all the examples refer to a 32 bit system with 32 bit integers,longs and pointers.Integers, like all variables are just regions of memory. When we talkabout integers, we usually represent them in decimal, as that is thenumbering system humans are most used to. Computers, being digital, cannotdeal with decimal, so internally to the computer integers are stored inbinary. Binary is another system of representing numbers which uses onlytwo numerals, 1 and 0, as opposed to the ten numerals used in decimal. Aswell as binary and decimal, hexadecimal (base sixteen) is often used incomputing as it is very easy to convert between binary and hexadecimal.Since it is often necessary to store negative numbers, there needs to be amechanism to represent negative numbers using only binary. The way this isaccomplished is by using the most significant bit (MSB) of a variable todetermine the sign: if the MSB is set to 1, the variable is interpreted asnegative; if it is set to 0, the variable is positive. This can cause someconfusion, as will be explained in the section on signedness bugs, becausenot all variables are signed, meaning they do not all use the MSB todetermine whether they are positive or negative. These variable are knownas unsigned and can only be assigned positive values, whereas variableswhich can be either positive or negative are called unsigned.----[ 1.2 What is an integer overflow?Since an integer is a fixed size (32 bits for the purposes of this paper),there is a fixed maximum value it can store. When an attempt is made tostore a value greater than this maximum value it is known as an integeroverflow. The ISO C99 standard says that an integer overflow causes"undefined behaviour", meaning that compilers conforming to the standardmay do anything they like from completely ignoring the overflow to abortingthe program. Most compilers seem to ignore the overflow, resulting in anunexpected or erroneous result being stored.----[ 1.3 Why can they be dangerous?Integer overflows cannot be detected after they have happened, so there isnot way for an application to tell if a result it has calculated previouslyis in fact correct. This can get dangerous if the calculation has to dowith the size of a buffer or how far into an array to index. Of coursemost integer overflows are not exploitable because memory is not beingdirectly overwritten, but sometimes they can lead to other classes of bugs,frequently buffer overflows. As well as this, integer overflows can bedifficult to spot, so even well audited code can spring surprises.--[ 2.0 Integer overflowsSo what happens when an integer overflow does happen? ISO C99 has this tosay: "A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type."NB: modulo arithmetic involves dividing two numbers and taking theremainder,e.g. 10 modulo 5 = 0 11 modulo 5 = 1so reducing a large value modulo (MAXINT + 1) can be seen as discarding theportion of the value which cannot fit into an integer and keeping the rest.In C, the modulo operator is a % sign.</NB>This is a bit wordy, so maybe an example will better demonstrate thetypical "undefined behaviour":We have two unsigned integers, a and b, both of which are 32 bits long. Weassign to a the maximum value a 32 bit integer can hold, and to b we assign1. We add a and b together and store the result in a third unsigned 32 bitinteger called r: a = 0xffffffff b = 0x1 r = a + bNow, since the result of the addition cannot be represented using 32 bits,the result, in accordance with the ISO standard, is reduced modulo0x100000000. r = (0xffffffff + 0x1) % 0x100000000 r = (0x100000000) % 0x100000000 = 0Reducing the result using modulo arithmetic basically ensures that only thelowest 32 bits of the result are used, so integer overflows cause theresult to be truncated to a size that can be represented by the variable.This is often called a "wrap around", as the result appears to wrap aroundto 0.----[ 2.1 Widthness overflowsSo an integer overflow is the result of attempting to store a value in avariable which is too small to hold it. The simplest example of this canbe demonstrated by simply assigning the contents of large variable to asmaller one: /* ex1.c - loss of precision */ #include <stdio.h> int main(void){ int l; short s; char c; l = 0xdeadbeef; s = l; c = l; printf("l = 0x%x (%d bits)\n", l, sizeof(l) * 8); printf("s = 0x%x (%d bits)\n", s, sizeof(s) * 8); printf("c = 0x%x (%d bits)\n", c, sizeof(c) * 8); return 0; } /* EOF */The output of which looks like this: nova:signed {48} ./ex1 l = 0xdeadbeef (32 bits) s = 0xffffbeef (16 bits) c = 0xffffffef (8 bits)Since each assignment causes the bounds of the values that can be stored ineach type to be exceeded, the value is truncated so that it can fit in thevariable it is assigned to.It is worth mentioning integer promotion here. When a calculationinvolving operands of different sizes is performed, the smaller operand is"promoted" to the size of the larger one. The calculation is thenperformed with these promoted sizes and, if the result is to be stored inthe smaller variable, the result is truncated to the smaller size again.For example: int i; short s; s = i;A calculation is being performed with different sized operands here. Whathappens is that the variable s is promoted to an int (32 bits long), thenthe contents of i is copied into the new promoted s. After this, thecontents of the promoted variable are "demoted" back to 16 bits in order tobe saved in s. This demotion can cause the result to be truncated if it isgreater than the maximum value s can hold.------[ 2.1.1 ExploitingInteger overflows are not like most common bug classes. They do not allowdirect overwriting of memory or direct execution flow control, but are muchmore subtle. The root of the problem lies in the fact that there is no wayfor a process to check the result of a computation after it has happened,so there may be a discrepancy between the stored result and the correctresult. Because of this, most integer overflows are not actuallyexploitable. Even so, in certain cases it is possible to force a crucialvariable to contain an erroneous value, and this can lead to problems laterin the code.Because of the subtlety of these bugs, there is a huge number of situationsin which they can be exploited, so I will not attempt to cover allexploitable conditions. Instead, I will provide examples of somesituations which are exploitable, in the hope of inspiring the reader intheir own research Example 1: /* width1.c - exploiting a trivial widthness bug */ #include <stdio.h> #include <string.h> int main(int argc, char *argv[]){ unsigned short s; int i; char buf[80]; if(argc < 3){ return -1; } i = atoi(argv[1]); s = i; if(s >= 80){ /* [w1] */ printf("Oh no you don't!\n"); return -1; } printf("s = %d\n", s); memcpy(buf, argv[2], i); buf[i] = '\0'; printf("%s\n", buf); return 0; }While a construct like this would probably never show up in real life code,it serves well as an example. Take a look at the following inputs: nova:signed {100} ./width1 5 hello s = 5 hello nova:signed {101} ./width1 80 hello Oh no you don't! nova:signed {102} ./width1 65536 hello s = 0 Segmentation fault (core dumped)The length argument is taken from the command line and held in the integeri. When this value is transferred into the short integer s, it istruncated if the value is too great to fit into s (i.e. if the value isgreater than 65535). Because of this, it is possible to bypass the boundscheck at [w1] and overflow the buffer. After this, standard stack smashingtechniques can be used to exploit the process.----[ 2.2 Arithmetic overflowsAs shown in section 2.0, if an attempt is made to store a value in aninteger which is greater than the maximum value the integer can hold, thevalue will be truncated. If the stored value is the result of anarithmetic operation, any part of the program which later uses the resultwill run incorrectly as the result of the arithmetic being incorrect.Consider this example demonstrating the wrap around shown earlier: /* ex2.c - an integer overflow */ #include <stdio.h> int main(void){ unsigned int num = 0xffffffff; printf("num is %d bits long\n", sizeof(num) * 8); printf("num = 0x%x\n", num); printf("num + 1 = 0x%x\n", num + 1); return 0; } /* EOF */The output of this program looks like this: nova:signed {4} ./ex2 num is 32 bits long num = 0xffffffff num + 1 = 0x0Note:The astute reader will have noticed that 0xffffffff is decimal -1, so itappears that we're just doing1 + (-1) = 0Whilst this is one way at looking at what's going on, it may cause someconfusion since the variable num is unsigned and therefore all arithmeticdone on it will be unsigned. As it happens, a lot of signed arithmeticdepends on integer overflows, as the following demonstrates (assume bothoperands are 32 bit variables):-700 + 800 = 1000xfffffd44 + 0x320 = 0x100000064Since the result of the addition exceeds the range of the variable, thelowest 32 bits are used as the result. These low 32 bits are 0x64, whichis equal to decimal 100.</note>Since an integer is signed by default, an integer overflow can cause achange in signedness which can often have interesting effects on subsequentcode. Consider the following example: /* ex3.c - change of signedness */ #include <stdio.h> int main(void){ int l; l = 0x7fffffff; printf("l = %d (0x%x)\n", l, l); printf("l + 1 = %d (0x%x)\n", l + 1 , l + 1); return 0; } /* EOF */The output of which is: nova:signed {38} ./ex3 l = 2147483647 (0x7fffffff) l + 1 = -2147483648 (0x80000000)Here the integer is initialised with the highest positive value a signedlong integer can hold. When it is incremented, the most significant bit(indicating signedness) is set and the integer is interpreted as beingnegative.Addition is not the only arithmetic operation which can cause an integer tooverflow. Almost any operation which changes the value of a variable cancause an overflow, as demonstrated in the following example: /* ex4.c - various arithmetic overflows */ #include <stdio.h> int main(void){ int l, x; l = 0x40000000; printf("l = %d (0x%x)\n", l, l); x = l + 0xc0000000; printf("l + 0xc0000000 = %d (0x%x)\n", x, x); x = l * 0x4; printf("l * 0x4 = %d (0x%x)\n", x, x); x = l - 0xffffffff; printf("l - 0xffffffff = %d (0x%x)\n", x, x); return 0; } /* EOF */Output: nova:signed {55} ./ex4 l = 1073741824 (0x40000000) l + 0xc0000000 = 0 (0x0) l * 0x4 = 0 (0x0) l - 0xffffffff = 1073741825 (0x40000001)The addition is causing an overflow in exactly the same way as the firstexample, and so is the multiplication, although it may seem different. Inboth cases the result of the arithmetic is too great to fit in an integer,so it is reduced as described above. The subtraction is slightlydifferent, as it is causing an underflow rather than an overflow: anattempt is made to store a value lower than the minimum value the integercan hold, causing a wrap around. In this way we are able to force anaddition to subtract, a multiplication to divide or a subtraction to add.------[ 2.2.1 ExploitingOne of the most common ways arithmetic overflows can be exploited is when acalculation is made about how large a buffer must be allocated. Often aprogram must allocate space for an array of objects, so it uses themalloc(3) or calloc(3) routines to reserve the space and calculates howmuch space is needed by multiplying the number of elements by the size ofan object. As has been previously shown, if we are able to control eitherof these operands (number of elements or object size) we may be able tomis-size the buffer, as the following code fragment shows: int myfunction(int *array, int len){ int *myarray, i; myarray = malloc(len * sizeof(int)); /* [1] */ if(myarray == NULL){ return -1; } for(i = 0; i < len; i++){ /* [2] */ myarray[i] = array[i]; } return myarray; }This seemingly innocent function could bring about the downfall of a systemdue to its lack of checking of the len parameter. The multiplication at[1] can be made to overflow by supplying a high enough value for len, so wecan force the buffer to be any length we choose. By choosing a suitablevalue for len, we can cause the loop at [2] to write past the end of themyarray buffer, resulting in a heap overflow. This could be leveraged intoexecuting arbitrary code on certain implementations by overwriting malloccontrol structures, but that is beyond the scope of this article.Another example: int catvars(char *buf1, char *buf2, unsigned int len1, unsigned int len2){ char mybuf[256]; if((len1 + len2) > 256){ /* [3] */ return -1; } memcpy(mybuf, buf1, len1); /* [4] */ memcpy(mybuf + len1, buf2, len2); do_some_stuff(mybuf); return 0; }In this example, the check at [3] can be bypassed by using suitable valuesfor len1 and len2 that will cause the addition to overflow and wrap aroundto a low number. For example, the following values: len1 = 0x104 len2 = 0xfffffffcwhen added together would result in a wrap around with a result of 0x100(decimal 256). This would pass the check at [3], then the memcpy(3)'s at[4] would copy data well past the end of the buffer.--[ 3 Signedness BugsSignedness bugs occur when an unsigned variable is interpreted as signed,or when a signed variable is interpreted as unsigned. This type ofbehaviour can happen because internally to the computer, there is nodistinction between the way signed and unsigned variables are stored.Recently, several signedness bugs showed up in the FreeBSD and OpenBSDkernels, so there are many examples readily available.----[ 3.1 What do they look like?Signedness bugs can take a variety of forms, but some of the things to lookout for are:* signed integers being used in comparisons* signed integers being used in arithmetic* unsigned integers being compared to signed integersHere is classic example of a signedness bug: int copy_something(char *buf, int len){ char kbuf[800]; if(len > sizeof(kbuf)){ /* [1] */ return -1; } return memcpy(kbuf, buf, len); /* [2] */ }The problem here is that memcpy takes an unsigned int as the len parameter,but the bounds check performed before the memcpy is done using signedintegers. By passing a negative value for len, it is possible to pass thecheck at [1], but then in the call to memcpy at [2], len will be interpetedas a huge unsigned value, causing memory to be overwritten well past theend of the buffer kbuf.Another problem that can stem from signed/unsigned confusion occurs whenarithmetic is performed. Consider the following example: int table[800]; int insert_in_table(int val, int pos){ if(pos > sizeof(table) / sizeof(int)){ return -1; } table[pos] = val; return 0; }Since the line table[pos] = val;is equivalent to *(table + (pos * sizeof(int))) = val;we can see that the problem here is that the code does not expect anegative operand for the addition: it expects (table + pos) to be greaterthan table, so providing a negative value for pos causes a situation whichthe program does not expect and can therefore not deal with.------[ 3.1.1 ExploitingThis class of bug can be problematic to exploit, due to the fact thatsigned integers, when interpreted as unsigned, tend to be huge. Forexample, -1 when represented in hexadecimal is 0xffffffff. Wheninterpreted as unsiged, this becomes the greatest value it is possible torepresent in an integer (4,294,967,295), so if this value is passed tomempcpy as the len parameter (for example), memcpy will attempt to copy 4GBof data to the destination buffer. Obviously this is likely to cause asegfault or, if not, to trash a large amount of the stack or heap.Sometimes it is possible to get around this problem by passing a very lowvalue for the source address and hope, but this is not always possible.----[ 3.2 Signedness bugs caused by integer overflowsSometimes, it is possible to overflow an integer so that it wraps around toa negative number. Since the application is unlikely to expect such avalue, it may be possible to trigger a signedness bug as described above.An example of this type of bug could look like this: int get_two_vars(int sock, char *out, int len){ char buf1[512], buf2[512]; unsigned int size1, size2; int size; if(recv(sock, buf1, sizeof(buf1), 0) < 0){ return -1; } if(recv(sock, buf2, sizeof(buf2), 0) < 0){ return -1; } /* packet begins with length information */ memcpy(&size1, buf1, sizeof(int)); memcpy(&size2, buf2, sizeof(int)); size = size1 + size2; /* [1] */ if(size > len){ /* [2] */ return -1; } memcpy(out, buf1, size1); memcpy(out + size1, buf2, size2); return size; }This example shows what can sometimes happen in network daemons, especiallywhen length information is passed as part of the packet (in other words, itis supplied by an untrusted user). The addition at [1], used to check thatthe data does not exceed the bounds of the output buffer, can be abused bysetting size1 and size2 to values that will cause the size variable to wraparound to a negative value. Example values could be: size1 = 0x7fffffff size2 = 0x7fffffff (0x7fffffff + 0x7fffffff = 0xfffffffe (-2)).When this happens, the bounds check at [2] passes, and a lot more of theout buffer can be written to than was intended (in fact, arbitrary memorycan be written to, as the (out + size1) dest parameter in the second memcpycall allows us to get to any location in memory).These bugs can be exploited in exactly the same way as regular signednessbugs and have the same problems associated with them - i.e. negative valuestranslate to huge positive values, which can easily cause segfaults.--[ 4 Real world examplesThere are many real world applications containing integer overflows andsignedness bugs, particularly network daemons and, frequently, in operatingsystem kernels.----[ 4.1 Integer overflowsThis (non-exploitable) example was taken from a security module for linux.This code runs in the kernel context: int rsbac_acl_sys_group(enum rsbac_acl_group_syscall_type_t call, union rsbac_acl_group_syscall_arg_t arg) { ... switch(call) { case ACLGS_get_group_members: if( (arg.get_group_members.maxnum <= 0) /* [A] */ || !arg.get_group_members.group ) { ... rsbac_uid_t * user_array; rsbac_time_t * ttl_array; user_array = vmalloc(sizeof(*user_array) * arg.get_group_members.maxnum); /* [B] */ if(!user_array) return -RSBAC_ENOMEM; ttl_array = vmalloc(sizeof(*ttl_array) * arg.get_group_members.maxnum); /* [C] */ if(!ttl_array) { vfree(user_array); return -RSBAC_ENOMEM; } err = rsbac_acl_get_group_members(arg.get_group_members.group, user_array, ttl_array, arg.get_group_members.max num); ... }In this example, the bounds checking at [A] is not sufficient to preventthe integer overflows at and [C]. By passing a high enough (i.e.greater than 0xffffffff / 4) value for arg.get_group_members.maxnum, wecan cause the multiplications at and [C] to overflow and force thebuffers ttl_array and user_array to be smaller than the applicationexpects. Since rsbac_acl_get_group_members copies user controlled datato these buffers, it is possible to write past the end of the user_arrayand ttl_array buffers. In this case, the application used vmalloc() toallocate the buffers, so an attempt to write past the end of the bufferswill simply raise an error, so it cannot be exploited. Even so, itprovides an example of what these bugs can look like in real code.Another example of a recent real world integer overflow vulnerabilitywas the problem in the XDR RPC library (discovered by ISS X-Force). In thiscase, user supplied data was used in the calculation of the size of adynamically allocated buffer which was filled with user supplied data. Thevulnerable code was this: bool_t xdr_array (xdrs, addrp, sizep, maxsize, elsize, elproc) XDR *xdrs; caddr_t *addrp; /* array pointer */ u_int *sizep; /* number of elements */ u_int maxsize; /* max numberof elements */ u_int elsize; /* size in bytes of each element */ xdrproc_t elproc; /* xdr routine to handle each element */ { u_int i; caddr_t target = *addrp; u_int c; /* the actual element count */ bool_t stat = TRUE; u_int nodesize; ... c = *sizep; if ((c > maxsize) && (xdrs->x_op != XDR_FREE)) { return FALSE; } nodesize = c * elsize; /* [1] */ ... *addrp = target = mem_alloc (nodesize); /* [2] */ ... for (i = 0; (i < c) && stat; i++) { stat = (*elproc) (xdrs, target, LASTUNSIGNED); /* [3] */ target += elsize; }As you can see, by supplying large values for elsize and c (sizep), itwas possible to cause the multiplication at [1] to overflow and causenodesize to be much smaller than the application expected. Sincenodesize was then used to allocate a buffer at [2], the buffer could bemis-sized leading to a heap overflow at [3]. For more information on thishole, see the CERT advisory listed in the appendix.----[ 4.2 Signedness bugsRecently, several signedness bugs were brought to light in the freebsdkernel. These allowed large portions of kernel memory to be read bypassingnegative length paramters to various syscalls. The getpeername(2) functionhad such a problem and looked like this: static int getpeername1(p, uap, compat) struct proc *p; register struct getpeername_args /* { int fdes; caddr_t asa; int *alen; } */ *uap; int compat; { struct file *fp; register struct socket *so; struct sockaddr *sa; int len, error; ... error = copyin((caddr_t)uap->alen, (caddr_t)&len, sizeof (len)); if (error) { fdrop(fp, p); return (error); } ... len = MIN(len, sa->sa_len); /* [1] */ error = copyout(sa, (caddr_t)uap->asa, (u_int)len); if (error) goto bad; gotnothing: error = copyout((caddr_t)&len, (caddr_t)uap->alen, sizeof (len)); bad: if (sa) FREE(sa, M_SONAME); fdrop(fp, p); return (error); }This is a classic example of a signedness bug - the check at [1] did nottake into account the fact that len could be negative, in which case theMIN macro would always return len. When this negative len parameter waspassed to copyout, it was interpretted as a huge positive integer whichcaused copyout to copy up to 4GB of kernel memory to user space.--[ ConclusionInteger overflows can be extremely dangerous, partly because it isimpossible to detect them after they have happened. If an integer overflowtakes place, the application cannot know that the calculation it hasperformed is incorrect, and it will continue under the assumption that itis. Even though they can be difficult to exploit, and frequently cannot beexploited at all, they can cause unepected behaviour, which is never a goodthing in a secure system.--[ AppendixCERT advisory on the XDR bug:CERT Advisory CA-2002-25 Integer Overflow In XDR LibraryFreeBSD advisory: Advisory: Boundary checking errors involving signed integers|=[ EOF ]=---------------------------------------------------------------=|SursaMai uitati-va pe phrack.com pentru ca aveti de invatat chestii interesante. 1 Quote