2011-01-27 11 views
43

Estoy buscando escribir un compilador JIT para una máquina virtual de hobby en la que he estado trabajando recientemente. Sé un poco de ensamblaje, (soy principalmente un programador de C. Puedo leer la mayoría del ensamblaje con referencia para los códigos de operación que no entiendo, y escribir algunos programas simples.) Pero estoy teniendo dificultades para entender los pocos ejemplos del código de auto modificación que he encontrado en línea.Cómo escribir código de auto modificación en ensamblado x86

Este es un ejemplo: http://asm.sourceforge.net/articles/smc.html

El programa de ejemplo proporcionado hace aproximadamente cuatro modificaciones diferentes cuando se ejecuta, ninguno de los cuales se explican claramente. Las interrupciones del kernel de Linux se usan varias veces y no se explican ni detallan. (El autor movió datos a varios registros antes de llamar a las interrupciones. Supongo que estaba pasando argumentos, pero estos argumentos no se explican en absoluto, dejando que el lector adivine.)

Lo que estoy buscando es el más simple , el ejemplo más directo en el código de un programa de auto modificación. Algo que puedo ver y usar para comprender cómo se debe escribir el código de auto modificación en el ensamblaje x86 y cómo funciona. ¿Hay algún recurso al que me pueda dirigir o algún ejemplo que pueda dar que demuestre esto adecuadamente?

Estoy usando NASM como mi ensamblador.

EDITAR: También estoy ejecutando este código en Linux.

+1

http://linux.die.net/man/2/mprotect debe explicar cuáles son los argumentos para mprotect. La función ID para llamar se pasa en EAX y los siguientes argumentos se pasan en EBX ECX y EDX. – KitsuneYMG

Respuesta

44

wow, esto resultó ser mucho más doloroso de lo que esperaba. El 100% del dolor fue Linux, protegiendo al programa de la sobreescritura y/o la ejecución de datos.

Dos soluciones se muestran a continuación. Y una gran cantidad de google se involucró así que el algo simple de poner algunos bytes de instrucciones y ejecutarlos fue mío, el mprotect y la alineación en el tamaño de la página fue seleccionado de las búsquedas de Google, cosas que tuve que aprender para este ejemplo.

El código de auto modificación es sencillo, si toma el programa o al menos solo las dos funciones simples, compile y luego desarme obtendrá los códigos de operación para esas instrucciones. o use nasm para compilar bloques de ensamblador, etc. De esto determiné el código de operación para cargar un inmediato en eax y luego regresar.

Lo ideal es poner esos bytes en algún ariete y ejecutar ese ariete. Para hacer que Linux haga eso, debe cambiar la protección, lo que significa que debe enviar un puntero que esté alineado en una página de mmap. Así que asigne más de lo que necesita, busque la dirección alineada dentro de esa asignación que está en un límite de página y proteja desde esa dirección y use esa memoria para poner sus códigos de operación y luego ejecutarlos.

el segundo ejemplo toma una función existente compilada en el programa, de nuevo debido a que el mecanismo de protección no puede simplemente señalarlo y cambiar los bytes, debe desprotegerlo de las escrituras. Por lo tanto, debe hacer una copia de seguridad de la llamada de límite de página anterior mprotect con esa dirección y suficientes bytes para cubrir el código que se va a modificar. Luego puede cambiar los bytes/códigos de operación para esa función de la forma que desee (siempre y cuando no se desborde en cualquier función que desee seguir usando) y ejecutarla. En este caso, puede ver que fun() funciona, luego lo cambio para simplemente devolver un valor, llamarlo nuevamente y ahora se ha modificado.

#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

no solo Linux, sino que la mayoría de los sistemas operativos modernos también protegen la memoria grabable de la ejecución de –

+0

¿Se puede hacer esto en Windows, es decir, desprotegiendo una página de RAM, o nos quedaríamos con pantallas azules de muerte? Quiero utilizar este método para crear un sistema de encriptación auto modificador. – tentimes

+0

El código funcionó bien en Arch Linux de 32 bits, pero falló en RHEL de 64 bits (tanto ELF de 64 bits, por supuesto, como también cuando se usa ELF de 32 bits). No sé si esto tiene que ver con la protección de memoria adicional en RHEL u otra cosa.La salida fue: '' ' 0x9a00008 0x9a01000 mprotect fallado ' '' – Alexander

3

También puede ver proyectos como GNU lightning. Usted le da el código para una máquina tipo RISC simplificada, y genera la máquina correcta de forma dinámica.

Un problema muy real que debe considerar es la interacción con bibliotecas extranjeras. Es probable que necesite admitir al menos algunas llamadas/operaciones a nivel del sistema para que su máquina virtual sea útil. El consejo de Kitsune es un buen comienzo para que piense acerca de las llamadas al nivel del sistema. Probablemente usará mprotect para asegurarse de que la memoria que ha modificado se convierta en ejecutable legalmente. (@KitsuneYMG)

Algunos FFI que permiten llamadas a bibliotecas dinámicas escritas en C deberían ser suficientes para ocultar una gran cantidad de detalles específicos del sistema operativo. Todos estos problemas pueden afectar un poco su diseño, por lo que es mejor comenzar a pensar en ellos desde el principio.

0

Nunca he escrito código de auto modificación, aunque tengo un conocimiento básico sobre cómo funciona. Básicamente, usted escribe en la memoria las instrucciones que desea ejecutar y luego salta allí. El procesador interpreta los bytes que ha escrito y las instrucciones (intentos) para ejecutarlos. Por ejemplo, los virus y los programas anticopia pueden usar esta técnica.
En cuanto a las llamadas al sistema, tenía razón, los argumentos se pasan a través de registros. Para obtener una referencia de las llamadas al sistema de Linux y su argumento, simplemente marque here.

8

Puesto que usted está escribiendo un compilador JIT, es probable que no quieren auto-modificable código, que desea generar código ejecutable en tiempo de ejecución. Estas son dos cosas diferentes. El código de auto-modificación es el código que se ha modificado después de que ya ha comenzado a funcionar. El código de auto-modificación tiene una gran penalización de rendimiento en los procesadores modernos, y por lo tanto sería indeseable para un compilador JIT.

Generar código ejecutable en tiempo de ejecución debería ser una cuestión simple de mmap() ing memoria con permisos PROT_EXEC y PROT_WRITE. También podría llamar a mprotect() en alguna memoria que haya asignado, como lo hizo Dwelch anteriormente.

+0

código auto modificación no siempre tenía penalizaciones de rendimiento en los procesadores modernos. Debe tener cuidado con lo que cambia y asegurarse de que la caché de la CPU esté sincronizada y de que no se modifique la protección de las ramas. Cambiarlos diluirá tu rendimiento. – Beachhouse

+0

si la auto modificación ocurre con poca frecuencia y/o en partes del código que no están actualmente en ejecución, ¿el rendimiento temporal es insignificante? –

3

Un ejemplo un poco más simple basado en el ejemplo anterior. Gracias a Dwelch ayudó mucho.

#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

Si no genera el código de posición independiente para 'hola()', esto podría fallar drásticamente. – CoffeeandCode

+0

¡No funciona! fallo seg! – ANTHONY

0

Esto está escrito en AT & T montaje. Como puede ver en la ejecución del programa, la salida ha cambiado debido a un código de modificación automática.

compilación: gcc -m32 modify.s modify.c

la opción -m32 se utiliza debido a que el ejemplo funciona en 32 bits máquinas

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-programa de prueba :

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

salida:

1 
0 
0 
0 
0 
0 
Cuestiones relacionadas