Vraag Hoe zelfmodificerende code te schrijven in x86-assembly


Ik kijk naar het schrijven van een JIT-compiler voor een hobby virtuele machine waar ik recent aan heb gewerkt. Ik ken een beetje assemblage, (ik ben voornamelijk een C-programmeur, ik kan de meeste assembly lezen met referentie voor opcodes die ik niet begrijp, en schrijf wat eenvoudige programma's.) Maar ik heb moeite met het begrijpen van de paar voorbeelden van zelfmodificerende code die ik online heb gevonden.

Dit is zo'n voorbeeld: http://asm.sourceforge.net/articles/smc.html

Het meegeleverde voorbeeldprogramma voert ongeveer vier verschillende wijzigingen uit wanneer ze worden uitgevoerd, maar geen daarvan wordt duidelijk uitgelegd. Linux-kernel-interrupts worden verschillende keren gebruikt en worden niet uitgelegd of gedetailleerd. (De auteur heeft gegevens naar verschillende registers verplaatst voordat de interrupts werden aangeroepen. Ik neem aan dat hij argumenten doorgeeft, maar deze argumenten worden helemaal niet uitgelegd, waardoor de lezer kan raden.)

Waar ik naar op zoek ben, is het eenvoudigste, meest directe voorbeeld in de code van een zelfmodificerend programma. Iets dat ik kan bekijken en gebruiken om te begrijpen hoe zelf-modificerende code in x86-assembly moet worden geschreven en hoe het werkt. Zijn er middelen waar u mij op kunt wijzen of welke voorbeelden u kunt geven die dit op gepaste wijze kunnen aantonen?

Ik gebruik NASM als mijn assembler.

EDIT: Ik voer deze code ook uit onder Linux.


42
2018-01-27 04:53


oorsprong


antwoorden:


wauw, dit bleek veel pijnlijker dan ik had verwacht. 100% van de pijn was linux die het programma beschermde tegen overschrijven en / of het uitvoeren van gegevens.

Twee oplossingen hieronder weergegeven. En er was veel googelen bij betrokken, dus het wat simpele zetten van wat instructieve bytes en ze uitvoeren was van mij, het mprotect en afstemmen op paginaformaat werd gehaald uit google-zoekopdrachten, dingen die ik voor dit voorbeeld moest leren.

De zelfmodificerende code is rechttoe rechtaan, als je het programma of tenminste alleen de twee eenvoudige functies neemt, compileert en vervolgens demonstreert, krijg je de opcodes voor die instructies. of gebruik nasm om blokken assembler te compileren, enz. Hiervan heb ik de opcode bepaald om onmiddellijk in eax te laden en vervolgens terug te keren.

Idealiter plaats je eenvoudig die bytes in een ram en voer die ram uit. Om Linux hiervoor te krijgen, moet je de bescherming wijzigen, wat betekent dat je het een aanwijzer moet sturen die op een mmap-pagina is uitgelijnd. Dus wijs meer toe dan u nodig heeft, zoek het uitgelijnde adres binnen die toewijzing op een paginarand en mprotect van dat adres en gebruik dat geheugen om uw opcodes in te stellen en vervolgens uit te voeren.

het tweede voorbeeld neemt een bestaande functie die is gecompileerd in het programma, opnieuw vanwege het beveiligingsmechanisme kun je er niet simpelweg naar verwijzen en bytes wijzigen, je moet het beschermen tegen schrijven. Dus u moet een back-up maken naar de voorafgaande pagina-grenslijn mprotect met dat adres en voldoende bytes om de te wijzigen code te dekken. Vervolgens kunt u de bytes / opcodes voor die functie op elke gewenste manier wijzigen (zolang u niet overloopt in een functie die u wilt blijven gebruiken) en deze uitvoeren. In dit geval kun je dat zien fun()werkt, dan verander ik het om gewoon een waarde terug te geven, het opnieuw te bellen en nu is het aangepast.

#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);
}

44
2018-01-27 16:38



Omdat je een JIT-compiler schrijft, wil je dat waarschijnlijk niet self-wijzigende code, wilt u uitvoerbare code genereren tijdens runtime. Dit zijn twee verschillende dingen. Zelfmodificerende code is code die is aangepast nadat het al is gestart. Zelfmodificerende code heeft een grote prestatievergoeding voor moderne processors en is daarom ongewenst voor een JIT-compiler.

Het genereren van uitvoerbare code tijdens runtime zou een simpele kwestie moeten zijn van het mmap () inlezen van enig geheugen met PROT_EXEC en PROT_WRITE permissies. U kunt mprotect () ook op een geheugen plaatsen dat u zelf hebt toegewezen, zoals dwelch hierboven deed.


8
2018-01-30 07:48



Je kunt ook naar projecten zoals kijken GNU-bliksem. U geeft het code voor een vereenvoudigde RISC-achtige machine en het genereert de juiste machine dynamisch.

Een heel reëel probleem waar u aan moet denken, is het samenwerken met buitenlandse bibliotheken. U zult waarschijnlijk op zijn minst enkele oproepen / operaties op systeemniveau moeten ondersteunen om uw VM nuttig te laten zijn. Het advies van Kitsune is een goed begin om u te laten nadenken over oproepen op systeemniveau. U zou waarschijnlijk mprotect gebruiken om ervoor te zorgen dat het geheugen dat u hebt aangepast, legaal uitvoerbaar wordt. (@KitsuneYMG)

Sommige FFI die oproepen naar dynamische bibliotheken toestaan ​​die zijn geschreven in C, zouden voldoende moeten zijn om veel OS-specifieke details te verbergen. Al deze problemen kunnen van grote invloed zijn op uw ontwerp, dus u kunt er best vroeg over beginnen nadenken.


3
2018-01-27 07:18



Een iets eenvoudiger voorbeeld op basis van het bovenstaande voorbeeld. Dank aan Dwelch veel geholpen.

#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
2018-04-22 08:58



Dit is geschreven in AT & T-assembly. Zoals je kunt zien aan de uitvoering van het programma, is de uitvoer veranderd door zelf-modificerende code.

Compilatie: gcc -m32 modify.s modify.c

de optie -m32 wordt gebruikt omdat het voorbeeld werkt op 32-bits machines

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-testprogramma:

 #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;
 }

Output:

1
0
0
0
0
0

1
2018-05-21 18:27



Ik heb zelf geen modificerende code geschreven, hoewel ik een basiskennis heb van hoe het werkt. In principe schrijf je in het geheugen de instructies die je wilt uitvoeren en spring je daar naartoe. De processor interpreteert die bytes die je hebt geschreven instructies en (probeert) ze uit te voeren. Virussen en antikopieerprogramma's kunnen deze techniek bijvoorbeeld gebruiken.
Wat de systeemaanroepen betreft, had u gelijk: argumenten worden via registers doorgegeven. Voor een verwijzing van linux-systeem-aanroepen en hun argument gewoon controleren hier.


0
2018-01-27 13:18