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.
Let's first take a look on crackme0x00.c:
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:
absolute(passwd) < 0
absolute()
// @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.
neg eax
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.
0x00000001
1
0b1111....1110
0xfffffffe
0xffffffff
-1
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).
INT_MAX
0x7fffffff
INT_MIN
0x80000000
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.
-INT_MAX
-INT_MIN
neg
absolute(INT_MIN)
[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).
if
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.
gen_new_passwd()
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.
save_passwd_into_vault()
/tmp/.lock-[pid]
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!
process()
exec
fork()
man fork
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.
stdin=PTY
stdout=PTY
man pty
interactive()
snake
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.
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).
snake/snake.c
Have you noticed an interesting piece of code in the main()?
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?
system()
man stty
crackme0x00
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.