Comment écrire du code Auto-modifiable dans l'assemblage x86

Je cherche à écrire un compilateur JIT pour une machine virtuelle hobby sur laquelle j'ai travaillé récemment. Je connais un peu d'assemblage, (je suis principalement un programmeur C. Je peux lire la plupart des assembly avec référence pour les opcodes que je ne comprends pas, et écrire quelques programmes simples.) mais j'ai du mal à comprendre les quelques exemples de code Auto-modificateur que j'ai trouvés en ligne.

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

L'exemple de programme fourni fait environ quatre modifications différentes lors de l'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 expliqués du tout, laissant le lecteur deviner.)

Ce que je cherche est l'exemple le plus simple et le plus simple dans le code d'un programme Auto-modificateur. Quelque chose que je peux regarder at, et utiliser pour comprendre comment le code auto-modificateur dans l'assemblage x86 doit être écrit, et comment cela fonctionne. Existe-il des ressources que vous pouvez m'indiquer, ou des exemples, vous pouvez donner ce que démontrent cela?

J'utilise NASM comme mon assembleur.

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

43
demandé sur Seki 2011-01-27 07:53:02

7 réponses

Wow, cela s'est avéré être beaucoup plus douloureux que prévu. 100% de la douleur était linux protégeant le programme contre l'écrasement et / ou l'exécution de données.

Deux solutions présentées ci-dessous. Et beaucoup de googling a été impliqué donc le peu simple mettre quelques octets d'instruction et les exécuter était le mien, le mprotect et l'alignement sur la taille de la page a été abattu à partir de recherches google, des choses que j'ai dû apprendre pour cet exemple.

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

Idéalement, vous mettez simplement ces octets dans une ram et exécutez cette ram. Pour que linux le fasse, vous devez modifier la protection, ce qui signifie que vous devez lui envoyer un pointeur aligné sur une page mmap. Donc allouer plus de vous devez, trouver l'adresse alignée dans cette allocation qui est sur une limite de page et mprotect de cette adresse et utiliser cette mémoire pour mettre vos opcodes, puis exécuter.

Le deuxième exemple prend une fonction existante compilée dans le programme, encore une fois à cause du mécanisme de protection, vous ne pouvez pas simplement la pointer et changer les octets, vous devez la protéger des Écritures. Vous devez donc revenir à l'appel de limite de page précédent mprotect avec cette adresse et suffisamment d'octets pour couvrir le le code doit être modifié. Ensuite, vous pouvez changer les octets / opcodes pour cette fonction de la manière que vous voulez (tant que vous ne débordez pas dans une fonction que vous voulez continuer à utiliser) et l'exécuter. Dans ce cas, vous pouvez voir que fun() fonctionne, puis je le change pour simplement retourner une valeur, l'appeler à 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
répondu old_timer 2016-11-14 15:09:37

Puisque vous écrivez un compilateur JIT, vous ne voulez probablement pas de code auto-modificateur , vous voulez générer du code exécutable au moment de l'exécution. Ce sont deux choses différentes. Le code auto-modificateur est le code qui est modifié après qu'il a déjà commencé à fonctionner . Le code auto-modificateur a une grande pénalité de performance sur les processeurs modernes, et serait donc indésirable pour un compilateur JIT.

Générer du code exécutable à l'exécution devrait être une simple question de mmap() mémoire avec les autorisations PROT_EXEC et PROT_WRITE. Vous pouvez également appeler mprotect () sur une mémoire que vous vous êtes allouée, comme dwelch l'a fait ci-dessus.

9
répondu Josh Haberman 2011-01-30 07:48:37

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

Un problème très réel auquel vous devriez penser est l'interfaçage avec des bibliothèques étrangères. Vous aurez probablement besoin de prendre en charge au moins certains appels/opérations au niveau du système pour que votre machine virtuelle soit utile. Le Conseil de Kitsune est un bon début pour vous aider à penser aux appels au niveau du système. Vous utiliseriez probablement mprotect pour vous assurer que le la mémoire que vous avez modifiée devient légalement exécutable. (@KitsuneYMG)

Certains FFI permettant des appels à des bibliothèques dynamiques écrites en C devraient suffire à masquer beaucoup de détails spécifiques au système d'exploitation. Tous ces problèmes peuvent avoir un impact sur votre conception un peu, il est donc préférable de commencer à y penser tôt.

3
répondu Kevin A. Naudé 2011-01-27 07:18:50

Un 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
répondu felknight 2012-04-22 08:58:05

Je travaille sur un jeu auto-modifiable pour enseigner l'assemblage x86, et j'ai dû résoudre ce problème exact. J'ai utilisé les deux bibliothèques suivantes:

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

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

Les Instructions sont lues avec Udis86, l'utilisateur peut les éditer en tant que chaîne, puis FASM est utilisé pour assembler les nouveaux octets. Ceux-ci peuvent être écrits en mémoire, et comme d'autres utilisateurs l'ont souligné, le la réécriture nécessite L'utilisation de VirtualProtect sous Windows ou mprotect sous Unix.

Les exemples de code sont un peu longs pour StackOverflow, donc je vais vous référer à 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 repo Windows fonctionnel est ici (très léger):

Https://github.com/Squalr/SelfHackingApp

Ces exemples sont sur Windows, mais c'est juste un question de permuter VirtualProtect vers mprotect pour que cela fonctionne sous Linux

2
répondu Zachary Canann 2018-08-31 04:57:00

Ceci est écrit dans l'assemblage AT&T. Comme vous pouvez le voir à partir de l'exécution du programme, la sortie a changé en raison du code Auto-modificateur.

Compilation: gcc-M32 modifier.s modifier.c

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

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

 #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
répondu Gregor Taube 2016-05-21 18:27:54

Je n'ai jamais écrit de code Auto-modifiable, bien que j'aie une compréhension de base de son fonctionnement. Fondamentalement, vous écrivez sur la mémoire les instructions que vous voulez exécuter puis sautez là. Le processeur interprète ces octets que vous avez écrits une instruction et (essaie) 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 passés via des registres. Pour une référence des appels système linux et leur argument il suffit de cocher ici.

0
répondu BlackBear 2011-01-27 13:18:27