Komfortabler Dateimanager mit vielen Funktionen

Developer Security Training mit Tücken

By Sven on 20.02.2006 - 09:39 in Entwicklung

Bis vor kurzem hat Microsoft kostenlos eine DVD verschickt, in der Frank Fischer einige für den Programmierer wichtige Sicherheitsaspekte bei der Entwicklung beschreibt. Ein Schwerpunkt ist die Beschreibung von Pufferüberläufen und Strategien zu ihrer Vermeidung. In kleinen Codebeispielen wird demonstriert, was bei Pufferüberläufen passiert und was der Entwickler dagegen tun kann.

Pufferüberläufe treten sehr häufig bei Stringfunktionen auf, weil beim Kopieren von Strings oft nicht geprüft wird, ob der Zielpuffer groß genug für die Aufnahme des Strings ist. Abhilfe schaffen hier die sicheren Stringfunktionen, welche die Zielpuffergröße immer beachten und sicherstellen, dass der Zielstring explizit mit einem NULL-Zeichen abgeschlossen wird. Zur Demonstration der sicheren Stringfunktionen wird folgendes Beispiel verwendet:

void DisplayResult(HRESULT hr);

int _tmain(int argc, TCHAR *argv[])
{
    TCHAR pszDest[10];

    // String size OK
    HRESULT hr = StringCchCopy(pszDest, sizeof(pszDest), "Hello");
    DisplayResult(hr);

    // String size TOO big
    hr = StringCchCopy(pszDest, sizeof(pszDest), "This is a rather long string");
    DisplayResult(hr);

    TCHAR pszFullName[25] = "Oliver";
    TCHAR *pszSrc = " Hughes";

    // Resultant string less than 25
    hr = StringCchCat(pszFullName, sizeof(pszFullName), pszSrc);
    DisplayResult(hr);

    // Now we'll add more to the string
    hr = StringCchCat(pszFullName, sizeof(pszFullName), " James Hughes ");
    DisplayResult(hr);

    printf("\nPress a key to continue\n");
    getch();

    return 1;
}

Der jeweils zweite Parameter bei StringCchCopy und StringCchCat gibt die Größe des Puffers an. Die Funktion DisplayResult ermittelt die Beschreibung für die von den Stringfunktionen zurückgegebenen Fehlercodes und gibt diese in einem Konsolenfenster aus:

Der Vorgang wurde erfolgreich beendet.
Der an einen Systemaufruf übergebene Datenbereich ist zu klein.
Der Vorgang wurde erfolgreich beendet.
Der an einen Systemaufruf übergebene Datenbereich ist zu klein.

Bis hier sieht alles ganz vernünftig aus, richtig gefährlich wird es aber, wenn man das vorgestellte Beispiel mit Unicode kompiliert. Hierfür müssen lediglich alle Strings als Unicode gekennzeichnet werden, zusätzlich wird printf durch das Makro _tprintf ersetzt. Durch den konsequenten Einsatz der Wrapper-Makros aus TCHAR.H ist es sehr einfach, Ansi- und Unicode-Version einer Anwendung in einem Quelltext zu verwalten. Anhand der Verwendung von _tmain als Hauptfunktion und TCHAR als universellen Datentyp für Ansi-/Unicode-Zeichen sieht man, dass der Autor des Beispiels eine Verwendung für Unicode bereits angedacht hat.

void DisplayResult(HRESULT hr);

int _tmain(int argc, TCHAR *argv[])
{
    TCHAR pszDest[10];

    // String size OK
    HRESULT hr = StringCchCopy(pszDest, sizeof(pszDest), _T("Hello"));
    DisplayResult(hr);

    // String size TOO big
    hr = StringCchCopy(pszDest, sizeof(pszDest), _T("This is a rather long string"));
    DisplayResult(hr);

    TCHAR pszFullName[25] = _T("Oliver");
    TCHAR *pszSrc = _T(" Hughes");

    // Resultant string less than 25
    hr = StringCchCat(pszFullName, sizeof(pszFullName), pszSrc);
    DisplayResult(hr);

    // Now we'll add more to the string
    hr = StringCchCat(pszFullName, sizeof(pszFullName), _T(" James Hughes "));
    DisplayResult(hr);

    _tprintf(_T("\nPress a key to continue\n"));
    getch();

    return 1;
}

Führt man das Programm nun aus, dann werden folgende Meldungen ausgegeben:

Der Vorgang wurde erfolgreich beendet.
Der an einen Systemaufruf übergebene Datenbereich ist zu klein.
Der Vorgang wurde erfolgreich beendet.
Der Vorgang wurde erfolgreich beendet.

Anschließend erscheint ein Hinweis von Windows:

—————————
test.exe – Fehler in Anwendung
—————————
Die Anweisung in „0x00680074“ verweist auf Speicher in „0x00000001“. Der Vorgang

„written“ konnte nicht auf dem Speicher durchgeführt werden.

Klicken Sie auf „OK“, um das Programm zu beenden.
Klicken Sie auf „Abbrechen“, um das Programm zu debuggen.
—————————
OK Abbrechen
—————————

Nun haben wir das, was eigentlich durch die Verwendung von sicheren Stringfunktionen vermieden werden sollte – einen waschechten Pufferüberlauf. Schaut man sich die Definition von StringCchCopy und StringCchCat nämlich genauer an, dann sieht man, dass diese Funktionen die Größe des Puffers in Zeichen und NICHT in Bytes haben möchten. Umso verwunderlicher ist das ganze, da in einem späteren Kapitel des Vortrags zu möglichen Unicode-Problemen mehrfach explizit darauf hingewiesen wird, dass Puffergrößen bei Unicode-Strings immer mit sizeof(WCHAR) berechnet werden sollen.

Im obigen Beispiel soll den beiden sicheren Stringfunktionen zugesichert werden, dass 10 beziehungsweise 25 Zeichen für den Text zur Verfügung stehen. In der Ansi-Version funktioniert das auch wunderbar, da hier ein Zeichen nur ein Byte groß ist und der sizeof-Operator 10 bzw. 25 zurückgibt. Unicode-Zeichen sind aber immer zwei Bytes groß, so dass der sizeof-Operator im Beispiel 20 bzw. 50 Bytes zurückgibt. Die beiden Stringfunktionen kopieren daher in Anbetracht des großen Puffers munter drauflos. Da aber in Wirklichkeit nur 10 bzw. 25 (und nicht 20 bzw. 50) Zeichen zur Verfügung stehen, kommt es zu einem Pufferüberlauf.

Vermeiden lässt sich das ganze, indem man bei Pufferangaben von Zeichenketten statt sizeof konsequent das Makro countof verwendet. Hierbei wird die von sizeof zurückgegebene Größe durch die Größe des ersten Elements geteilt, welches bei Ansi-Strings ein Byte und bei Unicode-Strings zwei Byte beträgt.

void DisplayResult(HRESULT hr);

#define countof(array) (sizeof(array)/sizeof(array[0]))

int _tmain(int argc, TCHAR *argv[])
{
    TCHAR pszDest[10];

    // String size OK
    HRESULT hr = StringCchCopy(pszDest, countof(pszDest), _T("Hello"));
    DisplayResult(hr);

    // String size TOO big
    hr = StringCchCopy(pszDest, countof(pszDest), _T("This is a rather long string"));
    DisplayResult(hr);

    TCHAR pszFullName[25] = _T("Oliver");
    TCHAR *pszSrc = _T(" Hughes");

    // Resultant string less than 25
    hr = StringCchCat(pszFullName, countof(pszFullName), pszSrc);
    DisplayResult(hr);

    // Now we'll add more to the string
    hr = StringCchCat(pszFullName, countof(pszFullName), _T(" James Hughes "));
    DisplayResult(hr);

    _tprintf(_T("\nPress a key to continue\n"));
    getch();

    return 1;
}

Die sicheren Stringfunktionen sind eine feine Sache und helfen bei richtiger Anwendung enorm, mögliche Pufferüberläufe schon im Ansatz zu bekämpfen. Es ist allerdings sehr schade, dass gerade dieses Beispiel auf der DVD einen solchen Patzer enthält. Entwickler, die beim Anschauen zum ersten Mal mit sicheren Stringfunktionen in Kontakt geraten, werden sich vermutlich am gezeigten Beispiel orientieren und damit unbeachsichtigt wieder potentielle Pufferüberläufe einbauen, die sie eigentlich vermeiden wollten.

Auf der Rückseite der DVD steht übrigens:

Start training to become our Security Champion! Pay close attention to the contents of this DVD and you’ll be ready to tackle almost any security question in the future. Have fun honing your skills!

Hoffentlich verlässt sich der eine oder andere Entwickler nicht zu sehr darauf.

Es gibt 2 Kommentare zu diesem Beitrag

Trackback URL | RSS-Feed für Kommentare

  1. Lefteous sagt:

    Man kann ja auch alternativ StringCbCopy und StringCbCat verwenden, wenn sich lieber an Bytes als an Zeichen orientieren möchte.

  2. Uwe Keim sagt:

    Oder gleich .NET verwenden 😀

Top