Tut08: Logic Errors

In this tutorial, we will learn about three class of popular logic bugs (i.e., non-memory safety bugs): an integer overflow, a race condition, and a command injection.

1. Integer overflows

Let's first take a look on crackme0x00.c:

void start() {
  int passwd;
  printf("IOLI Crackme Level 0x00\n");
  printf("Password: ");
  scanf("%d", &passwd);
  if (absolute(passwd) < 0) {
    printf("Password OK :)\n");
    round2();
  } else {
    printf("Invalid Password!\n");
  }
}

It is asking for a password that its absolute value is less than zero: absolute(passwd) < 0. Note that the absolute() function is nothing but to convert any negative integer to its positive form by negativing the provided integer, as in the standard library:

// @stdlib/abs.c
/* Return the absolute value of I.  */
int absolute(int i) {
  if (i<0)
    return -i;
  else
    return i;
}

Is this mathematically feasible? No. However, as our computer can only express a small part of the integer space: e.g., a 32-bit register can express 2^32 numbers of integers (more on later), this password check can be bypassed!

Let's look at the actual instructions of absolute():

0000000000402118 <absolute>:
  402118:       55                      push   rbp
  402119:       48 89 e5                mov    rbp,rsp
  40211c:       89 7d fc                mov    DWORD PTR [rbp-0x4],edi
  40211f:       83 7d fc 00             cmp    DWORD PTR [rbp-0x4],0x0
  402123:       79 07                   jns    40212c <absolute+0x14>
  402125:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  402128:       f7 d8                   neg    eax
  40212a:       eb 03                   jmp    40212f <absolute+0x17>
  40212c:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  40212f:       5d                      pop    rbp
  402130:       c3                      ret

How does the machine perform the neg eax instruction? Unlike the arithmetic way of multiplying -1 to the multiplicand, the machine simply flips each bit (from 0 to 1 and vice versa) and adds one to the result. In two's complement representation, it happens to serve as a negation operation for most of the integers that a register can express.

For example, 0x00000001 is a positive integer 1. If we flip every bit, it becomes 0b1111....1110, which is 0xfffffffe in a hexadecimal representation, and adding one to it, we get 0xffffffff. This is the two's complement representation of -1.


Two's complement

The biggest positive integer (i.e., INT_MAX) a 32-bit register can hold is 0x7fffffff, which is 2147483647. The smallest negative integer (i.e., INT_MIN) is 0x80000000, which is -2147483648 in decimal. One property that you might notice in two's complement is its asymmetry in representing the range of negative and positive integers: -2147483648 (INT_MIN) to 2147483647 (INT_MAX).

What's the arithmetic value of -INT_MAX? It is -2147483647. What about -INT_MIN? It is 2147483648, but it is bigger than INT_MAX! Now, let's approach this from the machine's perspective; try negating INT_MIN yourself by flipping the bits and adding one. Each bit in 0x80000000 are flipped, so it becomes 0x7fffffff, and then, adding one to 0x7fffffff results in 0x80000000, which is INT_MIN. In other words, when the machine negates INT_MIN, it ends up returning the same INT_MIN! Therefore, absolute(INT_MIN) shown above will return INT_MIN, which is not a positive integer.

[Task] Phase 1 - provide a password to take the if branch and make the program print "Password OK :)". That will bring you to the next phase (Crackme Level 0x01).

2. Race condition

Once the first phase is solved, crackme0x00 goes to the next phase. In this phase, it generates a password on-the-fly (see gen_new_passwd() in crackme0x00.c) and asks for the correct password.

void round2() {
  int passwd = gen_new_passwd();
  save_passwd_into_vault(passwd);

  printf("IOLI Crackme Level 0x01\n");
  printf("Password:");

  char buf[32];
  scanf("%31s", buf);

  if (atoi(buf) == passwd) {
    printf("Password OK :)\n");
    printf("[!] Have a great fun!\n");
    snake_main();
  } else {
    printf("Invalid Password!\n");
  }
}

One interesting behavior of this program is that save_passwd_into_vault() temporarily stores the password to /tmp/.lock-[pid], and immediately removes the temporary file.

void save_passwd_into_vault(int passwd) {
  char tmpfile[100];
  snprintf(tmpfile, sizeof(tmpfile), "/tmp/.lock-%d", getpid());
  if (access(tmpfile, F_OK) != -1) {
    printf("the lock file exists, please first clean up\n");
    exit(1);
  }

  FILE *fp = fopen(tmpfile, "w");
  if (!fp)
    err(1, "failed to create %s", tmpfile);
  fprintf(fp, "%d", passwd);
  fclose(fp);

  /* DELETED! */
  unlink(tmpfile);
}

How would you steal the password stored in this file? Although the lifetime of this file is very short, it is stored in a predictable location (i.e., /tmp/.lock-[pid]), which gives an attacker an opportunity to leak the content inside the file by having a process racing to access the same file.

PID is (likely) assigned in a sequential order so that your exploit code might bruteforce after spawning a target (with process() in pwntool). In fact, the parent process knows the PID of a child process even before exec-ing the child's process image. If you are not familiar with the concept of fork(), please read man fork before writing the exploit!

Tip. About template.py

You might invoke process() together with stdin=PTY and stdout=PTY to manage the child process up to this stage (man pty). However, the interactive() of pwntools doesn't render the output of snake (next phase). We recommend using the below template for the next stage.

import os
import pty

(pid, fd) = pty.fork()
if pid == 0:
    # child
    os.execle("./target", "./target", os.environ)
    exit(0)
else:
    # parent
    lock = "/tmp/.lock-%d" % pid

    # this is how to write a message to the child
    os.write(fd, "...")

[Task] Phase 2 - exploit the race condition to steal the password that's generated on-the-fly. Provide the password and get "Password OK :)" printed, and get to the final phase.

3. Command injection


Micro Snake

Once you have passed the first two phases, you can see an Easter Egg -- an old-fashioned snake game. First, take a look at snake/snake.c (from Micro Snake).

Have you noticed an interesting piece of code in the main()?

// snake/snake.c
void snake_main() {
   ...
   if (WEXITSTATUS(system ("stty cbreak -echo stop u")))
   {
      fprintf (stderr, "Failed setting up the screen, is 'stty' missing?\n");
      return 1;
   }
}

Interestingly, the binary invokes a libc's system() when it starts (check man stty!). In fact, such a pattern is vulnerable to a command injection attack (e.g., in a setuid binary). How would you hijack crackme0x00 without exploiting a memory corruption bug like previous labs?

At this point, you might realize that this technique would have come in handy when you were solving the bomb challenges (labs 1 and 2). How come?

Good luck!

[Task] Final phase - inject a command to the snake game to print the flag. Submit the flag on the submission site.