In the recent panic surrounding the Heartbleed bug, we ask ourselves why, and how, these bugs still happen. We know that it was a preventable bug, with a simple fix, but with potentially important repercussions.
The basic source of the problem, before the bug, is, of course, over-engineering. Why would one in his right mind allow such an operation? Well, we can defend the idea of the heartbeat, basically a keep-alive message, but certainly not how it was implement.
You will object that ICMP ping does that and allows variable payloads. True. But the variable payload of ping has the purpose of helping us determine whether or not the network can transport bits correctly, and if not, estimate the bit error rate. Ping, also, suffered from a similar bug.
But what is the use of having a variable length payload in the SSL heartbeat? To use as a nonce? If so, I think that a 128 or 256 bits random value can serve as a nonce, as good as any human-readable string. Fixed-sized messages do eliminate the problem of checking bounds. It also bounds compute-time. While probably negligibly, a bigger packet takes more time than a small packet to be processed (especially if copies are involved).
Long story short, someone was too smart for his own good.
This being said, what can we do to help prevent these buffer overflow (read or write) errors? We can suggest the usual (code review, better unit tests, etc., etc.) but we could also consider techniques based on support from the operating system, run-time library or even the CPU itself.
The operating system can help us prevent some of this by using address space layout randomization, that is, giving the program and its various segments different positions in memory, so that over-reading (-writing) at a specific address is now unlikely to yield information. Taken to its extreme, the idea asks for even the code itself to be position-independent and remixed at load time. Function order in memory varies. Stack location varies. Randomization makes the task of leading a useful attack a lot harder.
The run-time library can help us secure data by allocating and freeing memory blocks as if they’re dangerous. Add padding, check if the program has over-read or over-written in the pad. Zero memory before freeing it. On Linux, for example, a new process gets zeroed-out memory, but if the program allocates and frees memory, eventually an allocated block of memory will contain some of the program’s previous data. This is solved by zeroing out the memory when we free it—something that’s not done because it is considered expensive. These techniques are usually considered “debug tools” (Electric Fence, Dmalloc).
The characteristics of the underlying machine can be exploited to create secure memory blocks. Protected mode allows for any number of segments, each with its descriptor, to be created. This allows one to specify a memory region of pre-determined size with specific permissions. If you try to read or write beyond the limits of the segment, or if you try to write to a segment that is read-only, or execute code in a segment that disallows it, the CPU itself generates an exception and the operation is prevented from completing. The exception can be trapped and dealt with in some application-specific fashion.