Buffer Overflows
C is considered to be insecure by many because it doesn't prevent
buffers from overflowing. The standard C library does indeed encourage
unsafe programming. It's still possible to write secure C code if you have
enough self-discipline not to write to memory in unsafe manner. Read access
overflows are more difficult to fix, but with good design they're quite
irrelevant.
Possible Exploits
- Arbitrary code execution. This requires modifying either return address
of function in stack or a function pointer somewhere in memory, assuming
your program uses any.
- Indirect arbitrary code execution. Modify some data structure so that
the code execution can later be tricked into overwriting the wanted part
of memory. This includes malloc() header exploiting, overflowing some
static buffer which later gets copied to stack without limit checks,
etc.
- Overwrite some variable so that we can access data we weren't supposed
to. For example a filename or database connect string which later gets
used. This could be used to create or overwrite files allowing access to
the computer (/etc/passwd, ~/.rhosts).
- Exposing potentially secret information in memory.
Common Problem Cases
- String manipulation with standard C library: strcpy and friends.
- Format string vulnerabilities. Allowing attacker to pass unwanted %n
modifier to printf() allows writing to wanted position in memory. These
can be difficult to notice somtimes. printf(str) and syslog(str) are
still easy to spot, but wrappers to them are more difficult.
- Integer overflows by not restricting user input well enough.
For example you would do
buf = malloc(user_given_length+1);
read_input_data(buf, user_given_length);
If user just gave large
enough value, the +1 would wrap the integer into malloc(0) but
we still said to read_input_data() that we have a huge buffer.
- Just plain carelessness.
Preventing Exploits
Besides writing secure code, there's some solutions to run existing
potentially insecure software more safely:
- Non-executable stack and heap. It gets difficult to execute arbitrary
code if we can't place it anywhere where it could be executed. OpenBSD
nowadays includes non-exec stack for all platforms and non-exec heap for
platforms supporting it directly (non-x86).
grsec provides both of these for Linux/x86.
- Even if arbitrary code can't be executed, the attacker could still call
the existing code with modified hostile environment. grsec contains
several options to randomize the address space layout which prevents
these exploits quite well. Some programs may not like this though.
- Signing return addresses and verifying them before jumping will also
prevent those exploits very well. Signing works by picking a random
value at program startup, saving return_address XOR random_value to
stack at the beginning of each function, then verifying that
return_address XOR random_value == signed_address when returning from
the function. Attacker would have to guess the random_value to get past
this check. This however requires recompiling programs with a modified
compiler and it makes them a bit slower and a bit larger.
- Immunix Linux has compiled
many of it's binaries using this technique.
StackGuard is
their patch for GCC to provide this feature, but looks like it's
only available for EGCS (some years old GCC).
- However if the random_value gets known, all is lost. It may be
possible to find this value using some other read access
overflows, for example using format string vulnerabilities to
do
printf("%s", known_address_of_random_value);
Another way would be to just overwrite the random_value using
%n modifier. The address could probably also be randomized, but
that would require some ELF loader changes.
- Somewhat safe fix to above problem would be to place the
random_value at the end of non-writable memory page with a
non-readable memory page coming after it. This way we can't
modify it and reading it as string would crash the program, as
long as we made sure that the value doesn't contain \0 bytes.
- StackGuard offers some other ways to sign the return address as
well, but they're useful only for preventing stupid strcpy()
exploits.
- None of the above methods can prevent problems with overwritten variable
being used maliciously. Luckily that kind of exploits are difficult to
create and probably impossible to find with many programs.
- GCC has also
bounds checking patch. I couldn't find an explanation how it actually
works, but apparently it can detect almost all possible buffer overflows.
It however makes code much slower and larger (one mail said 500% slower,
100% larger) which makes it unacceptable to many.
- Fat
pointers patch to GCC provides another way to do bounds checking
much faster. There are some problems with this though.
- Cyclone,
a safe dialect of C. Pretty interesting language. It's designed so that
C code can be quickly ported to it but still be fully safe. It defines
several new pointer types and sets restrictions to them. Some tests
showed that with CPU-intensive pointer operations it was over twice as
slow as regular C code. Cyclone pretty much requires using garbage
collector. It contains also region-based memory management but that
doesn't work well with everything.
Overflow Detection Tools
- Valgrind is one of the
best tools to detect invalid memory usage at runtime without
recompiling. It runs the code in virtual x86 CPU and detects when
accessing uninitialized or invalid memory. Valgrind only checks that it
accesses valid memory, it doesn't notice if you write past an array in
stack or data segment if it still points to valid memory. It has some
code to detect malloc()ed memory overflows better. Valgrind currently
works only with Linux/x86.
- Electric Fence
uses more portable method to detect overflows. It allocates memory using
anonymous mmap() and sets the next memory page non-readable/writable
using mprotect(). The data is placed at the end of the allocated memory
page so that buffer overflow will try to access invalid memory page
causing process to segfault. This has the problem that it has to
allocate memory in full pages (usually 4kB) for each malloc(), even if
only few bytes is needed. If allocation size isn't divisible with
required memory alignment it doesn't detect overflows of 1-3 (32bit)
or 1-7 (64bit) bytes. Underflows are neither detected.
- njamd - Detects
malloc() overflows, underflows, memory leaks and can give some
statistics. Works pretty much the same way as Electric Fence.
- GCC bounds checking patches above.
- TCC - Tiny C Compiler
supports bounds checking. This is also one of the smallest and fastest
C compilers around (8x faster than GCC).