web-dev-qa-db-fra.com

Comment écrire du code à modification automatique dans un assemblage x86

Je cherche à écrire un compilateur JIT pour une machine virtuelle de loisir sur laquelle j'ai travaillé récemment. Je connais un peu Assembly (je suis principalement un programmeur C. Je peux lire la plupart des Assembly avec des références pour les opcodes que je ne comprends pas et écrire des programmes simples.) Mais j'ai du mal à comprendre les quelques exemples. du code d'auto-modification que j'ai trouvé en ligne.

Voici un exemple: http://asm.sourceforge.net/articles/smc.html

L'exemple de programme fourni effectue environ quatre modifications différentes lors de son exécution, dont aucune n'est clairement expliquée. Les interruptions du noyau Linux sont utilisées plusieurs fois et ne sont ni expliquées ni détaillées. (L'auteur a déplacé les données dans plusieurs registres avant d'appeler les interruptions. Je suppose qu'il passait des arguments, mais ces arguments ne sont pas du tout expliqués, ce qui laisse le lecteur deviné.)

Ce que je recherche, c’est l’exemple le plus simple et le plus direct en code d’un programme à modification automatique. Quelque chose que je puisse examiner et utiliser pour comprendre comment le code à modification automatique dans un assemblage x86 doit être écrit et comment il fonctionne. Y a-t-il des ressources que vous pouvez m'orienter ou des exemples que vous pouvez donner qui pourraient le démontrer de manière adéquate?

J'utilise NASM en tant qu'assembleur.

EDIT: J'exécute également ce code sous Linux.

43
jakogut

wow, cela s'est avéré être beaucoup plus douloureux que ce à quoi je m'attendais. 100% de la douleur était linux protégeant le programme d'être écrasé et/ou l'exécution de données.

Deux solutions présentées ci-dessous. Et beaucoup de recherches sur Google étaient impliquées, donc les octets d’instruction simples à exécuter et à exécuter étaient les miens, la protection de fichier et l’alignement sur la taille de la page ont été extraits de recherches sur Google.

Le code d'auto-modification est simple, si vous prenez le programme ou au moins juste les deux fonctions simples, compilez, puis désassemblez, vous obtiendrez les opcodes pour ces instructions. ou utiliser nasm pour compiler des blocs d'assembleur, etc. À partir de cela, j'ai déterminé l'opcode pour charger un immédiat dans eax puis y revenir.

Idéalement, vous mettez simplement ces octets dans un bélier et vous l'exécutez. Pour que Linux fasse cela, vous devez changer la protection, ce qui signifie que vous devez lui envoyer un pointeur aligné sur une page mmap. Par conséquent, allouez plus que ce dont vous avez besoin, recherchez l'adresse alignée dans l'allocation qui se trouve sur une limite de page, protégez-vous de cette adresse et utilisez cette mémoire pour placer vos codes d'opération, puis exécutez-la.

le second exemple prend une fonction existante compilée dans le programme, toujours à cause du mécanisme de protection que vous ne pouvez pas simplement pointer dessus et changer d'octets, vous devez la déprotéger des écritures. Vous devez donc sauvegarder l'appel précédent, mprotect, avec cette adresse et suffisamment d'octets pour couvrir le code à modifier. Ensuite, vous pouvez modifier les octets/codes d'opération pour cette fonction comme vous le souhaitez (tant que vous ne vous étendez pas dans une fonction que vous souhaitez continuer à utiliser) et l'exécuter. Dans ce cas, vous pouvez voir que fun() fonctionne, puis je le modifie pour renvoyer simplement une valeur, appelez-le à nouveau et maintenant, il a été modifié.

#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

Puisque vous écrivez un compilateur JIT, vous ne voulez probablement pas modifier vous-même code, vous voulez générer du code exécutable au moment de l’exécution. Ce sont deux choses différentes. Le code à modification automatique est le code qui est modifié après avoir déjà commencé à exécuter . Le code auto-modificateur a un impact important sur les performances des processeurs modernes et ne serait donc pas souhaitable pour un compilateur JIT.

La génération de code exécutable au moment de l’exécution devrait être une simple affaire de mmap () avec de la mémoire avec les permissions PROT_EXEC et PROT_WRITE. Vous pouvez également appeler mprotect () sur une mémoire que vous avez allouée, comme l'a fait dwelch ci-dessus.

9
Josh Haberman

Vous pouvez également regarder des projets tels que GNU lightning . Vous lui donnez du code pour une machine simplifiée de type RISC et elle génère dynamiquement la machine correcte.

Un problème très réel auquel vous devriez penser est celui de l’interface avec les bibliothèques étrangères. Vous aurez probablement besoin de prendre en charge au moins certaines opérations/appels au niveau système pour que votre VM soit utile. Le conseil de Kitsune est un bon début pour vous faire réfléchir aux appels au niveau du système. Vous utiliserez probablement mprotect pour vous assurer que la mémoire que vous avez modifiée devient légalement exécutable. (@KitsuneYMG)

Certaines FFI autorisant les appels vers des bibliothèques dynamiques écrites en C devraient suffire à masquer une grande partie des détails propres au système d'exploitation. Tous ces problèmes peuvent avoir un impact considérable sur votre conception. Il est donc préférable de commencer à y penser tôt.

3
Kevin A. Naudé

Exemple un peu plus simple basé sur l'exemple ci-dessus. Merci à Dwelch a beaucoup aidé.

#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

Je travaille sur un jeu à modification automatique pour enseigner l'assemblage x86, et je devais résoudre ce problème. J'ai utilisé les deux bibliothèques suivantes:

Assembleur FASM https://github.com/ZenLulz/Fasm.NET

Désassembleur UDIS86: https://github.com/vmt/udis86

Les instructions sont lues avec Udis86, l'utilisateur peut les éditer sous forme de chaîne, puis FASM est utilisé pour assembler les nouveaux octets. Celles-ci peuvent être réécrites en mémoire et, comme l'ont souligné d'autres utilisateurs, l'écriture en arrière nécessite l'utilisation de VirtualProtect sous Windows ou de mprotect sous Unix.

Les exemples de code sont un peu longs pour StackOverflow, je vous renvoie donc à un article que j'ai écrit avec des exemples de code:

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

Un référentiel Windows fonctionnel est ici (très léger):

https://github.com/Squalr/SelfHackingApp

Ces exemples concernent Windows, mais il suffit simplement de remplacer VirtualProtect par mprotect pour que cela fonctionne avec Linux.

2
Zachary Canann

Ceci est écrit dans AT & T Assembly. Comme vous pouvez le constater lors de l'exécution du programme, la sortie a été modifiée à cause d'un code à modification automatique. 

Compilation: gcc -m32 modify.s modify.c 

l'option -m32 est utilisée car l'exemple fonctionne sur des machines 32 bits

Assemblage:

.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

Programme de test C:

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

Sortie:

1
0
0
0
0
0
1
Gregor Taube

Je n'ai jamais écrit de code à modification automatique, même si j'ai une compréhension de base de son fonctionnement. Fondamentalement, vous écrivez en mémoire les instructions que vous souhaitez exécuter, puis vous y accédez. Le processeur interprète les octets dans lesquels vous avez écrit des instructions et (tente) de les exécuter. Par exemple, les virus et les programmes anti-copie peuvent utiliser cette technique.
En ce qui concerne les appels système, vous aviez raison, les arguments sont transmis via des registres. Pour une référence des appels système Linux et leurs arguments, vérifiez simplement ici .

0
BlackBear