C-Laufzeitbibliotheken sichern

Veröffentlicht: 05. Jul 2004 | Aktualisiert: 14. Nov 2004
Von Michael Howard

Michael Howard dokumentiert die Maßnahmen, um die C-Laufzeitbibliotheken gegenüber bösartigen Daten weniger anfällig zu machen. Die in Visual Studio 2005 verfügbaren Änderungen betreffen die C-Laufzeit und die C++-Standardvorlagenbibliotheken. Dieser Artikel enthält auch Links zu englischsprachigen Seiten.

Auf dieser Seite

Was ist neu bei der neuen CRT? Was ist neu bei der neuen CRT?
Und wie sieht es bei C++ aus? Und wie sieht es bei C++ aus?
Und wie sieht es mit Standards aus? Und wie sieht es mit Standards aus?
Zusammenfassung Zusammenfassung
Entdecken Sie den Sicherheitsfehler Entdecken Sie den Sicherheitsfehler

Anmerkung Dieser Artikel basiert auf einer Softwarevorabversion. Einige der in diesem Artikel enthaltenen Informationen können sich bei Veröffentlichung des Produkts ändern.

Hin und wieder müssen wir alle einmal einen gründlichen Frühjahrsputz machen, sei es bei uns zu Hause oder in unserem Code. Und dann fragen wir uns unweigerlich, wo bloß der ganze Dreck herkommt und warum der uns vorher nie aufgefallen ist. So machen wir uns daran, Sachen wegzuwerfen und wieder andere behalten wir. Und wenn Sie so sind wie ich, dann ersetzen Sie einige der Sachen, die Sie weggeworfen haben, durch etwas Besseres und Neueres.

Wenn wir ehrlich sind, dann müssen wir zugeben, dass die C-Laufzeitbibliothek schon eine Generalüberholung vertragen könnte. Und damit meine ich ein wirkliches Großreinemachen und nicht nur ein bisschen Aufräumen!

Lassen Sie uns einmal überlegen: Wann wurden Funktionen wie strcpy und strcat erfunden? Sie wurden in der guten alten Zeit entworfen, als Kernighan und Ritchie die C-Sprache entwickelten, noch nicht überall Gefahren lauerten und es noch nicht von Netzwerken nur so wimmelte. Verstehen Sie mich bitte nicht falsch, denn natürlich können Sie mit Funktionen wie strcpy sicheren Code schreiben. Die wirklichen Übeltäter sind die Daten. Aber Funktionen wie strcpy helfen Ihnen nicht beim Schreiben von sicherem Code, und ein Fehler in einer solchen Funktion kann katastrophale Folgen haben.
gets ist gar der Teufel in Person!

Was also können wir tun? Vielleicht haben Sie schon mal von strsafe.h gehört. Dabei handelt es sich um einen Satz konsistenter, sicherer Funktionen zum Ändern von Zeichenfolgen, die während der Windows Security Push-Aktion in 2002 entwickelt wurden. Sie stehen in Visual Studio .NET 2003 und im Platform SDK zur Verfügung. Weitere Informationen über strsafe finden Sie unter Strsafe.h: Safer String Handling in C.

Weitere Funktionen wie strlcpy und strlcat stehen in vielen Opensource-Betriebssystemen zur Verfügung. Weitere Informationen zu diesen Funktionen finden Sie unter Secure Programming for Linux and Unix HOWTO von David Wheeler.

Mit strl* und strsafe haben wir zwar schon die richtige Richtung eingeschlagen, doch müssen wir darüber hinaus stabilere Funktionen in die zentrale C-Laufzeitbibliothek integrieren, und hier kommt dann die aktualisierte CRT ins Spiel. Das Microsoft Visual C++-Bibliothekenteam entschloss sich dazu, jede einzelne Funktion in der CRT einmal genauestens unter die Lupe zu nehmen, um Sicherheitsrisiken und mögliche Lösungen aufzuzeigen. Wie Sie sich vorstellen können, wurden viele Funktionen (an die 400, um genau zu sein) neu geschrieben, um sie sicherer zu machen und Ihnen dabei zu helfen, noch sichereren Code zu schreiben.

Was ist neu bei der neuen CRT?

Zunächst einmal möchte ich anmerken, dass die in diesem Artikel vorgestellten neuen CRT-Funktionen in Visual Studio 2005 erscheinen und bis zur Endversion noch weiteren Änderungen unterliegen können. Zweitens möchte ich darauf hinweisen, dass die Existenz neuer Bibliotheken unsicheren Code zwar nicht einfach durch ein kurzes Umlegen eines Compilerschalters zu sicherem Code macht, doch können diese Sie auf jeden Fall in Ihren Bemühungen unterstützen.

Die sichereren Alternativen ersetzen nicht die alten Funktionen. Anders ausgedrückt, strcpy bleibt immer noch strcpy. Die sicherere Version hat einen neuen Namen, strcpy_s. Wenn Sie jedoch mit der neuen Bibliothek kompilieren, werden die alten Funktionen außer Kraft gesetzt. Hier also eine kleine Warnung: Compilerwarnungen werden dabei nicht lange auf sich warten lassen. Doch es ist einfacher, eine Compilerwarnung zu beseitigen als einen Sicherheitsfehler. Das können Sie mir wirklich glauben!

Einige Funktionen, z.B. calloc, führen einfach mehr Parameterüberprfungen durch, doch die Funktionalitt bleibt dieselbe. Es gibt also keine Funktion calloc_s. Auf calloc kommen wir gleich noch einmal zu sprechen.

Eine Änderung, die mir besonders gut gefällt, betrifft den Austausch von strncat durch die Funktion strncat_s. Ein Problem bei strncat liegt darin, dass das letzte Argument nicht die Gesamtgröße des Zielpuffers darstellt. Es entspricht eher der Mindestrestgröße im Zielpuffer und der zu kopierenden Menge. Dies kann zu allen möglichen trivialen Fehlern, schlimmstenfalls aber zu komplexen Fehlern führen. Schauen Sie sich einmal das folgende Beispiel an:

if (szURL != NULL) {  
  char   szTmp[MAX_PATH];  
  char  *szExtSrc, *szExtDst;  
 
  strncpy(szTmp, szURL, MAX_PATH);  
 
  szExtSrc = strchr(szURL, '.');  
  szExtDst = strchr(szTmp, '.');  
 
  if(szExtDst) {  
    szExtDst[0] = 0;  
 
     if (fValid)   
       strncat(szTmp, szExtSrc, MAX_PATH);   
  } 
}

Der Aufruf von strncat ist falsch, ja sogar gefährlich. Ein Pufferberlauf ist hier geradezu vorprogrammiert. Ein sicheres Kopieren von MAX_PATH-Zeichen in szTemp ist nicht möglich, da der Aufruf von strncpy der Zeichenfolge bereits szURL hinzugefügt hat, was den noch in szTmp verbliebenen Platz verringert. Hier ein einfacheres Beispiel:

char szTarget[12]; 
char *s = "Hello, World"; 
 
strncpy(szTarget,s,sizeof(szTarget)); 
strncat(szTarget,s,sizeof(szTarget));

Wenn Sie dies in Visual C++ 2003 kompilieren, kommt es zu einer Fehlermeldung, in der Ihnen mitgeteilt wird, dass die Daten um szTarget zerstört wurden. Hier ist das /GS-Compilerflag in Aktion. Es hat einen stapelbasierten Pufferberlauf erkannt und die Anwendung angehalten.

Eine Möglichkeit, diesen Fehler zu beheben, liegt in der Verwendung des folgenden Codes:

char szTarget[12]; 
char *s = "Hello, World"; 
 
strncpy(szTarget,s,sizeof(szTarget)); 
strncat(szTarget,s,strlen(szTarget) - strlen(s));

Doch auch hier treibt ein gemeiner Fehler sein Unwesen. Wenn die Länge des Zielpuffers genau der Länge des Quellpuffers entspricht, terminieren viele n-Funktionen den Zielpuffer nicht mit einer Null, so dass der Aufruf von strlen(szTarget) eine Länge zurückgibt, die die Länge des Ziels überschreitet, da kein nachgestelltes '\0'-Zeichen vorhanden ist. Das hat das Ganze nur noch schlimmer gemacht!

Ein etwas widerstandsfähigeres Programm, das die neue Laufzeitbibliothek verwendet, sieht wie folgt aus:

char szTarget[12]; 
char *s = "Hello, World"; 
 
size_t cSource = strlen_s(s,20); 
strncpy_s(temp,sizeof(szTarget),s,cSource); 
strncat_s(temp,sizeof(szTarget),s,cSource);

Die beiden neuen Funktionen strncpy_s und strncat_s teilen eine ähnliche Funktionssignatur:

  • Beide geben einen Fehlercode (errno_t) anstatt eines Zeigers zurück.

  • Zielpuffer (char *)

  • Gesamtzeichenzahl des Zielpuffers (size_t)

  • Quellpuffer (const char *)

  • Gesamtzeichenzahl des Quellpuffers (size_t)


Beachten Sie die beiden Pufferzeichenzahlen, eine für jeden Puffer. Es ist nicht nötig, ständig über die Zeichenzahl des Zielpuffers Buch zu führen, was das Ganze schon um einiges einfacher macht. Es gibt noch einige andere interessante Features. Beide Funktionen beenden die Zeichenfolge immer mit einer Null. Schauen Sie sich das Codebeispiel aus meinen früheren Ausfhrungen zum Thema Spot the Security Flaw an:

void noOverflow(char *str) 
{ 
  char buffer[10]; 
  strncpy(buffer,str,(sizeof(buffer)-1)); 
  buffer[(sizeof(buffer)-1)]=0; 
  /* Avoiding buffer overflow with the above two lines */ 
}

Dieses Beispiel fand ich in einem im Dezember 2003 von einem großen multinationalen Softwareunternehmen veröffentlichten Dokument (nein, es handelt sich hier nicht um Microsoft), in dem Entwicklern die Vorteile einer sicheren Codierung erläutert wurden. Leider enthält dieser Code einen gravierenden Sicherheitsfehler. Wenn *str auf NULL zeigt, schlägt strncpy fehl, wenn versucht wird, einen NULL-Zeiger zu kopieren - dumm gelaufen! Bei strlcat, das in mehreren Opensource-Softwareanwendungen verwendet wird, tritt derselbe Fehler auf, jedoch nicht bei strncat_s.

Die Ursache, warum strncat_s nicht fehlschlägt, liegt in der Tatsache begründet, dass alle aktualisierten Laufzeitfunktionen viel strengere Eingabe-Parameterüberprfungen durchführen. Hier der Parameterberprfungsblock von strncat_s:

/* validation section */ 
_VALIDATE_RETURN_ERRCODE(front != NULL, EINVAL); 
_VALIDATE_RETURN_ERRCODE(sizeInTChars > 0, EINVAL); 
_VALIDATE_RETURN_ERRCODE(back != NULL || count == 0, EINVAL); 
Das berprfungsmakro lautet: 
#define _VALIDATE_RETURN_ERRCODE( expr, errorcode \  
{                                                 \  
   _ASSERTE( ( expr ) );                          \   
   if ( !( expr ) )                               \ 
   {                                              \ 
  errno = errorcode;                              \    
  _INVALID_PARAMETER(expr);                       \  
  return ( errorcode );                           \   
   } \  
}

_INVALID_PARAMETER liefert Dateiinformationen in einem Debugbuild für Fehler, um Anwendern zu helfen, den Code zu debuggen.

Wurde uns in der Schule nicht stets gepredigt, Funktionsargumente zu überprfen? Jetzt endlich geschieht das auch in der CRT. Das Ganze hat nur läppische zwanzig Jahre gedauert.

Sie sollten sich darber im Klaren sein, dass strsafe-Funktionen wie StringCchCopy und StringCchCat anders als strncpy_s und strncat_s reagieren. Wenn strncat_s einen Fehler erkennt, setzt die Funktion die Zeichenfolge auf NULL. Standardmäßig füllt strsafe das Ziel jedoch mit soviel Daten wie mglich und beendet dann die Zeichenfolge mit NULL. Dasselbe Verhalten können Sie in strsafe mit folgendem Code imitieren:

StringCchCatEx(dst,sizeof(dst)/sizeof(dst[0]),src,NULL,NULL,STRSAFE_NULL_ON_FAILURE)

Zu den anderen Pufferbearbeitungsfunktionen, die ebenfalls aktualisiert wurden, zählen die verschiedenen printf- und scanf-Funktionen, mbstowcs, strerror, _strdate und _strtime, asctime sowie ctime. Es wurden jedoch nicht nur Pufferbearbeitungsfunktionen aktualisiert, sondern auch die Funktionen _makepath, _splitpath, getenv, rand sowie viele andere.

Bei der calloc-Funktion handelt es sich um eine weitere interessante Funktion. Sie ist anfällig für einen Ganzzahlüberlauffehler, wenn size * num nach 2^32 umbricht. Um diesen Fehler zu beheben, überprft die aktualisierte calloc-Funktion, ob die Berechnung überläuft.

/* ensure that (size * num) does not overflow */ 
if (num > 0 && (_HEAP_MAXREQ / num) <= size) 
{ 
   errno=ENOMEM; 
   return NULL; 
}

Und wie sieht es bei C++ aus?

Das Visual C++-Team hat sich nicht nur mit den C-Laufzeitbibliotheken beschäftigt. In der Standardvorlagenbibliothek (Standard Template Library, STL) liegen bekannte Fehler vor. Wussten Sie, dass es zu einem Pufferüberlauf kommen kann, wenn Sie Iteratoren verwenden? Viele der mit einer fehlerhaften Verwendung von Iteratoren verbundenen Sicherheitsrisiken können ausgeschaltet werden, wenn Sie Iteratoren nutzen, die abbrechen (bzw. auslösen), wenn der zulässige Bereich überschritten wird. Hier ein Beispiel:

#include <vector> 
 
vector<int> v(10);      // vector with size == 10 
v[20] = 10;             // yields buffer overrun 
vector<int>::iterator it = v.end(); 
 
// advancing past the end causes a buffer overrun 
++it;

Durch Kompilieren dieses Codes mit #define _SECURE_SCL (1) werden alle Iteratorenbereiche überprft.

Sie können die neuen Funktionen auch einsetzen, ohne das neue #define zu verwenden. Die folgende Version des oben dargestellten Codes verursacht beispielsweise keinen überlauf:

vector<int> v(10);   // vector with size == 10 
stdext::checked_iterator<vector<int> > ck_it(v, v.end()); 
 
// advancing past the end will terminate the program 
++ck_it;

Andere aktualisierte Klassen und Methoden enthalten operator[] (vector-, string-, deque-, bitset-Klassen usw.), front und back (vector-, queue-, list-Klassen usw.). Auch einige Algorithmen wurden aktualisiert, darunter copy, copy_backward, *_copy, transform, fill, set_* usw.

Und wie sieht es mit Standards aus?

Jetzt denken Sie sich wahrscheinlich: Und was ist mit den Standards? Werden C und C++ nicht von Standards bestimmt? Das stimmt, sie sind standardisiert, und Microsoft hat die neuesten Entwicklungen dem Standards Committee vorgelegt Der Entwurf kann unter http://std.dkuug.dk/jtc1/sc22/wg14/www/docs/n1031.pdf eingesehen werden.

Zusammenfassung

Ich bin wirklich begeistert von den neuen Bibliotheken. Sie machen Ihren Code zwar nicht von selbst sicherer, doch sind sie ein probates Mittel zur Vermeidung von Pufferüberläufen. Bevor ich mich verabschiede, danke ich dem Visual C++-Bibliothekenteam für die groartige Arbeit. Hier sei besonders Martyn Lovell erwähnt, der mir dabei geholfen hat, diesen Artikel auszuarbeiten.

"Die CRT ist tot, lang lebe die neue CRT!"

Entdecken Sie den Sicherheitsfehler

Viele Leser haben den Fehler in meinem letzten Artikel entdeckt. Die Antwort finden Sie weiter vorne in diesem Artikel. Die so genannte "sichere" strncpy-Funktion überprft nicht, ob das Argument NULL ist und kann bewirken, dass Ihre Anwendung zusammenbricht bzw. eine Zugriffsverletzung auftritt.

So, hier haben wir nun den Fehler dieses Monats. Was ist an dem folgenden Code falsch?

void ReadDataFromFile(char *szFilename, 
LPOVERLAPPED_COMPLETION_ROUTINE func) { 
 
   HANDLE hFile = CreateFile(szFilename, 
                  FILE_ALL_ACCESS, 
                  FILE_SHARE_READ, 
                  NULL, 
                  OPEN_EXISTING, 
                  FILE_ATTRIBUTE_NORMAL |    
                  FILE_FLAG_OVERLAPPED, 
                  NULL);  
   OVERLAPPED io; 
   memset(&io,0,sizeof OVERLAPPED); 
   DWORD dwWritten=0, dwRes=0; 
 
   // Issue read operation 
   const size_t cBuff = 1024; 
   char buff[cBuff]; 
   if (!ReadFileEx(hFile,buff,cBuff,&io,func)){ 
      // oops! Bail! 
   } 
 
   // rest of code 
 
}

Anzeigen: