Und wieder ist, heute am 8 April 2014, ein kapitaler Software Fehler publik geworden.
Mich interessiert, wie es zu solchen Fehlern kommt, warum sie so lange nicht erkannt wurden, wie sie zu erkennen sind und am aller wichtigsten, wie man solche Fehler vermeiden kann.
Hier kann man übrigens testen ob ein Server von HeartBleed betroffen ist http://filippo.io/Heartbleed/.
Analyse des HeartBleed Bugs
Die beste bisher aufgetauchte Beschreibung des Fehlers habe ich hier gefunden https://news.ycombinator.com/item?id=7549943 von drv und darin die interessanten Stellen fett markiert :
TLS heartbeat consists of a request packet including a payload; the other side reads and sends a response containing the same payload (plus some other padding).
In the code that handles TLS heartbeat requests, the payload size is read from the packet controlled by the attacker:
1: 2: |
n2s(p, payload); pl = p; |
Here, p is a pointer to the request packet, and payload is the expected length of the payload (read as a 16-bit short integer: this is the origin of the 64K limit per request).
pl is the pointer to the actual payload in the request packet.
Then the response packet is constructed:
1: 2: 3: 4: |
/* Enter response type, length and copy payload */ *bp++ = TLS1_HB_RESPONSE; s2n(payload, bp); memcpy(bp, pl, payload); |
The payload length is stored into the destination packet, and then the payload is copied from the source packet pl to the destination packet bp.
The bug is that the payload length is never actually checked against the size of the request packet. Therefore, the memcpy() can read arbitrary data beyond the storage location of the request by sending an arbitrary payload length (up to 64K) and an undersized payload.
I find it hard to believe that the OpenSSL code does not have any better abstraction for handling streams of bytes; if the packets were represented as a (pointer, length) pair with simple wrapper functions to copy from one stream to another, this bug could have been avoided. C makes this sort of bug easy to write, but careful API design would make it much harder to do by accident.
Vorkehrung
Die genannte Vorkehrung wäre eine einfache Datenstruktur, ein Tupel (pointer, length), um die zusammengehörenden Daten zu vereinen, damit es nicht zu Verwechslungen kommen kann.
In F# Syntax sähe das so aus
1:
|
let buffer = (pointer, length) |
oder man erzeugt einen Record
1:
|
type Buffer = { pointer : int64; length : int64 } |
Ja aber wenn wir schon bei Verwechslungen sind, wie vermeidet man hier dass die length
und pointer
nicht aus versehen vertauscht zugewiesen werden?
1: 2: |
let buffer = { l; p } // Vertauscht! Compiler reklamiert nicht :-( let buffer = { p; l } // pointer, length |
Eine Verwechslung der Parameter kann der Compiler so nicht erkennen, es sind ja in diesem Beispiel beides long
! Was kann man dagegen tun? F# hat dazu die sogenannten Units of Measures, also Maßeinheiten, wie Kilogramm und Meter, die ganzen SI-Einheiten sind schon vordefiniert, das nur am Rande, denn wir brauchen hier was anderes. Wie wäre es mit den Einheiten Pointer
und Length
? Diese kann man sich wie folgt definieren
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: |
// Units of Measures type [<Measure>] Pointer type [<Measure>] Length // Record Type type Buffer = { pointer : int64<Pointer> length : int64<Length> } let p = 123456L<Pointer> let l = 512L<Length> let buf = { pointer = p; length = l } // val buf : Buffer = {pointer = 123456L; // length = 512L;} // Verwechslung bei der Zuweisung let buf = { pointer = l; length = p } // error FS0001: Type mismatch. Expecting a // int64<Pointer> // but given a // int64<Length> // The unit of measure 'Pointer' does not match the unit of measure 'Length' |
Als Kommentar sind die Compiler Meldungen angehängt, dort ist schön zu sehen wie F# reklamiert, dass dem Pointer
keine Length
zugewiesen werden kann.
Noch mehr Bugs?
Interessant ist auch, dass genau da wo der HeartBleed Bug die Ursache hat, schon einmal etwas gefixt wurde am 27 Feb 2012,
“This patch fixes two padding related bugs for the Heartbeat Response messages. For DTLS, the wrong pointer was used, which may overwrite the payload with the random padding. For TLS, there was no random padding at all.”
http://marc.info/?l=openssl-dev&m=133035562503173
Das gibt der alten ungeschriebenen Regel wiedereinmal mehr recht:
Da wo ein Bug ist, sind noch mehr.
Fazit
Wichtig war es mir hier anhand aktueller echter Probleme aufzuzeigen, wie man dies elegant und besser lösen kann. Und mit dem Potential das F# mitbringt ist es bestens geeignet um komplexe Probleme einfach und korrekt zu lösen. Man denke dabei auch daran, wie viele Unit-Tests man einsparen kann, wenn der Compiler so strikt prüfen kann.
val int64 : value:’T -> int64 (requires member op_Explicit)Full name: Microsoft.FSharp.Core.Operators.int64——————–
type int64 = System.Int64Full name: Microsoft.FSharp.Core.int64——————–
type int64<‘Measure> = int64
Full name: Microsoft.FSharp.Core.int64<_>
type MeasureAttribute =
inherit Attribute
new : unit -> MeasureAttributeFull name: Microsoft.FSharp.Core.MeasureAttribute——————–
new : unit -> MeasureAttribute