It has been disclosed that the compromise of the Debian Project's servers
was made possible, in part, by a previously unpatched vulnerability in the
kernel's memory management code. For the curious, this article describes
how that vulnerability works, and what is required to exploit it. We'll
also look at how it could have remained unfixed for months.
Named memory and heap
Shared libraries, shared memory segments
The kernel organizes a process's memory in a way vaguely similar to the
diagram at the right. The addresses shown there correspond to the default
ia-32 implementation. This picture has been simplified somewhat, but it
conveys the basic idea. The real picture on a specific system can be had
by running "cat /proc/self/maps
The bulk of the memory used by a program for its variables and heap storage is
found in the section marked "named memory and heap" in the diagram. This
memory area is initially made large enough to hold the static variables
created by the program, but, as soon as more memory is required (to satisfy
malloc() calls, perhaps) that region of memory must be expanded.
Since the beginning, Unix-like systems have provided a system call (named
brk()) which can be used to change the size of the heap area. The
caller simply passes in the virtual address indicating where the new
"break point" should be set, and the area is expanded or contracted as need
Back in September, Andrew Morton noticed that no sort of bounds checking
was being applied to the address passed to brk(). In theory, this
omission means that a process could request an arbitrarily large heap
area. In practice, most programs would not get that far. The kernel does
not allow virtual memory areas to overlap each other, so any expansion of
the heap area that caused it to impinge upon the shared library areas
0x40000000 would be rejected with an error. So it would appear
that the lack of bounds checking was never that serious of a problem; all
it could do is allow a user to set up some huge page tables.
Obviously, the situation is worse than that. The memory layout diagram is
missing one important area; on ia-32 systems, the kernel itself is mapped
in starting at 0xc0000000 - right above the process stack area. Processes
normally do not have any access to that part of memory, of course. But, as
it turns out, if you can convince brk() to expand your heap area
up into the kernel's address range, you have direct access to the kernel
code and data areas. At that point, the integrity of the system is lost.
The key to cracking the system is changing the process memory layout so that
the heap area can be expanded into the kernel's space. You cannot easily
do that with a normal C program, but, with a bit of assembly trickery
things become easier.
A proof of concept
exploit has already been posted to Bugtraq, so one can see how it is
done. It is really a matter of (1) moving the program origin up into
the highest part of virtual memory, where the stack usually lives, and
(2) shorting out the C library's startup code which sets up the
address space in the first part. Once you do that, an unpatched system
will happily expand your heap area into kernel space.
So, as the Debian Project learned at great cost, this little omission in
the implementation of the brk() system call is fully usable for a
complete local root exploit.
There have been a lot of questions about how such a vulnerability could
remain unfixed for so long. In fact, it was patched in the 2.6.0-test
series almost as soon as it was found. The fix also went to Marcelo
Tosatti, the 2.4 maintainer, but it was too late for the 2.4.22 release, which happened on
August 25. So the fix was merged into 2.4.23-pre7, which came out on October 9.
The current 2.4.23 kernel is not vulnerable - but that was too late to help
The real problem, of course, is that nobody realized the severity of this
bug. Had the kernel developers understood that current kernels were
vulnerable to this sort of attack, the alert would have gone out and the
various distributors would have sent out the usual set of updates. But
this patch was just one of over 2000 patches merged by Linus in September.
It would seem that it simply became part of the stream of fixes, and nobody
looked at it particularly closely.
Except, of course, somebody did. Chances are, the posting of this fix
drew an attacker's attention to the brk() code. With a bit of effort, the
exploit got written, and now thousands or millions of systems are at risk.
What the kernel (along with most other projects) needs is more friendly
eyes looking for this sort of problem. We do reasonably well, in that most
vulnerabilities are found and fixed by the good guys before they can be
exploited. There are cases where that doesn't happen, however, and the
brk() bug was one of them. Security auditing is hard work, and
usually unrewarding. But it would have been nice if somebody had looked
hard enough at this problem to raise the alarm.
to post comments)