Task 1: off-by-one

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 using getchar()  and assign it to str[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 assignment str[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 that auth is 0x41 (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).

#include<stdio.h>
#include<string.h>

#define PWD "s2d821RT"
#define BUFSIZE 9 // 15 chars + NULL
#define AUTH   '\x01'  // byte 0x01
#define NOAUTH '\x00'  // byte 0x00

int checkpassword() {
  char auth = NOAUTH; // authentication flag: NOAUTH fail, AUTH 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 = AUTH; // password is valid
  printf("auth = %02x, str = %s, i = %d\n",auth,str,i);
  return auth;
}

int main(int argc, char *argv[]) {
  if (checkpassword() == AUTH)
    printf("Authenticated!\n");
  else
    printf("Wrong password!\n");
}

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.