Seit 1972 sind Buffer-Overflow-Angriffe verstanden, und doch dominieren Pufferüberläufe bis heute die Warn-Listen von Security-Spezialisten. Ein Plädoyer für das Einhalten von Coding-Standards, mehr und bessere Sourcecode-Reviews sowie den Gebrauch guter Tools zur statischen Analyse.
Ein Bonmot des Rechtsmediziners Wolfgang Eisenmenger zur nachlässigen Leichenschau in Deutschland lässt sich problemlos auch auf Software ummünzen: Gliche die Qualität von Autos der von Software, wären alle Pannenstreifen überfüllt. Ausbildungsmängel, unzureichende Qualitätssicherung und Kontrolle sind ursächlich für pannenreiche Software. Der Artikel zeigt wie Codeanalyse die Qualität und Sicherheit steigern kann.
Schon 1972 gab es die erste, wenn auch theoretische Beschreibung des Buffer-Overflow-Angriffs [1], seit 1998 sind SQL-Injections [2] verstanden. Beide stellen heute noch die Mehrheit der Ursachen für IT-Sicherheitslücken, und beide lassen sich leicht vermeiden: Buffer Overflows durch die »n«-Funktionen wie »strncpy()« statt »strcpy()«, SQL-Injections durch »prepared«-Statements. Doch wer prüft, ob Entwickler sich daran halten?
Coding-Standards, die sichere Funktionen vorschreiben, würden helfen solche alten Fehler nicht zu wiederholen. Doch kaum eine Firma und kaum ein Projekt erklären solche Leitlinien als verbindlich, geschweige denn kontrolliert jemand deren Einhaltung. Damit entwickeln die wenigsten Entwickler eine Neigung, sich an Sicherheitsvorgaben zu orientieren.
Klammern und einrücken
Es macht die Code-Review als Arbeitsaufgabe zudem nicht attraktiver, wenn der damit Beauftragte von Hand prüfen muss, ob sein Kollege zum Beispiel seine geschweiften Klammern richtig eingerückt hat. Tools können und sollten solche stupiden Arbeiten übernehmen, doch die müssen flexibel sein.
Schon allein bei der optischen Anordnung der geschweiften Klammern und ihrer Einrück-Tiefe scheiden sich die Geister: Während der Autor des Artikels seine Pascal-Vergangenheit nie ganz abschütteln kann und für ihn ein Block auf einer neuen Zeile mit einer Klammer (Pascal-»begin«-artig) losgeht, sehen viele schon den Schleifenkopf oder die Bedingung als ausreichend an und setzen die öffnende Klammer an deren Ende. Listing 1 verdeutlicht die Unterschiede in Pseudocode.
Auch die Einrücktiefe im Quelltext ist Interpretationssache: Zwei Leerzeichen, oder ein Tab? Und wie viele Leerzeichen maximal sind das Äquivalent zum Tabulator? Klar, dass kriegt sicher ein unterstützendes Tool geregelt, aber setzt es das Verbot von »strcpy()« und Funktionen, die als »deprecated« markiert sind, durch? Ein umfangreiches Beispiel für Coding-Standards für Entwicklung von Embedded-Software in C liefert [3].
Manche Entwicklungsumgebungen, Eclipse oder Vim beispielsweise, unterstützen bereits ab Werk das Einhalten von Standards ganz oder teilweise. Daneben gibt es eigenständige Programme wie Uncrustify [4].
Listing 1
Zwei Arten zu klammern
01 if (x == y) then
02 {
03 do something;
04 }
05
06 while ( x < y) {
07 x = x+1;
08 }
Schule fürs Leben
Bereits optisch aufzuräumen hilft Fehler in Kontrollstrukturen zu erkennen. Den ersten Erfolg hierbei fuhr der Autor bereits als Schüler ein. Als sich einmal das selbst geschriebene Lernprogramm einer Lehrerin unter hämischer Anteilnahme der Klasse eigenartig verhielt, drückte sie dem vorlauten Tobias den Quelltext in Form einer Diskette in die Hand, kombiniert mit der Hausaufgabe, eventuelle Fehler zu entfernen.
Der Quellcode war mal eingerückt, mal nicht, es ging zu wie Kraut und Rüben. Nachdem ein Code-Beautifier drübergelaufen war, zeigte sich, dass an einigen Stellen Blockenden nicht markiert waren. Um dem Compiler das Maulen abzugewöhnen, hatte die Lehrerin die unterzähligen Blockabschlüsse einfach an anderen Stellen platziert. Die Anekdote zeigt: Allein durch optisch saubere Programmierung sind Fehler zu vermeiden.
JS Lint: Kritikerpapst für Javascript-Autoren
Einen Schritt über sauberen Stil hinaus gehen Codeanalyse-Tools: Für Javascript zum Beispiel bietet JS Lint [5] einen Online-Check an, der sich in Abbildung 1 testweise das Skript »quadrat.js« aus [6] vornimmt. Die initial viel zu ausführliche Kritik grenzt ein, wer JS Lint anweist, sich auf einen Browser zu beschränken. Gleichwohl zeigt sich JS Lint von Fall zu Fall überkritisch. So moniert es gern den Umgang mit den Gänsefüßchen, den anerkannte Referenzen wie [7] deutlich entspannter sehen.
C-Programmierer hantieren mit Splint
Analog zu JS Lint liefert Splint [8] für C eine gründliche Analyse des Codes. Listing 2 zeigt ein bewusst misslungenes Beispielprogramm. Schon bei manueller Durchsicht fallen der potenzielle Buffer Overflow in Zeile 8 und die Formatstring-Schwachstelle in Zeile 9 auf. GCC kompiliert den Quellcode aber ohne größeren Widerstand zu einem ausführbaren Programm. Abbildung 2 bestätigt den Eindruck, wie marode das Programm ist.
Bevor Splint zeigen kann, ob es die Mängel auch erkennt, gilt es den klassischen Fünfkampf aus Download, Auspacken, Kompilieren und gegebenenfalls Installieren zu absolvieren:
tar -xzf splint-3.1.2.src.tgz cd splint-3.1.2 ./configure make make install # wenn gewünscht
Ist der Kampf gewonnen, muss Splint noch erfahren, wo es die Informationen zu den Headerfiles findet. Ohne Installation liegen die im Unterverzeichnis »lib«, der Pfad lautet somit zum Beispiel »export LARCH_PATH=home/tobias/code/splint-3.1.2/lib/”«.
Manche Distributionen halten in ihren Repositories auch die aktuellen Splint-Binaries vor. Auf jeden Fall darf es jetzt losgehen mit:
/bin/splint -strict example1.c
Listing 3 zeigt die Ausgabe und damit das volle Ausmaß des digitalen Grauens. Splint deckt unnachgiebig jede Schlamperei auf und bringt mit sieben Warnings deutlich mehr als die eine, die »gcc -Wall«, also die Ausgabe mit allen Warnings, entdeckt (Listing 4).

Abbildung 2: Es ist gut zu erkennen, von dem Beispielprogramm ist in erster Linie Unheil zu erwarten.
Listing 2
example1.c mit Fehlern
01 #include <strings.h>
02 #include <stdio.h>
03
04 #define BUFSIZE 10
05 int main(int argc, char * argv [])
06 {
07 char buffer [BUFSIZE];
08 strcpy(buffer, argv[1]);
09 printf(buffer);
10 } // end main()
Listing 3
splint -strict example1.c
01 Splint 3.1.2 --- 11 May 2019 02 03 example1.c: (in function main) 04 example1.c:9:5: Format string parameter to printf is not a compile-time 05 constant: buffer 06 Format parameter is not known at compile-time. This can lead to security 07 vulnerabilities because the arguments cannot be type checked. (Use 08 -formatconst to inhibit warning) 09 example1.c:9:5: Called procedure printf may access file system state, but 10 globals list does not include globals fileSystem 11 A called function uses internal state, but the globals list for the function 12 being checked does not include internalState (Use -internalglobs to inhibit 13 warning) 14 example1.c:9:5: Undocumented modification of file system state possible from 15 call to printf: printf(buffer) 16 report undocumented file system modifications (applies to unspecified 17 functions if modnomods is set) (Use -modfilesys to inhibit warning) 18 example1.c:10:4: Path with no return in function declared to return int 19 There is a path through a function declared to return a value on which there 20 is no return statement. This means the execution may fall through without 21 returning a meaningful result to the caller. (Use -noret to inhibit warning) 22 example1.c:8:5: Possible out-of-bounds store: strcpy(buffer, argv[1]) 23 Unable to resolve constraint: 24 requires maxRead(argv[1] @ example1.c:8:20) <= 9 25 needed to satisfy precondition: 26 requires maxSet(buffer @ example1.c:8:12) >= maxRead(argv[1] @ 27 example1.c:8:20) 28 derived from strcpy precondition: requires maxSet(<parameter 1>) >= 29 maxRead(<parameter 2>) 30 A memory write may write to an address beyond the allocated buffer. (Use 31 -boundswrite to inhibit warning) 32 example1.c:8:20: Possible out-of-bounds read: argv[1] 33 Unable to resolve constraint: 34 requires maxRead(argv @ example1.c:8:20) >= 1 35 needed to satisfy precondition: 36 requires maxRead(argv @ example1.c:8:20) >= 1 37 A memory read references memory beyond the allocated storage. (Use 38 -boundsread to inhibit warning) 39 example1.c:5:14: Parameter argc not used 40 A function parameter is not used in the body of the function. If the argument 41 is needed for type compatibility or future plans, use /*@unused@*/ in the 42 argument declaration. (Use -paramuse to inhibit warning) 43 44 Finished checking --- 7 code warnings
Listing 4
gcc -Wall example1.c
01 example1.c:9:12: warning: format string is not a string literal (potentially insecure) [-Wformat-security] 02 printf(buffer); 03 ^~~~~~ 04 example1.c:9:12: note: treat the string as an argument to avoid this 05 printf(buffer); 06 ^ 07 "%s", 08 1 warning generated.
Fehlerbericht
Als erstes Warning kommt der Hinweis auf die mögliche Format-String-Schwachstelle in Zeile 9. Das ist für eine Check-Software nicht schwer zu erkennen, denn diese »printf()«-Funktion besitzt einen variablen Parameter, was gefährlich ist. Hier darf ein Angreifer »printf()«, wie Abbildung 2 zeigt, einfach einen Formatstring übergeben. Printf erwartet dann die Daten auf dem Stack und versucht, nicht vorhandene Daten auszulesen. Mit ein paar Kunstgriffen kann ein Angreifer so auch den Stackinhalt überschreiben.
Die Lösung ist einfach: »printf()« braucht immer mindestens einen fixen Formatstring und, soweit nötig, die dort einzusetzenden Zeichenketten. Zeile 9 lautet damit richtig »printf(“%s”, buffer)«.
Die zweite, neue Warnung bezieht sich auf den unsauberen Stil: »main()« ist mit einem Rückgabewert vom Typ Integer deklariert. Doch findet sich im Kontrollfluss keine einzige Return-Anweisung, »main()« bekommt also keinerlei Rückgabewert. Auch das kann zu unschönen Überraschungen führen.
Vor dem Buffer Overflow warnt Splint an dritter Stelle: Es könne nicht erkennen, wie sichergestellt ist, dass niemand über die Array-Grenzen der Variable »buffer« hinausschriebe. Das ist zwar etwas verbrämt formuliert, beschreibt aber genau den hier möglichen Überlauf. Die Lösung dafür heißt »strncpy()« – übrigens 1990 in den ISO-C-Standard aufgenommen und damit alt genug, dass es Programmierer endlich standardmäßig benutzen.
In derselben Zeile entdeckt Splint noch einen weiteren Fehler, einen möglichen Out-of-Bounds-Read: Was passiert bei einem Aufruf ohne weitere Parameter? Statt zuerst »argc« zu checken, nimmt das Programm einfach an, dass es einen Parameter gibt. Wer es ausprobiert, bekommt einen weiteren Segmentation Fault. Auf den Fehler deutet auch das letzte Warning hin: »argc« sei zwar als Funktionsparameter deklariert, doch nie benutzt.
Zwischenfazit
Wer Splint regelmäßig über seine Programme laufen lässt, entdeckt die kleinen Flüchtigkeitsfehler schnell und ist gleichzeitig vor groben Mängeln gewarnt. Eigentlich sollte das Standard sein, denn diese Art der Qualitätsicherung verhindert effektiv Sicherheitslücken.
Splint instruieren
Es kommt durchaus vor, dass Splint warnt, weil ihm Informationen fehlen, um Code richtig einzuschätzen. Der Programmierer kann dies aber über Kommentare im Quellcode beheben. Eine korrigierte Version von Listing 1 zeigt Listing 5. Es enthält einige Steuerinformationen, die bewirken, dass Splint keine Warnings mehr absondert.
Die Hinweise an Splint sind in C als Kommentar verpackt, der mit einem »@«-Symbol startet und endet, wie beispielsweise in Zeile 6. Hier verspricht der Programmierer, dass anderweitig sichergestellt ist, dass mindestens »argv[1]« existiert. Nun verschwindet das Out-of-Bounds-Read-Warning. Tatsächlich prüft Zeile 12 brav »argc«, und der Programmierer löst das Versprechen ein.
Zeile 7 sagt Splint zu, dass keine Dateisystemoperationen geplant seien, und beschwichtigt es damit bezüglich »printf()« in Zeile 15, das zudem nun um einen ordentlichen Format-String reicher ist. Zeile 10 ist eine Kurzschreibweise, die das gesamte Array mit Ascii 0 befüllt. Diese Syntax kennt Splint nicht und würde monieren, nicht alle Elemente des Arrays seien gefüllt. Zeile 9 schaltet diese Warnung ab. Damit Splint den Rest des Codes weiterhin kritisch beschaut, schaltet Zeile 11 die Überwachung wieder ein. Das Plus beziehungsweise Minus vor dem Wert macht die Musik.
In Zeile 14 ist eine explizite Typkonversion dazugekommen – sonst beschwert sich Splint über den falschen Datentyp. Dort ist übrigens ein klassischer Programmierdenkfehler berücksichtigt: C terminiert Strings mit der binären Null. Auch diese verbraucht ein Byte im Puffer. Daher passen in einen zehn Zeichen fassenden Puffer maximal neun lesbare Zeichen. Wer es ausprobieren möchte, nimmt Listing 6, das mit allen nötigen Splint-Hinweisen ausgestattet explizit warnt (Abbildung 3).
Listing 5
Das Beispiel mit Splint-Anweisungen
01 #include <strings.h>
02 #include <stdio.h>
03
04 #define BUFSIZE 10
05 int main(int argc, char * argv [])
06 /*@requires maxRead(argv) >= 1 @*/
07 /*@-modfilesys@*/
08 {
09 /*@-initallelements@*/
10 char buffer [BUFSIZE] = {'\0'};
11 /*@+initallelements@*/
12 if ( ( argc > 0 ) && (argv != NULL) )
13 {
14 strncpy(buffer, argv[1], (size_t) (BUFSIZE-1));
15 printf("%s\n",buffer);
16 }
17 return 0;
18 } // end main()
Listing 6
Ein Zeichen zu viel
01 #include <stdio.h>
02 #include <string.h>
03 #define BUFSIZE 10
04
05 int main(/*@unused@*/ int argc,
06 /*@unused@*/ char * argv []
07 )
08 /*@-modfilesys@*/
09 {
10 /*@unused@*/
11 char pwd[18] = "It is top secret!";
12 /*@-initallelements@*/
13 char target [BUFSIZE] = { '\0' };
14 /*@+initallelements@*/
15 char source [17] = "0123456789ABCDEF";
16
17 printf("source: %s\ntarget: %s\n",source,target);
18 strncpy(target, source, (size_t) BUFSIZE );
19 printf("source: %s\ntarget: %s\n",source,target);
20 return 0;
21 }
Warum es sich lohnt
Für Splint extra Kommentare in den Quelltext einzufügen, klingt unproduktiv. Es gilt aber zu bedenken: Splint moniert nur, was unüblich ist, also worüber ein Dritter, der den Quellcode liest, stolpern würde. (Nach ein paar Monaten wird übrigens der Programmautor selbst praktisch zum Dritten.) Außerdem machen Splint-Kommentare bewusst, wo die Fallen liegen. Gesichtet und dokumentiert, sind sie auch schnell umschifft.
Die Kommentare erinnern an das Hoare-Kalkül, ein Verfahren zur formalen Programmverifikation. Dabei definiert der Entwickler Vorbedingungen, einen Rechenschritt und eine Endbedingung. Die Endbedingung muss sich aus der Vorbedingung durch den Rechenschritt herleiten lassen. Wer sich detaillierter einlesen will, findet den Originalartikel unter [9] und etwas lesefreundlichere Vorlesungsfolien der FU Berlin unter [10].
Diese sehr formale Herangehensweise ist für viele Anwendungsprogramme nur beschränkt geeignet, aber eine gute Denkschule. Denn sie zwingt dazu, die einzelnen Programmschritte nicht “nur einfach” in Codeform zu bringen, sondern wirklich zu überdenken.
RATS-Fänger
Wem Splint zu pingelig und zu ausführlich arbeitet, sollte versuchen RATS (Rough Auditing Tool for Security, [11]) zu domestizieren. Statt konkreter Warnungen gibt das Tool zu kritischen Code-Stellen allgemeinere Hinweise. Installiert kriegt man es wieder entlang der klassischen Fünferkette:
unzip rough-auditing-tool-for-security-master.zip cd rough-auditing-tool-for-security-master ./configure make make install # wenn gewünscht
Was »rats« mit den Parametern »-w3 example1.c« aus Listing 2 macht, zeigt Listing 7. Wer das Tool nur im aktuellen Verzeichnis liegen und nicht installiert hat, muss mit »-d« noch den Pfad zur »rats-c.xml« übergeben, die typische C-Fehler beschreibt.
Anhand der RATS-Meldungen finden Entwickler im Code die kritischen Stellen auch, die Erklärungen sind gerade in Bezug auf die Sicherheitsprobleme etwas verständlicher. Allerdings bietet Splint den Vorteil, über die bekannten Sicherheitsthemen hinaus auch weitere Probleme im Quelltext aufzuzeigen. Splint prüft auch konkreter. Zum Vergleich: Die korrigierte Programm-Fassung aus Listing 5 wirft bei RATS zwei Warnungen aus, die beide nicht zutreffen, getriggert durch die Erkennungsmerkmale “feste Puffergröße” und »strncpy()«.
Listing 7
rats -w3 example1.c
01 Entries in c database: 310 02 Analyzing example1.c 03 example1.c:7: High: fixed size local buffer 04 Extra care should be taken to ensure that character arrays that are allocated on the stack are used safely. They are prime targets for buffer overflow attacks. 05 06 example1.c:8: High: strcpy 07 Check to be sure that argument 2 passed to this function call will not copy more data than can be handled, resulting in a buffer overflow. 08 09 example1.c:9: High: printf 10 Check to be sure that the non-constant format string passed as argument 1 to this function call does not come from an untrusted source that could have added formatting characters that the code is not prepared to handle. 11 12 Total lines analyzed: 11 13 Total time 0.000192 seconds 14 57291 lines per second
Problem mit der Fangquote
Treten substanzlose Warnungen an den Tag, steigt die Gefahr des Überblätterns echter Hinweise. Der entstehende Eindruck, alles in Sachen Sicherheit erledigt zu haben, ist natürlich fatal. Auf die Haben-Seite kann RATS sich aber buchen, dass es Code in vielerlei Sprachen zu prüfen in der Lage ist.
Löhnen für Coverity
Keine freie Software, aber wegen seiner Aufspür-Fähigkeiten in der Open-Source-Szene gut beleumundet [12] ist Coverity [13], ein kommerzielles Werkzeug zur statischen Codeanalyse von C-, C++-, C#- und Java-Programmen. Dabei gibt es eine lokale Variante und eine Lösung als Clouddienst. Wie letztere sich mit den eigenen Unternehmenszielen verträgt, muss jeder selbst schauen.
Wer ein vom Hersteller anerkanntes Open-Source-Projekt verwaltet, darf auf den Cloud-Ableger [14] zurückgreifen und gratis eine Codeanalyse durchführen. Den Service nutzt unter anderem der Linux-Kernel – seither hat sich gemessen an der Zahl der Findings die Codequalität deutlich erhöht.
Aus der Not eine Tugend
Wer sich einen gründlichen und sauberen Programmierstil angewöhnen möchte, ist zweifellos mit Splint gut beraten und in guter Gesellschaft. Entwickler, die auch jedem False Positive gründlich nachgehen wollen, finden in RATS einen helfenden Weggefährten.
Wichtig ist in allen Fällen das Ergebnis: Zwang zur Qualitätssicherung, Umdenken und Umlernen durch die stete, unnachgiebige Kritik der Kontrollprogramme und dadurch Sicherheitspannen-ärmere Software. Dass statische Codeanalyse, Reviews und Coding-Standards in der Realität etwas bringen, zeigt Open BSD mit nur zwei remote ausnutzbaren Sicherheitslücken in 20 Jahren.
Infos
-
James P. Anderson, “Computer Security Technology Planning Study, Volume II”:https://csrc.nist.gov/csrc/media/publications/conference-paper/1998/10/08/proceedings-of-the-21st-nissc-1998/documents/early-cs-papers/ande72.pdf
-
Rain.forest.puppy, “NT Web Technology Vulnerabilities”: Phrack Magazine, Volume 8, Issue 54, Dec 1998, http://phrack.org/issues/54/8.html#article
-
“Embedded C Coding Standard”: https://barrgroup.com/Embedded-Systems/Books/Embedded-C-Coding-Standard/Introduction
-
Uncrustify: http://uncrustify.sourceforge.net
-
JS Lint: http://www.jslint.com
-
Javascript-Tutorials: https://wiki.selfhtml.org/wiki/JavaScript/Tutorials/Einstieg/Einbindung_in_HTML
-
W3schools, “Javascript Strings”: https://www.w3schools.com/js/js_strings.asp
-
Splint: http://splint.org
-
C. A. R. Hoare, “An Axiomatic Basis for Computer Programming”: https://web.archive.org/web/20160304013345/http://www.spatial.maine.edu/~worboys/processes/hoare%20axiomatic.pdf
-
Margarita Esponda, “Programmverifikation”: http://www.inf.fu-berlin.de/lehre/SS12/ALP2/slides/V11_Programmverifikation.ALP2.pdf
-
RATS: https://github.com/andrew-d/rough-auditing-tool-for-security
-
Artikel und News zu Coverity: https://www.linux-magazin.de/?s=Coverity
-
Coverity Static Application Security Testing (SAST): https://www.synopsys.com/software-integrity/security-testing/static-analysis-sast.html
-
Coverity Scan: https://scan.coverity.com








