The Rust programming language is famously memory safe, but what does that actually mean, and why should we care? This blog series is designed as a high-level introduction to the world of binary exploitation, the process of manipulating and taking control of programs via memory bugs!
Note: I haven’t had the time to fully polish this post yet, so it might change in the next few days!
What is memory safety?
Memory safety is, in essence, the absence of undefined behaviour resulting from invalid memory access or management. In particular, this means that a program written in (the safe subset of) Rust is guaranteed to never corrupt its own memory.
If you don’t have a lot of experience working with program’s memory at a low-level, that definition probably seems rather abstract and confusing. To build some intuition, let’s look at an example C program.
An example
int main() {
char password[16] = {0};
int logged_in = 0;
fgets(password, 16, stdin);
if (strcmp(password, "super_secret\n") == 0) {
logged_in = 1;
}
if (logged_in) {
printf("Welcome!\n");
// launch nukes or something
}
}
This program is perfectly safe and valid. But what if you made a typo in the fgets call?
int main() {
char password[16] = {0};
int logged_in = 0;
fgets(password, 61, stdin); // oops!
if (strcmp(password, "super_secret\n") == 0) {
logged_in = 1;
}
if (logged_in) {
printf("Welcome!\n");
// launch nukes or something
}
}
C is very memory unsafe, so it’s not going to complain at compile-time. But at runtime, if a user enters more than 15 characters it’ll write some of those characters past the end of the password array! This is a form of memory corruption called a buffer overflow.
What exactly is after this array in memory? Well it’s not specified by the C standard, but it’s likely to be the logged_in variable. That’s an integer, but in memory everything is just bytes, so the overflow could replace the contents of logged_in with something nonzero, allowing a user to log in with an incorrect password. The world has been plunged into nuclear war because of your typo!
For example, an input of AAAAAAAAAAAAAAAAA (17 ‘A’s) would set the least significant byte of logged_in to 0x41.
Importantly, this attack works because programs trust their memory. The code assumed that the memory in which the logged_in variable is stored wouldn’t be spuriously modified between when it was initialised and used. This is a valid assumption in a program that’s free of memory bugs (recall that this is guaranteed by memory safe languages)!
This is why memory safety is so important, and why memory vulnerabilities are so dangerous. An attacker who can arbitrarily manipulate the memory of a program can gain total control over that program, and subtle bugs can often lead to this level of control.
What about Rust?
The above vulnerability is not possible in safe Rust. The compiler guarantees (among many other things) that safe Rust programs will never access memory in invalid ways. For example, indexing into a Vec is bounds-checked at runtime, preventing out-of-bounds reads and writes.