Wie sieht Code, der nen Pufferüberlauf als Sicherheitslücke hat, aus?



  • Kann dazu mal jemand ein Beispiel mit Quellcode geben.





  • Der einfachste Code ist eigentlich sowas schon:

    #include <stdio.h>
    
    void some_func()
    {
    	char buf[128];
    	gets(buf);
    }
    
    void main()
    {
    	some_func();
    }
    

    Der Nutzer muss jetzt nur etwas eingeben, was größer als der Puffer ist und gleichzeitig auch so intelligent ist, dass die Anweisungen für die Rücksprungadresse überschrieben wird. Und damit ist er drin...

    Gibt im Tanenbaum dazu einige Seiten, ist ganz interessant.



  • Ich werde eure Links und die Buchempfehlung* mal durchlesen.

    Aber welcher Tanenbaum? Ich habe hier von Tanenbaum die Bücher über "Betriebssysteme", "Computernetzwerke" und "Computerarchitektur".



  • Auch beliebt in der Art

    struct foo
    {
        int blah;
        char dings[100];
    };
    
    void fun(char* dings)
    {
       foo f;
       f.blah = 123;
       strcpy(f.dings, dings);
       do_some_foo(&f);
    }
    

    Oder auch einfach

    void do_some_foo(char* path)
    {
        char buffer[100];
        sprintf(buffer, "doing some foo with %s.", path);
        write_trace_message(buffer);
        // ...
    }
    


  • Ich kenn mich nicht allzu gut damit aus, aber das sieht doch so aus, dass sich viele Sicherheitslücken dieser Art vermeiden lassen, wenn man dynamische Dinge wie std::string (in C++) statt char-Arrays nimmt?



  • Alle.



  • RonjaRaeuber schrieb:

    Ich kenn mich nicht allzu gut damit aus, aber das sieht doch so aus, dass sich viele Sicherheitslücken dieser Art vermeiden lassen, wenn man dynamische Dinge wie std::string (in C++) statt char-Arrays nimmt?

    Ja, klar.

    Aber std::string und andere Dinge die dynamische Speicherverwaltung verwenden verwenden eben leider dynamische Speicherverwaltung. Was heisst: sie sind bei sehr einfachen Aufgaben um ein gutes Stück langsamer als wenn man ein fixed-size Array verwendet.
    Und daher sind fixed-size Array halt - je nach Anwendung - immer noch recht beliebt.

    Und natürlich gibt es Leute die einfach stur das runterklopfen was sie irgendwann kurz nachm Krieg mal gelernt haben, und sich weigern irgendwas anders zu machen/dazuzulernen/...


  • Mod

    Das eigentliche Problem ist nicht statisch vs dynamisch, sondern fehlende Bereichsprüfungen. std::string hat bei seinen Leseoperationen schon Bereichsprüfungen drin, weil die in die std::string-Methoden so eingebaut sind und man sie daher nicht vergessen kann. Ein mit malloc allokiertes Array kann man genau so überlaufen lassen, wie ein statisches, wenn entsprechende Prüfungen fehlen. Es ist bloß schwieriger (aber ich will nicht sagen unmöglich), einen Schadcode einzuschleusen. Man kann jedoch auf jeden Fall das Programm dadurch einfach zum Absturz bringen, was oft schon schlimm genug ist.



  • @SeppJ
    Naja... oft macht das Dynamische Anfordern die Bereichsprüfung hinfällig, nämlich wenn man sinnvollerweise so viel anfordert wie man braucht, und das "wie man braucht" halt sinnvollerweise auch korrekt ermittelt.

    Das "eigentliche Problem" ist also nicht unbedingt das Fehlen der Bereichsprüfung, denn wie schon gesagt kommt man oft auch ohne Bereichsprüfung aus. Allgemein könnte man sagen: das eigentliche Problem ist schlechter Code 🤡
    (Oder man könnte auch sagen: die Kombination aus statischer Grösse + keine Bereichsprüfung)



  • Pufferüberlauf schrieb:

    Ich werde eure Links und die Buchempfehlung* mal durchlesen.

    Aber welcher Tanenbaum? Ich habe hier von Tanenbaum die Bücher über "Betriebssysteme", "Computernetzwerke" und "Computerarchitektur".

    Das über Betriebssysteme. Hab ich mir damals für die Klausur in eben diesem Fach zur Gemüte geführt, und ich fands gut. Vor allem halt das Kapitel über Sicherheit im OS, wo genau sowas erklärt wurde.



  • Gibt es so ne Art Quota für Prozesse?

    Das brauche ich nämlich:

    http://www.heise.de/security/artikel/Windows-Speicherverwuerfelung-ausgetrickst-1790963.html



  • RonjaRaeuber schrieb:

    Ich kenn mich nicht allzu gut damit aus, aber das sieht doch so aus, dass sich viele Sicherheitslücken dieser Art vermeiden lassen, wenn man dynamische Dinge wie std::string (in C++) statt char-Arrays nimmt?

    Nur kann man die eben nicht immer gebrauchen. An manchen Ecken ist man einfach auf char-Arrays ( egal ob fixed-size oder dynamisch ) angewiesen.

    Ich versuche überall wo es aus meiner Sicht Sinn macht, schon aus Gründen der Faulheit, STL-Strings zu verwenden. Wenns aber eine performancekritische Ecke ist oder ein Anwendungsfall, wo 0-Zeichen auch mal spontan auftauchen, dann verzichte ich leichten Herzens auf den STL-String.

    Wie schon jemand ansatzweise sagte: Das Problem ist nicht die Verwendung einer bestimmte Klasse, sondern das Problem ist schlampiger Code 😉


  • Mod

    It0101 schrieb:

    An manchen Ecken ist man einfach auf char-Arrays ( egal ob fixed-size oder dynamisch ) angewiesen.

    Ich versuche überall wo es aus meiner Sicht Sinn macht, schon aus Gründen der Faulheit, STL-Strings zu verwenden. Wenns aber eine performancekritische Ecke ist oder ein Anwendungsfall, wo 0-Zeichen auch mal spontan auftauchen, dann verzichte ich leichten Herzens auf den STL-String.

    Wenn du ein char-Array wegen Nullzeichen vorziehst, dann hast du std::string nicht verstanden (der hervorragend mit Nullzeichen klarkommt). Wenn du ein dynamisches char-Array einem std::string vorziehst, insbesondere aus Performancegründen, dann machst du etwas falsch, denn std::string ist ein dynamisches char-Array und somit zumindest nicht langsamer (eventuell ein bisschen speicherfressender), im Optimalfall sogar schneller (weil die Entwickler Zeit hatten für Optimierungen (kurze Strings), die du niemals selber implementieren würdest).

    Weitere Vorteile: Die im Thread genannten Gefahren entfallen, ebenso mögliche Fehler der dynamischen Speicherverwaltung
    Weitere Nachteile: Auch mit std::string kann man Mist machen, insbesondere unnötige Kopien. Aber das sind eher Anfängerfehler. Bereichsprüfungsfehler und Fehler bei manueller Speicherverwaltung machen auch erfahrene Programmierer noch.



  • Mir ist schon klar dass das geht, aber wenn ich mit 0-Zeichen arbeite, also z.B. mit Blöcken von Binärdaten, oder irgendwelchen Messages, die aus dem Socket rausfallen oder dort einwandern, dann arbeite ich viel mit den mem-Funktionen und da halte ich mich weitestgehend von den STL-Strings fern. Ist vermutlich eher eine Frage der persönlichen Vorliebe.

    Wenn man sich nicht den "C++-Only"-Dogmen unterwirft, kann man da relativ frei agieren.



  • SeppJ schrieb:

    (weil die Entwickler Zeit hatten für Optimierungen (kurze Strings), die du niemals selber implementieren würdest).

    Es gibt nur eine Implementation von std::string ?

    Also ich würde meine Hand nicht für jede STL Implementation eines Compileranbieters ins Feuer legen...

    Im Zweifelsfalle müsste man vor der Benutzung eines Compilers erstmal alle mitgelieferten Quellcodes studieren, wenn man nicht blind vertrauen will.

    Und ich persönlich traue einem Compiler nur soweit wie ich ihn werfen kann. Das Problem: Compiler kann man nicht werfen 😃


  • Mod

    It0101 schrieb:

    Wenn man sich nicht den "C++-Only"-Dogmen unterwirft, kann man da relativ frei agieren.

    Das ist nicht dogmatisch. Wenn du einerseits (richtig) argumentierst, dass schlampiger Code die Ursache für Sicherheitsprobleme ist, dann ist es verwunderlich, dass du Verfahren propagierst, die eben diese Schlampigkeit provozieren, obwohl doch Alternativen zur Verfügung stehen, die genau das gleiche machen, dabei aber (kostenlos!) vor vielen Fehlern schützen. std::string mag ein überladenes Interface haben (die STL-Container sind da viel besser), aber er ist nicht inhärent langsamer als ein ge-malloc-tes char-Array.

    Bei den IO-Streams kann man auch gute Gründe (insbesondere Performance) gegen ihre Benutzung finden. Dort ist dogmatisches Festhalten viel verbreiteter. Bei der Ablehnung der STL-Container habe ich hingegen eher das Gefühl, dass hier oft ehemalige C-Programmierer kategorisch alles ablehnen, was C++ ist, weil C++ von sich aus langsamer sein muss als C, ohne sich mal mit der Technik dahinter zu beschäftigen oder gar zu vergleichen.



  • SeppJ schrieb:

    dass du Verfahren propagierst, die eben diese Schlampigkeit provozieren, obwohl doch Alternativen zur Verfügung stehen

    Wie ich schon sagte: Wenn Alternativen zur Verfügung stehen, die in dem speziellen Fall besser/praktischer/handlicher sind, dann ist nur jedem anzuraten, diese auch zu verwenden.

    Z.B. im Umgang mit Blöcken von Binärdaten halte ich die STL für unbrauchbar. Es gibt zwar hier Freunde von vector<char> aber ich persönlich halte davon nix. vector<char> mag evtl. sogar an die Performance normaler char* Blöcke herankommen, aber es trifft einfach nicht meinen Geschmack. Es ist aus meiner Sicht sogar Ausdruck der absolut zwanghaften und kranken Mentalität die sich ausgebreitet hat, die STL um jeden Preis zu nutzen. Sieht man hier im C++-Forum relativ häufig. Ich behalte mir immer Alternativen vor und lasse mir so genug Freiraum in der Wahl meiner Waffen.
    Fällt aber vermutlich ebenfalls in die Kategorie des persönlichen Geschmacks, auch wenn natürlich, wie du zurecht feststellst, das Fehlerrisiko bei char* höher ist als bei z.b. vector<char> oder String.


  • Mod

    Wenn dein Argument persönlicher Geschmack ist, dann ist das kein überzeugendes Argument. Besonders wenn du das was nicht deinen Geschmack trifft auch noch als krankhaft abstempelst. Was soll denn an vector<char> für Blöcke auszusetzen sein, außer, dass du persönlich es nicht magst?



  • So ich bin mal über meinen Schatten gesprungen 🙂

    Hier mal die STL String-Version vs. die char* Version:

    unsigned iterationen = 10000000;
    {
        // Stringverkettung mit strncpy
        long t1 = GetTickCount();
    
        const char *TestString = "abcedfghijklmnopqrstuvwxyz";
        unsigned Len = (unsigned)strlen( TestString );
        char *Buffer = new char[ iterationen * sizeof( char ) * Len ];
        unsigned Size = 0;
        for ( unsigned i = 0; i < iterationen; ++i )
        {
            strncpy( Buffer + Size, TestString , Len );
            Size += Len;
        }
    
        long t2 = GetTickCount();
        delete [] Buffer;
        std::cout << t2 - t1 << std::endl;
    }
    {
        // Stringverkettung mit STL-Strings
        long t1 = GetTickCount();
    
        std::string S1( "abcedfghijklmnopqrstuvwxyz" );
        std::string Kette = "";
        Kette.reserve( iterationen * S1.length() );
        for ( unsigned i = 0; i < iterationen; ++i )
            Kette += S1;
    
        long t2 = GetTickCount();
        std::cout << t2 - t1 << std::endl;
    }
    

    Kann mir bitte jemand sagen, was hier an der STL-String-Variante noch nicht optimal programmiert ist? Ich habe reserve genutzt, da mir ja die Gesamtlänge des Strings vorher bekannt ist. Somit müsste eigentlich die STL-Variante fast genauso schnell sein wie die char*-Variante. Was hab ich falsch gemacht, dass die STL-Variante so spürbar langsamer ist?

    Anmerkung:
    gebaut mit Codeblocks 10.05, Compiler: GCC, mit Optimierungsflag -O3

    Laufzeit der strncpy-Variante: 400ms
    Laufzeit der STL-String-Variante: 500ms


Anmelden zum Antworten