2011-01-27 14 views
43

Sto cercando di scrivere un compilatore JIT per una macchina virtuale per hobby su cui ho lavorato di recente. Conosco un po 'di assemblaggio, (sono principalmente un programmatore C. Posso leggere la maggior parte degli assembly con riferimento per gli opcode che non capisco e scrivere alcuni semplici programmi.) Ma sto facendo fatica a capire i pochi esempi di codice auto-modificante che ho trovato online.Come scrivere codice auto-modificante nell'assemblaggio x86

Questo è un esempio: http://asm.sourceforge.net/articles/smc.html

Il programma esempio fornito fa circa quattro diverse modifiche quando eseguito, nessuno dei quali è chiaramente spiegato. Gli interrupt del kernel di Linux sono usati più volte e non sono spiegati o dettagliati. (L'autore ha spostato i dati in diversi registri prima di chiamare gli interrupt. Suppongo che passasse degli argomenti, ma questi argomenti non sono spiegati affatto, lasciando il lettore indovinare.)

Quello che sto cercando è il più semplice , esempio più semplice nel codice di un programma auto-modificante. Qualcosa che posso guardare e usare per capire come deve essere scritto il codice auto-modificante nell'assemblaggio x86 e come funziona. Ci sono delle risorse a cui puoi indirizzarmi, o qualche esempio che puoi dare che lo dimostrerebbe adeguatamente?

Sto usando NASM come mio assemblatore.

EDIT: Sto anche eseguendo questo codice su Linux.

+1

http://linux.die.net/man/2/mprotect dovrebbe spiegare quali sono gli argomenti per mprotect. L'ID funzione da chiamare viene passato in EAX e gli argomenti successivi vengono passati in EBX ECX ed EDX. – KitsuneYMG

risposta

44

wow, questo si è rivelato molto più doloroso di quanto mi aspettassi. Il 100% del dolore era la protezione del programma da sovrascrittura e/o esecuzione dei dati.

Due soluzioni illustrate di seguito. E un sacco di google è stato coinvolto in modo che il semplice mettere alcuni byte di istruzioni ed eseguirli fosse mio, il mprotect e l'allineamento sulla dimensione della pagina sono stati scelti dalle ricerche di google, cose che ho dovuto imparare per questo esempio.

Il codice di auto modifica è semplice, se si prende il programma o almeno solo le due semplici funzioni, si compila e poi si disassembla si ottengono gli opcode per tali istruzioni. o usare nasm per compilare blocchi di assembler, ecc. Da questo ho determinato il codice operativo per caricare un immediato in eax e poi tornare.

Idealmente si mettono semplicemente quei byte in una ram e si esegue quella ram. Per fare in modo che Linux faccia cambiare la protezione, significa che devi inviarlo a un puntatore che è allineato su una pagina mmap. Quindi allocare più del necessario, trovare l'indirizzo allineato all'interno di quell'allocazione che si trova sul limite della pagina e mprotect da tale indirizzo e utilizzare quella memoria per inserire i codici op e quindi eseguirli.

il secondo esempio prende una funzione esistente compilata nel programma, ancora una volta a causa del meccanismo di protezione non è possibile semplicemente indicarlo e modificare i byte, è necessario proteggerlo dalle scritture. Pertanto, è necessario eseguire il backup della pagina precedente con la chiamata mprotect con quell'indirizzo e abbastanza byte per coprire il codice da modificare. Quindi puoi cambiare i byte/opcode per quella funzione nel modo che preferisci (a condizione che non si riversi in alcuna funzione che vuoi continuare ad usare) ed eseguirla. In questo caso puoi vedere che fun() funziona, quindi lo cambio semplicemente per restituire un valore, richiamarlo di nuovo e ora è stato modificato.

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

non solo Linux, ma anche i più moderni sistemi operativi proteggono la memoria scrivibile dall'esecuzione di –

+0

Ciò può essere fatto in Windows, ovvero non protetto una pagina di RAM, o saremmo bloccati con schermi blu di morte? Voglio usare questo metodo per creare un sistema di crittografia auto-modificante. – tentimes

+0

Il codice ha funzionato bene su Arch Linux a 32 bit, ma non è riuscito su RHEL a 64 bit (sia l'ELF a 64 bit, ovviamente, ma anche quando si utilizza l'ELF a 32 bit). Non so se questo ha a che fare con la protezione aggiuntiva della memoria su RHEL o qualcos'altro.L'uscita era: '' ' 0x9a00008 0x9a01000 mprotect fallito ' '' – Alexander

3

Puoi anche guardare progetti come GNU lightning. Dagli un codice per una macchina di tipo RISC semplificata e genera la macchina corretta in modo dinamico.

Un problema molto reale a cui dovresti pensare è l'interfaccia con le biblioteche straniere. Probabilmente sarà necessario supportare almeno alcune chiamate/operazioni a livello di sistema affinché la VM sia utile. Il consiglio di Kitsune è un buon inizio per farti pensare a chiamate a livello di sistema. Probabilmente useresti mprotect per assicurarti che la memoria che hai modificato diventi legalmente eseguibile. (@KitsuneYMG)

Alcuni FFI che consentono chiamate a librerie dinamiche scritte in C dovrebbero essere sufficienti per nascondere molti dettagli specifici del sistema operativo. Tutti questi problemi possono influire sul tuo design un po ', quindi è meglio iniziare a pensarci prima.

0

Non ho mai scritto codice auto-modificante, anche se ho una conoscenza di base su come funziona. Fondamentalmente scrivi in ​​memoria le istruzioni che vuoi eseguire e poi salta li. Il processore interpreta quei byte che hai scritto un istruzioni e (tentativi) per eseguirli. Ad esempio, virus e programmi anti-copia possono usare questa tecnica.
Per quanto riguarda le chiamate di sistema, avevi ragione, gli argomenti sono passati attraverso i registri. Per un riferimento alle chiamate di sistema linux e ai loro argomenti, è sufficiente controllare here.

8

Dal momento che si sta scrivendo un compilatore JIT, probabilmente non vogliono automodificante codice, si desidera generare codice eseguibile in fase di esecuzione. Queste sono due cose diverse. Il codice auto-modificante è il codice che viene modificato dopo che è già stato avviato in esecuzione. Il codice auto-modificante ha una grande penalizzazione delle prestazioni sui processori moderni e pertanto sarebbe indesiderabile per un compilatore JIT.

Generare codice eseguibile in fase di runtime dovrebbe essere una semplice questione di mmap() di memoria con le autorizzazioni PROT_EXEC e PROT_WRITE. Puoi anche chiamare mprotect() su un po 'di memoria che hai assegnato tu stesso, come ha fatto dwelch sopra.

+0

Codice di Autodisciplina modifica non sempre ha avuto sanzioni prestazioni su processori moderni. Devi stare attento a ciò che cambi, e assicurarti che la cache della CPU sia sincronizzata e la protezione delle diramazioni non sia alterata. Cambiare quelli acquisterà la tua performance. – Beachhouse

+0

se l'automodificazione si verifica relativamente di rado e/o su parti del codice che non vengono eseguite _currently_, il risultato prestazionale temporaneo è trascurabile? –

3

Un esempio un po 'più semplice basato sull'esempio sopra. Grazie a dwelch ho aiutato molto.

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

Se non si genera codice indipendente dalla posizione per 'hola()', questo potrebbe fallire drasticamente. – CoffeeandCode

+0

Non funziona !! seg fault! – ANTHONY

0

Questo è scritto nell'assemblaggio T AT &. Come puoi vedere dall'esecuzione del programma, l'output è cambiato a causa del codice auto-modificante.

compilazione: gcc -m32 modify.s modify.c

l'opzione -m32 viene utilizzata perché l'esempio funziona su macchine a 32 bit

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 test-programma :

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

uscita:

1 
0 
0 
0 
0 
0