assembly x86 jit vm-implementation self-modifying

assembly - Cómo escribir código de auto modificación en ensamblaje x86



jit vm-implementation (6)

Dado que está escribiendo un compilador JIT, probablemente no desee el código de modificación automática , desea generar código ejecutable en tiempo de ejecución. Estas son dos cosas diferentes. El código de auto-modificación es un código que se modifica después de que ya ha comenzado a ejecutarse . 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.

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.

Este es uno de esos ejemplos: http://asm.sourceforge.net/articles/smc.html

El programa de ejemplo provisto hace aproximadamente cuatro modificaciones diferentes cuando se ejecuta, ninguna de las cuales se explica claramente. Las interrupciones del kernel de Linux se usan varias veces y no se explican ni detallan. (El autor movió los 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 ejemplo más sencillo y 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.


Esto está escrito en ensamblaje de AT & T. 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 usa porque el ejemplo funciona en máquinas de 32 bits

Asamblea:

.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

Programa de prueba 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; }

Salida:

1 0 0 0 0 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ías 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 .


También puede ver proyectos como el rayo GNU . 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 en el que debes pensar es interactuar 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.


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


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 directo, si toma el programa o al menos solo las dos funciones simples, compila y luego desarma 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); }