it-swarm.com.de

Wie läuft Shellcode wirklich ab?

Ich habe das Buch "The Shellcoders Handbook" gelesen und darin befindet sich ein C-Code, der Shellcode ausführt (es wird nur exit syscall aufgerufen).

 char shellcode [] = "\ xbb\x00\x00\x00\x00\xb8\x01\x00\x00\x00\xcd\x80"; 
 
 int main ( ) {
 int * ret; 
 ret = (int *) & ret + 2; 
 (* ret) = (int) Shellcode; 
} 

Ich interessiere mich für diese drei Zeilen in der Hauptfunktion. Was genau machen sie und wie führen sie den Shellcode aus?

Ich hätte es vielleicht herausgefunden: Bevor main auf den Stack aufgerufen wurde, wurden ebp und return address von einem vorherigen Stack-Frame gepusht, also überschreiben wir hier diese Adresse und platzieren unseren Shellcode dort. Ist das richtig?

18

TL; DR Dies ist eine Möglichkeit, Shellcode auszuführen, der nicht mehr funktioniert.

Was ist eine Funktion?

Shellcode ist nur Maschinencode an Stellen, an denen er normalerweise nicht gefunden wird, z. B. eine Variable vom Typ char. In C gibt es keinen Unterschied zwischen Funktionen und Variablen. Eine Funktion ist nur eine Variable, die auf ausführbaren Code verweist. Dies bedeutet, dass wenn Sie eine Variable erstellen, die auf ausführbaren Code verweist und ihn aufruft, als wäre es eine Funktion, diese ausgeführt wird. Um zu veranschaulichen, wie es sich nur um eine Variable handelt, sehen Sie sich dieses einfache Programm an:

#include <stdio.h>
#include <stdint.h>

void print_hello(void)
{
    printf("Hello, world!\n");
}

void main(void)
{
    uintptr_t new_print_hello;

    printf("print_hello = %p\n", print_hello);
    new_print_hello = (uintptr_t)print_hello;
    (*(void(*)())new_print_hello)();
    print_hello();
}

Wenn dieses Programm kompiliert und ausgeführt wird, gibt es folgende Ausgabe aus:

$ ./a.out
print_hello = 0x28bc4bf6da
Hello, world!
Hello, world!

Dies macht es leicht zu erkennen, dass eine Funktion nichts anderes als eine Adresse im Speicher ist, die mit dem Typ uintptr_t Kompatibel ist. Sie können sehen, wie eine Funktion einfach als Variable referenziert werden kann, in diesem Fall indem Sie ihren Wert drucken oder sie in eine andere Variable eines kompatiblen Typs kopieren und die Variable wie eine Funktion aufrufen, wenn auch mit ein wenig Zauberei um den C-Compiler glücklich zu machen. Sobald Sie sehen, dass eine Funktion nichts anderes als eine Variable ist, die auf einen ausführbaren Speicher verweist, ist es nicht schwer zu sehen, wie eine Variable, die auf einen manuell definierten Bytecode verweist, auch ausgeführt werden kann.

Wie funktionieren Funktionen?

Jetzt, da Sie wissen, dass eine Funktion nur eine Adresse im Speicher ist, müssen Sie wissen, wie eine Funktion tatsächlich ausgeführt wird. Sobald Sie eine Funktion aufrufen, normalerweise mit der Anweisung call, ändert sich der Anweisungszeiger (der auf die aktuell ausgeführte Anweisung zeigt) und zeigt auf die erste Anweisung der Funktion. Die Position unmittelbar vor dem Aufruf der Funktion wird von call im Stapel gespeichert. Sobald die Funktion beendet ist, wird sie mit der Anweisung ret beendet, die sie vom Stapel entfernt und auf der IP-Adresse speichert. Eine (etwas vereinfachte) Ansicht ist also, dass call die IP auf den Stapel schiebt und ret sie zurückspringt.

Abhängig von der Architektur und dem Betriebssystem, in dem Sie sich befinden, können die Argumente an die Funktion in Registern oder im Stapel übergeben werden, und der Rückgabewert kann in verschiedenen Registern oder im Stapel vorliegen. Dies wird als Funktionsaufruf ABI bezeichnet und ist für jeden Systemtyp spezifisch. Shellcode, der für einen Systemtyp entwickelt wurde, funktioniert möglicherweise nicht auf einem anderen, selbst wenn die Architektur identisch und das Betriebssystem unterschiedlich ist oder umgekehrt.

Was macht Ihr Shellcode?

Schauen wir uns die Demontage des von Ihnen bereitgestellten Shellcodes an:

0000000000201010 <shellcode>:
   201010:      bb 00 00 00 00          mov    ebx,0x0
   201015:      b8 01 00 00 00          mov    eax,0x1
   20101a:      cd 80                   int    0x80

Dies macht drei Dinge. Erstens setzt es das ebx auf 0. Zweitens setzt es das eax -Register auf 1. Schließlich löst es den Interrupt 0x80 aus, der auf 32-Bit-Systemen der Syscall-Interrupt ist. In der SysV, die ABI aufruft, wird die Syscall-Nummer in eax platziert, und bis zu 6 Argumente werden in ebx, ecx, edx, esi, übergeben. edi und ebp. In diesem Fall wird nur ebx gesetzt, was bedeutet, dass der Systemaufruf nur ein Argument akzeptiert. Sobald der 0x80-Interrupt aufgerufen wird, übernimmt der Kernel diese Werte und überprüft sie, wobei der richtige Systemaufruf ausgeführt wird. Die Systemrufnummern sind in /usr/include/asm/unistd_32.h Definiert. Wenn wir uns das ansehen, sehen wir, dass Syscall 1 exit() ist. Daraus können wir die drei Dinge erkennen, die dieser Shellcode bewirkt:

  1. Es setzt das erste Argument des Systemaufrufs auf 0 (was Exit-Erfolg bedeutet).
  2. Es setzt die Syscall-Nummer auf 1, was der Exit-Aufruf ist.
  3. Es ruft den Systemaufruf auf und bewirkt, dass das Programm mit dem Status 0 beendet wird.

Wenn Sie das Gesamtbild betrachten, sehen Sie, dass der Shellcode im Wesentlichen exit(0) entspricht. Es benötigt ret nicht, da es nie zurückkehrt und stattdessen das Programm beendet. Wenn Sie möchten, dass die Funktion zurückgegeben wird, müssen Sie am Ende ret hinzufügen. Wenn Sie zumindest nicht ret verwenden, stürzt das Programm ab, es sei denn, es wird beendet, bevor es das Ende der Funktion erreicht, wie in Ihrem Beispiel mit dem Systemaufruf exit().

Was ist los mit deinem Shellcode?

Die Methode zum Aufrufen des angezeigten Shellcodes funktioniert nicht mehr . Früher, aber heutzutage erlaubt Linux nicht, dass beliebige Daten ausgeführt werden, was ein arkanes Casting erforderlich macht. Diese ältere Technik wird im berühmten Smashing The Stack For Fun And Profit Artikel gut erklärt:

   Lets try to modify our first example so that it overwrites the return
address, and demonstrate how we can make it execute arbitrary code.  Just
before buffer1[] on the stack is SFP, and before it, the return address.
That is 4 bytes pass the end of buffer1[].  But remember that buffer1[] is
really 2 Word so its 8 bytes long.  So the return address is 12 bytes from
the start of buffer1[].  We'll modify the return value in such a way that the
assignment statement 'x = 1;' after the function call will be jumped.  To do
so we add 8 bytes to the return address.  Our code is now:

example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
   int *ret;

   ret = buffer1 + 12;
   (*ret) += 8;
}

void main() {
  int x;

  x = 0;
  function(1,2,3);
  x = 1;
  printf("%d\n",x);
}
------------------------------------------------------------------------------

   What we have done is add 12 to buffer1[]'s address.  This new address is
where the return address is stored.  We want to skip pass the assignment to
the printf call.  How did we know to add 8 to the return address?  We used a
test value first (for example 1), compiled the program, and then started gdb

Das korrekte Version Ihres Shellcodes für neuere Systeme wäre:

const char shellcode[] = “\xbb\x00\x00\x00\x00\xb8\x01\x00\x00\x00\xcd\x80”;

int main(){
    int (*ret)() = (int(*)())shellcode;
    ret();
}
39
forest