A typical and subtle problem in C is the so called off-by-one bug. Consider the following example:
A simple buggy program
#include<stdio.h> #include<string.h> #define PWD "s2d821RT" #define BUFSIZE 9 // 8 chars + NULL int checkpassword() { int auth = 0; // authentication flag: 0 fail, 1 success char str[BUFSIZE] = { 0 }; int i = 0; printf("&auth = %p, str = %p, &i = %p\n",&auth,str,&i); printf("Insert password: "); fflush(stdout); // inputs the password for (i=0;(str[i] = getchar())!='\n' && i<BUFSIZE;i++); if (str[i]=='\n') str[i]='\0'; // removes newline if (strcmp(str, PWD) == 0) auth = 1; // password is valid printf("auth = %08x, str = %s, i = %d\n",auth,str,i); return auth; } int main(int argc, char *argv[]) { if (checkpassword()) printf("Authenticated!\n"); else printf("Wrong password!\n"); }
Explanation:
- the program checks if a user typed password is equal to the intended one (hardcoded in the program for simplicity:
PWD = "s2d821RT"
) - user input is read into buffer
str[BUFSIZE]
using the for loop
for (i=0;(str[i] = getchar())!='\n' && i<BUFSIZE;i++);
which reads a single char usinggetchar()
and assign it tostr[i]
until newline is read or buffer capacity is reached - the above loop contains an off-by-one bug: the check on the index
i
should be done before the assignmentstr[i] = getchar()
so that i never overflows. The correct loop would be:
for (i=0; i<BUFSIZE && (str[i] = getchar())!='\n';i++);
- an overflow of one byte is possible
gcc rearranges variables so that arrays are after non-arrays. Thus, to observe the effect of the overflow we disable this protection through –no-stack-protector
# gcc examples/offbyone.c -o offbyone --no-stack-protector #
The program prints the address of the three variables on the stack: auth
, str
, i
. If we run the program we can see that auth
is right after str
. In fact, BUFSIZE
is exactly 9 bytes:
# ./offbyone &auth = 0x7fff5be79c0c, str = 0x7fff5be79c03, &i = 0x7fff5be79bfc Insert password: ^C # python -c "print 0x7fff5be79c0c - 0x7fff5be79c03" 9
Notice also that address space is randomized. If we re-execute we get different addresses but the same offset:
# ./offbyone &auth = 0x7fff7c718ffc, str = 0x7fff7c718ff3, &i = 0x7fff7c718fec Insert password: ^C # python -c "print 0x7fff7c718ffc - 0x7fff7c718ff3" 9
The following is the expected behaviour (correct and wrong passwords):
# ./offbyone &auth = 0x7ffe139232cc, str = 0x7ffe139232c3, &i = 0x7ffe139232bc Insert password: s2d821RT auth = 00000001, str = s2d821RT, i = 8 Authenticated! # ./offbyone &auth = 0x7ffc5603f73c, str = 0x7ffc5603f733, &i = 0x7ffc5603f72c Insert password: AAAAAAAA auth = 00000000, str = AAAAAAAA, i = 8 Wrong password!
Notice that we print auth in hexadecimal notation: 00 00 00 01 in case of correct authentication and 00 00 00 00 in case of failure
Off-by-one overflow
We said that the for loop overflows by one byte. Buffer is 9 bytes so if we insert 10 chars we should overflow variable auth
, which is right after str
.
# ./offbyone &auth = 0x7ffc00ffa3cc, str = 0x7ffc00ffa3c3, &i = 0x7ffc00ffa3bc Insert password: AAAAAAAAAA auth = 00000041, str = AAAAAAAAAA, i = 9 Authenticated!
Explanation:
- the last A overwrites the first byte of auth. Recall that, because of little-endianness, bytes are stored in reversed order (least-significant first). Thus A overwrites the least significant byte of
auth
- since A’s ASCII code is
0x41
we obtain thatauth
is0x41
(as shown in the output) - recall that in C anything different from 0 (false) is true. Thus, authentication is successful!
Exercise
Consider the following variant of the previous program in which the authentication flag is a single byte that is set to 0x00 and 0x01 (bytes 0 and 1) to represent failure and success (modified lines are marked).
Similarly to the previous example, your task is to authenticate exploiting the off-by-one overflow. Use the binary bin/offbyone2
. Once authenticated you will obtain the the password for Task 2!
How to pass RAW bytes to a program
In order to pass byte values use the echo
command with -e
option and \x
notation for bytes, as follows:
# echo -e '\x41\x42\x43\x44' ABCD
Use a pipe to pass the input to the program:
# echo -e '\x41\x42\x43\x44' | bin/offbyone2 &auth = 0x7ffcaa1ee8df, str = 0x7ffcaa1ee8d6, &i = 0x7ffcaa1ee8d0 Insert password: auth = 00, str = ABCD, i = 4 Wrong password!
For non-printable chars hexdump
can help visualising what you are generating with echo:
# echo -e '\x41\x42\x43\x44\x04\x05\x06\x07' | hexdump -C 00000000 41 42 43 44 04 05 06 07 0a |ABCD.....| 00000009
Notice that 0x0a
is the ASCII code for newline \n
. Non-printable bytes appear as dots on the right.