Wie existiert ein Programm im RAM, Debugen, Variablen im Ram, usw.



  • Wie existiert ein Programm im RAM, Debugen, Variablen im Ram, usw.
    Hi,
    Ich habe mehrere Fragen.
    Da ich mich auf Windows und VC++ beziehe, hab ichs mal hier rein gepostet.

    Ich hab ein bisschen auf der Seite vom Microsoft Visual C++ Toolkit 2003 rumgestöbert und bin auf folgenden Artikel gestoßen:
    Microsoft Visual C++ Toolkit 2003 - Security Checks at Runtime and Compile Time

    Da ich nicht auf dem "Oberflächigen Niveau" von C++ arbeiten will (sprich: möglichst viel Hochsprache verwenden), sondern die Materie etwas tiefer verstehen will, poste ich jetzt hier rein.

    1. Frage:
    Folgender fehlerhafter Code aus dem Artikel:

    void Test1()
    {
       char buffer1[100];
       for (int i=0 ; i < 200; i++)
       {
          buffer1[i] = 'a';
       }
       buffer1[sizeof(buffer1)-1] = 0;
       cout << buffer1 << endl;
    }
    

    Artikel schrieb:

    An attacker using a buffer overrun attack overwrites the return address with a carefully thought out one that would give the attacker control: this sample just writes 'a's (hex 61s) over the return address.

    Jetzt bin ich etwas verwirrt (was das Überschreiben der "return address" angeht). Ich versuche einmal zu schlussfolgern.
    Das OS läd ein Programm komplett in den Ram, wenn es ausgeführt wird.
    Ich gehe mal davon aus, dass jedes Byte nach dem PE-Header für eine Anweisung steht (da beim Debuggen die Bytes in asm Code übersetzt werden [liege ich da richtig]?). Somit würde ich daraus folgen, das folgender Code

    void test()
    {
       char t[2];
       return;
    }
    

    im Speicher folgendermaßen vorliegt:

    • 2 Byte für die Variable (das Arrays) t
    • 1 Byte mit der Sprunganweisung
    • 1 (oder 2-egal) Byte(s) für die Ziel-Sprungadresse

    Wenn meine Schlussfolgerungen richtig sind, könnte man die Zielsprung-Sprungadresse bei einer Rückkehr aus der Funktion test im Speicher (ungefähr) folgendermaßen "manipulieren":

    t[2] = 0xFFh; // als Bsp
    

    Liege ich mit meiner Schlussfolgerung richtig?
    Das würde ja heißen, dass ich den Programmcode im Speicher, wenn ich einen Zeiger auf die Startadresse meines Programmes (müsste doch eigendlich NULL sein, da Windows jedem Programm seinen eigenen Speicherbereich zuordnet) habe, dass ich dann ganz einfach über Zeigerarithmetik (über char) auf meinen eigenen Code zugreifen kann (lesen, schreiben).

    2. Frage:
    Folgender fehlerhafter Code aus dem Artikel:

    void Test2()
    {
       char buffer1[100];
       char buffer2[100];
       buffer1[0] = 0;
       for (int i=0 ; i <= sizeof(buffer2); i++)
       {
          buffer2[i] = 'a';
       }
       buffer2[sizeof(buffer2)-1] = 0;
       cout << buffer2 << '-' << buffer1 << endl;
    }
    

    Artikel schrieb:

    This loop overruns buffer2 by one character because it uses <= instead of <. It will overwrite the first character of buffer1.

    ??? (Unter der Annahme, dass meine bisherigen Schlussfolgerungen stimmen) buffer2 wird doch nach buffer1 deklariert. Wieso schreibt man dann in das Array buffer1, wenn man über buffer2 hinausschreibt (buffer1 ist ja vor buffer2 deklariert)?

    Und das hier:

    void Test3()
    {
       char buffer1[100];
       char buffer2[100];
       memset(buffer1,'a',sizeof(buffer1)-1);
       buffer1[sizeof(buffer1)-1]=0;
       memset(buffer2,'b',sizeof(buffer2)-1);
       buffer2[sizeof(buffer2)-1]=0;
       *(buffer1-1) = 'c';
       cout << buffer1 << endl;
       cout << buffer2 << endl;
     }
    

    Artikel schrieb:

    (To run Test 3, use the command "gs-rtc 3".) The last byte of buffer2 is overwritten in this case and the dialog finds the problem around buffer1. Without the runtime checks, the output of this test is:

    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    aaaaaaaaaaaaaaaaaaaaaaaa
    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

    Heißt das, wenn ich einen String ausgebe, die Ausgabefunktion so lange im Speicher sucht, bis sie ein '\n' findet (ist ja klar 😃 ). Aber wieso ist wieder buffer1 im Speicher nach buffer2??

    Ich fühle, dass ich mit meiner jetzigen Ansicht ziemlich daneben liege (einmal davon abgesehen, dass ich schon auf die Begriffe Codesegment usw. gestoßen bin 🙂 ).

    3. Frage (damit ihr mich nicht anmotzt, warum ich diesen Beitrag in dieses Forum gepostet habe 😉 ):
    Gibt es derartige (wie im Artikel beschriebene) Debugmöglichkeiten in VC++ 6.0? 😃

    Wow, wäre echt genial, wenn ihr mir helfen könnt mein Wissen zu berichtigen.
    Habt ihr mir vielleicht noch ein paar Artikel zu diesem Thema?

    Grüße Rapha
    ps. Ich weiß, dass ich ziemlich unkreativ im Titelfinden bin 🙂



  • Ich weiss es auch nicht wirklich und mich würde die richtige antwort auch interessieren aber das mit buffer1 und buffer2 macht imho doch sinn...

    buffer2 wird ja erst nach buffer1 definiert also vermutlich auch erst als zweites auf den stack gepackt... damit liegt es also über buffer1... wenn ich also bei der zweiten frage in buffer2[100] was schreibe ist das dann buffer1[0] weil der ja "unter" buffer2 auf dem stack liegt...

    analog bei drittens



  • Deine Annahmen mit den Rücksprungadressen sind falsch, da alle Adressen aus 32Bit
    bestehen.
    Aber mehr kann ich dir auch nicht helfen, sorry



  • Hi,

    @Windalf
    stimmt, du hast recht. Hatte ganz den Stack vergessen(der funktioniert ja anders rum) 🙄 .

    @SirLant
    Hm ok, stimmt natürlich. Ein Byte wäre ein bisschen wenig (0 bis 255) und war sowieso nur geschätzt 🙂 .

    Danke für eure Hilfe.

    erwartungsvolle Grüße
    Rapha



  • was mir so einfällt (Unter vorbehalt von Reihenfolge-Irrtümern 😃 )

    eine PE ist etwas komplizierter aufgebaut, da gibt es
    * Code-Segment(e) mit Assembler-Befehlen
    * Daten-Segment(e) mit Konstanten Daten (evtl?) getrennt nach read-only / read write. Dort gehen alle globalen Variablen rein, die initialisiert sind
    * "Platzhalter" für null-initialisierte Bereiche, dort gehen alle nicht-initialiserten globalen Variablen rein
    * Relokationstabelle (s.u.)
    * Ressourcen (fällt eigentlich unter "Daten", aber wird getrennt behandelt)
    * bestimmt noch anderer Krempel. am besten mal auf wotsit gucken.

    Im Header steht u.a. die Einsprungadresse

    Ein Prozeß bekommt seinen eigenen virtuellen Adreßraum von 4GB, (d.h. die Adresse X des einen Prozesses hat nich mit der Adresse X eines anderen Prozesses zu tun, aber natürlich sind nicht alle 4GB mit physischem Speicher "hinterlegt")

    Kann ein Prozeß nicht an seine "Wunschadresse" (für die er kompiliert ist) geladen werden, dann müssen alle Sprungadressen beim Laden entsprechend angepaßt werden - dazu die Reloc-Tabelle

    Dort kommen rein:

    - OS-Daten und Code (niedrige Adressen,meist 0..2GB)
    - EXE-Daten+Code
    - Heap
    - Stack (von oben nach unten)
    - User-DLL's (code+daten)

    Globale Variablen werden "einmal" abgelegt (der Speicher wird beim Laden der EXE fest zugeordnet)

    Dynamisch allozierte Daten (new, malloc etc.) werden auf dem Heap abgelegt. Der ist prinzipiell eine Liste von freien und benutzten Speicherschnipseln, die nach bedarf neu zugeordnet werden können.
    Windows erlaubt verschiedene Heaps mit verschiedenen Eigenschaften, auf diesen Heaps (mit 4K Allokationsgranularität) implementiert C++ wiederum seinen eigenen Heap (ich glaub 8 Byte granularität)

    Auf den Stack kommen:
    - lokale Variablen
    - Beim Funktionsaufruf: Aufrufparameter (je nach Aufrufkonvention) und Rücksprungadresse

    Nehmen wir mal an du hast eine Funktion

    int Foo(int x)
    {
      int y = rand(), z;
      z = x * y;
      return z;
    }
    
    // Aufruf:
    int main()
    {
      int result;
      result = Foo(3);
      cout << result;
      return 0;
    }
    

    Hier kommt auf den Stack (von hohen nach niedrigen Adressen):

    - Platz für "result". diese Variable wird nicht initialisiert
    - der Funktionsparameter für Foo() (3)
    - Die Rücksprungadresse (also die Adresse der cout - Anweisung)
    - in der Funktion Foo() dann Platz für y und z
    - Beim Aufruf von rand() kommt die Rücksprungadresse (also die Adresse von "z = x*y") auf den Stack.
    jetzt hoppeln wir wieder zurück: rand()-Rücksprungadresse vom Stack nehmen, Multiplikation ausführen, y und z vom Stack schmeißen (Ergebnis wird in einem Register zurückgegeben), Rücksprungadresse nach "cout" vom Stack, Paltz für Argument (3) vom Stack. Der Registerinhalt wird direkt auf die Stackadresse für "result" geschrieben (Adressierung relativ zum Stackzeiger)

    (Auch main wird wie eine Funktion aufgerufen, also liegen auf dem Stack jetzt noch mindestens der Speicher für "result", und die Rücksprungadresse von main() zum C/C++-Library-Startup code usw.)

    Funktionsparameter, lokale Variablen müssen auf dem Stack liegen, damit rekursive Funktionsaufrufe möglich sind.

    btw. C-Strings sind '\0' - terminiert (also ein Byte/wchar_t mit dem Wert 0), Funktionen wie printf rasseln also durch den Speicher bis sie eine 0 (nicht '\n') finden

    VC6 enthält für die C/C++ - Heapallokationen in den Debug-Build-Bibliotheken ähnliche Tests. starte einfach mal fongeldes programm unter dem Debugger:

    int main()
    {
      int * p = new int[1];
      p[1] = -1;
    }
    

    Im "Output" Fenster siehst du bereits eine Meldung über das Speicher-überschreiben und das Leck. Kommerzielle Tools (wie z.B. Boundschecker) erlaubel ähnliche Tests viel gründlicher

    Na gut, nicht ALLES was mir so einfällt, aber ich glaub ich sollte langsam ins Bett...



  • Wow super, danke für eure Mühe!! 👍 👍
    Besonders dir natürlich, peterchen 👍
    Ich werd mich in nächster Zeit noch weiter in dieser Richtung informieren.


Anmelden zum Antworten