wake-up-neo.com

So schreiben Sie selbst modifizierenden Code in der x86-Assembly

Ich schreibe gerade einen JIT-Compiler für eine virtuelle Hobby-Maschine, an der ich kürzlich gearbeitet habe. Ich kenne ein bisschen Assembly (ich bin hauptsächlich ein C-Programmierer. Ich kann die meisten Assemblys mit Bezug auf Opcodes lesen, die ich nicht verstehe, und einige einfache Programme schreiben.), Aber ich kann die wenigen Beispiele nur schwer verstehen von selbst-modifizierendem Code, den ich online gefunden habe.

Dies ist ein solches Beispiel: http://asm.sourceforge.net/articles/smc.html

Das bereitgestellte Beispielprogramm führt vier verschiedene Modifikationen durch, von denen keine klar erklärt wird. Linux-Kernel-Interrupts werden mehrfach verwendet und nicht erklärt oder detailliert beschrieben. (Der Autor hat Daten in mehrere Register verschoben, bevor er die Interrupts aufrief. Ich gehe davon aus, dass er Argumente weitergegeben hat, aber diese Argumente werden nicht erklärt, sodass der Leser davon ausgehen kann.)

Was ich suche, ist das einfachste und einfachste Beispiel für den Code eines sich selbst ändernden Programms. Etwas, das ich betrachten kann und verwende, um zu verstehen, wie selbst modifizierender Code in x86 Assembly geschrieben werden muss und wie er funktioniert. Gibt es irgendwelche Ressourcen, auf die Sie mich hinweisen können, oder gibt es Beispiele, die dies ausreichend belegen könnten?

Ich verwende NASM als Assembler.

EDIT: Ich führe diesen Code auch unter Linux aus.

43
jakogut

wow, das war viel schmerzhafter als ich erwartet hatte. 100% der Schmerzen waren Linux, um das Programm vor dem Überschreiben und/oder Ausführen von Daten zu schützen.

Zwei Lösungen unten gezeigt. Und viel googeln war mit einbezogen, so dass der etwas einfache Befehl, einige Befehls-Bytes und ausführte, meiner gehörte. Der mprotect und das Ausrichten der Seitengröße wurde von Google-Suchvorgängen entfernt, was ich für dieses Beispiel lernen musste.

Der selbst modifizierende Code ist unkompliziert. Wenn Sie das Programm oder zumindest nur die beiden einfachen Funktionen verwenden, kompilieren und dann zerlegen, erhalten Sie die Opcodes für diese Anweisungen. oder nasm verwenden, um Assemblerblöcke usw. zu kompilieren. Daraufhin habe ich den Opcode ermittelt, um einen Direkteintrag in eax zu laden und dann zurückzukehren.

Idealerweise legen Sie diese Bytes einfach in einen RAM und führen diesen RAM aus. Um Linux dazu zu bringen, müssen Sie den Schutz ändern, das heißt, Sie müssen ihm einen Zeiger senden, der auf einer mmap-Seite ausgerichtet ist. Weisen Sie also mehr zu, als Sie benötigen, suchen Sie die ausgerichtete Adresse innerhalb dieser Zuweisung, die sich an einer Seitengrenze befindet, mprotect von dieser Adresse und verwenden Sie diesen Speicher, um Ihre Opcodes abzulegen und dann auszuführen.

im zweiten Beispiel wird eine vorhandene Funktion in das Programm kompiliert. Aufgrund des Schutzmechanismus können Sie nicht einfach darauf zeigen und Bytes ändern. Sie müssen den Schreibschutz aufheben. Sie müssen also zum vorherigen Seitenbegrenzungsaufruf mprotect mit dieser Adresse und genügend Bytes zurückkehren, um den zu ändernden Code abzudecken. Dann können Sie die Bytes/Opcodes für diese Funktion auf beliebige Weise ändern (sofern Sie nicht in eine Funktion übergehen, die Sie weiterhin verwenden möchten) und sie ausführen. In diesem Fall können Sie sehen, dass fun() funktioniert. Dann ändere ich ihn, um einfach einen Wert zurückzugeben, ihn erneut aufzurufen, und jetzt wurde er geändert.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

unsigned char *testfun;

unsigned int fun ( unsigned int a )
{
    return(a+13);
}

unsigned int fun2 ( void )
{
    return(13);
}

int main ( void )
{
    unsigned int ra;
    unsigned int pagesize;
    unsigned char *ptr;
    unsigned int offset;

    pagesize=getpagesize();
    testfun=malloc(1023+pagesize+1);
    if(testfun==NULL) return(1);
    //need to align the address on a page boundary
    printf("%p\n",testfun);
    testfun = (unsigned char *)(((long)testfun + pagesize-1) & ~(pagesize-1));
    printf("%p\n",testfun);

    if(mprotect(testfun, 1024, PROT_READ|PROT_EXEC|PROT_WRITE))
    {
        printf("mprotect failed\n");
        return(1);
    }

    //400687: b8 0d 00 00 00          mov    $0xd,%eax
    //40068d: c3                      retq

    testfun[ 0]=0xb8;
    testfun[ 1]=0x0d;
    testfun[ 2]=0x00;
    testfun[ 3]=0x00;
    testfun[ 4]=0x00;
    testfun[ 5]=0xc3;

    ra=((unsigned int (*)())testfun)();
    printf("0x%02X\n",ra);


    testfun[ 0]=0xb8;
    testfun[ 1]=0x20;
    testfun[ 2]=0x00;
    testfun[ 3]=0x00;
    testfun[ 4]=0x00;
    testfun[ 5]=0xc3;

    ra=((unsigned int (*)())testfun)();
    printf("0x%02X\n",ra);


    printf("%p\n",fun);
    offset=(unsigned int)(((long)fun)&(pagesize-1));
    ptr=(unsigned char *)((long)fun&(~(pagesize-1)));


    printf("%p 0x%X\n",ptr,offset);

    if(mprotect(ptr, pagesize, PROT_READ|PROT_EXEC|PROT_WRITE))
    {
        printf("mprotect failed\n");
        return(1);
    }

    //for(ra=0;ra&lt;20;ra++) printf("0x%02X,",ptr[offset+ra]); printf("\n");

    ra=4;
    ra=fun(ra);
    printf("0x%02X\n",ra);

    ptr[offset+0]=0xb8;
    ptr[offset+1]=0x22;
    ptr[offset+2]=0x00;
    ptr[offset+3]=0x00;
    ptr[offset+4]=0x00;
    ptr[offset+5]=0xc3;

    ra=4;
    ra=fun(ra);
    printf("0x%02X\n",ra);

    return(0);
}
45
old_timer

Da Sie einen JIT-Compiler schreiben, möchten Sie wahrscheinlich nicht selbstmodifizierenden Code, sondern zur Laufzeit ausführbaren Code generieren. Dies sind zwei verschiedene Dinge. Selbständernder Code ist ein Code, der geändert wird, nachdem bereits gestartet wurde. Selbstmodifizierender Code hat bei modernen Prozessoren einen erheblichen Leistungsabfall zur Folge und wäre daher für einen JIT-Compiler unerwünscht.

Das Generieren von ausführbarem Code zur Laufzeit sollte eine einfache Sache sein, indem mmap () etwas Speicher mit den Berechtigungen PROT_EXEC und PROT_WRITE benötigt wird. Sie können auch mprotect () auf einem Speicher aufrufen, den Sie selbst zugewiesen haben, wie es oben bei dwelch der Fall war.

9
Josh Haberman

Sie können auch Projekte wie GNU lightning betrachten. Sie geben den Code für eine vereinfachte RISC-Maschine an und sie generiert die korrekte Maschine dynamisch.

Ein sehr reales Problem, über das Sie nachdenken sollten, ist die Anbindung an ausländische Bibliotheken. Sie werden wahrscheinlich mindestens einige Aufrufe/Vorgänge auf Systemebene unterstützen müssen, damit VM nützlich ist. Die Ratschläge von Kitsune sind ein guter Anfang, damit Sie über Anrufe auf Systemebene nachdenken. Möglicherweise verwenden Sie mprotect, um sicherzustellen, dass der von Ihnen geänderte Speicher legal ausgeführt werden kann. (@KitsuneYMG)

Einige FFI, die Aufrufe von in C geschriebenen dynamischen Bibliotheken zulassen, sollten ausreichen, um viele der betriebssystemspezifischen Details auszublenden. All diese Probleme können sich stark auf Ihr Design auswirken. Daher sollten Sie frühzeitig darüber nachdenken.

3
Kevin A. Naudé

Ein bisschen einfacheres Beispiel basierend auf dem obigen Beispiel. Danke an dwelch hat viel geholfen.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>

char buffer [0x2000];
void* bufferp;

char* hola_mundo = "Hola mundo!";
void (*_printf)(const char*,...);

void hola()
{ 
    _printf(hola_mundo);
}

int main ( void )
{
    //Compute the start of the page
    bufferp = (void*)( ((unsigned long)buffer+0x1000) & 0xfffff000 );
    if(mprotect(bufferp, 1024, PROT_READ|PROT_EXEC|PROT_WRITE))
    {
        printf("mprotect failed\n");
        return(1);
    }
    //The printf function has to be called by an exact address
    _printf = printf;

    //Copy the function hola into buffer
    memcpy(bufferp,(void*)hola,60 //Arbitrary size);


    ((void (*)())bufferp)();  

    return(0);
}
3
felknight

Ich arbeite an einem selbstmodifizierenden Spiel, um x86 Assembly zu unterrichten, und musste genau dieses Problem lösen. Ich habe die folgenden zwei Bibliotheken verwendet:

FASM-Assembler https://github.com/ZenLulz/Fasm.NET

UDIS86 Disassembler: https://github.com/vmt/udis86

Anweisungen werden mit Udis86 gelesen, der Benutzer kann sie als Zeichenfolge bearbeiten, und dann wird FASM zum Zusammenstellen der neuen Bytes verwendet. Diese können in den Arbeitsspeicher zurückgeschrieben werden, und wie andere Benutzer darauf hingewiesen haben, ist für das Zurückschreiben die Verwendung von VirtualProtect unter Windows oder mprotect unter Unix erforderlich.

Die Codebeispiele sind für StackOverflow nur ein bisschen lang, daher verweise ich auf einen Artikel, den ich mit Codebeispielen geschrieben habe:

https://medium.com/squallygame/how-we-wrote-a-self-hacking-game-in-c-d8b9f97bfa99

Ein funktionierendes Windows-Repo ist hier (sehr leicht):

https://github.com/Squalr/SelfHackingApp

Diese Beispiele sind unter Windows, aber es ist nur eine Frage des Austauschs von VirtualProtect gegen mprotect, damit dies für Linux funktioniert

2
Zachary Canann

Dies ist in AT & T Assembly geschrieben. Wie Sie an der Ausführung des Programms sehen können, hat sich die Ausgabe aufgrund von selbstmodifizierendem Code geändert. 

Zusammenstellung: gcc -m32 modify.s modify.c 

die Option -m32 wird verwendet, da das Beispiel auf 32-Bit-Computern funktioniert

Aessembly:

.globl f4
.data     

f4:
    pushl %ebp       #standard function start
    movl %esp,%ebp

f:
    movl $1,%eax # moving one to %eax
    movl $0,f+1  # overwriting operand in mov instuction over
                 # the new immediate value is now 0. f+1 is the place
                 # in the program for the first operand.

    popl %ebp    # standard end
    ret

C Testprogramm:

 #include <stdio.h>

 // Assembly function f4
 extern int f4();
 int main(void) {
 int i;
 for(i=0;i<6;++i) {
 printf("%d\n",f4());
 }
 return 0;
 }

Ausgabe:

1
0
0
0
0
0
1
Gregor Taube

Ich habe noch nie selbst modifizierenden Code geschrieben, obwohl ich ein grundlegendes Verständnis dafür habe, wie er funktioniert. Grundsätzlich schreiben Sie die Anweisungen, die Sie ausführen möchten, in den Speicher und springen dort hin. Der Prozessor interpretiert die Bytes, die Sie Anweisungen geschrieben haben, und versucht sie auszuführen. Zum Beispiel können Viren und Kopierschutzprogramme diese Technik verwenden.
Bezüglich der Systemaufrufe hatten Sie Recht, Argumente werden über Register übergeben. Für eine Referenz von Linux-Systemaufrufen und ihrem Argument, überprüfen Sie hier .

0
BlackBear