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
What does neg eax
instruction behave?
Unlike the mathematical operation,
which multiples -1 on its value (-i
in c),
it 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 behave
like a negation operation
for most of integers, each register can express.
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 value of -INT_MAX
? It is -2147483647
that a 32-bit register can express.
What about -INT_MIN
? It is 2147483648
but it is bigger than INT_MAX
!
In terms of neg
,
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, abs(INT_MIN)
ends up returning the same INT_MIN
which is not a positive integer.
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 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, "...")
3. Command injection
Once you passed the two phases,
you can see an Easter Egg
-- the old-fashion snake game.
First, take a look at
snake/snake.c
(from Micro Snake).
Have you noticed an interesting 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 inject attack
(e.g., in a setuid binary).
How would you hijack crackme0x00
(without exploiting a memory corruption bug
like previous labs)?
Do you wish that you are aware of this technique when solving bomb challenges (lab1/2)? why?
Good luck!