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
ishould 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
0x41we obtain thatauthis0x41(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.