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
.
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 neg
ates 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
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.