Está en la página 1de 19

Programacion en Assembler AUTOMUTACION DE CODIGO

Este documento se ha realizado con fines educativos. Pensando en el planteamiento de preguntas y sus respuestas online, todos los ejemplos tratados seran programas para MSDOS y usaremos TASM y TDEBUG para su compilacion y depuracion, permitiendo su seguimiento a traves del IRC. Los ejemplos y la charla estan basados en archivos de tipo .COM, que permiten mayor flexibilidad en cuanto a modificacion de datos en memoria. Si se quiere portar el codigo para conseguir un .EXE se debera tener en cuenta el modo en que se procesa los atributos de ejecucion, lectura y escritura sobre un segmento de memoria para evitar la generacion de excepciones y/o errores. Se trabajara con 16bits y .COM porque es la forma mas simple de ejecutar codigo en un PC. No hace falta que tengamos un Athlon a 800 Mhz para hacer pruebas.. con el 386 ese que teniamos por ahi tambien funcionaran. Este documento no esta enfocado a la explicacion de rutinas de tipo virico. Su intencion es la enseanza de tecnicas de programacion y su implantacion.

Tabla de Contenidos: Aplicaciones de la automutacion de codigo Las principales aplicaciones de esta tecnica de programacion son las siguientes: Optimizacion: de velocidad. de espacio. Tecnicas Anti-debug y Anti-dasm. Ocultacion de codigo. Polimorfismo. Evasion de tecnicas automatizadas. Diversion: Investigacion y desarrollo.

Optimizacion

Las caracteristicas principales en las tecnicas de optimizacion de codigo basadas en la automodificacion, van orientadas en estos dos focos principalmente: - Optimizacion de velocidad de ejecucion.

Se evita la ejecucion de instrucciones no necesarias mediante la generacion en tiempo de ejecucion de algoritmos especificos, utilizando partes de funciones genericas. La automodificacion aqui se encarga de convertir algoritmos genericos en funciones que realizan funciones "especificas" ahorrando tiempo de ejecucion al estar la rutina optimizada para la funcion que va a realizar. - Optimizacion de espacio necesario. Se optimiza el uso de espacio ocupado por el archivo y el uso de memoria necesaria para su ejecucion. En este apartado veremos como el propio codigo sirve como almacen para variables y como evitar repeticion de funciones que realizan procedimientos parecidos. Como inconvenientes (o ventajas si tratamos el tema desde el punto de vista de la proteccion) disminuyen las capacidades de depuracion al estar el codigo en constante modificacion en tiempo de ejecucion, y se incrementa la dificultad en cuanto a programacion y la lectura y comprension de codigo fuente. Programas que manejan gran cantidad de datos como los de gestion bancaria a nivel internacional son los unicos por ahora que aplican esta tecnica para la optimizacion. Aunque su uso puede ser facilmente implementado en programa que requieran gran cantidad de operaciones por segundo como renderizacion de imagenes en tiempo real o accesos a archivos de datos de gran tamao. Optimizacion: Velocidad El analisis de un programa es parte fundamental en su desarrollo. En determinadas partes de este es necesario que cierto proceso se ejecute de la forma mas rapida posible. Esto se puede conseguir analizando las partes del codigo que incluyan bucles anidados. Para ello, se debe comenzar a depurar sobre los bucles internos del programa, es decir, los que mas veces se repitan durante la ejecucion. Veamos este ejemplo: Codigo en ASM hipotetico mov cx, 0ffffh xor ax, ax IN1: -codigopush cx ; Guardamos contador de 1| bucle mov cx, 0ffffh IN2: -codigoadd ax,2 dec cx jnz IN2 ; Z flag se activa si "dec" genera un 0 pop cx ; Recuperamos contador dec cx jnz IN1 Su homonino en C seria algo asi:

Codigo en C hipotetico for (a=0; a<100; a++) { -Codigofor (b=0; b<100; b++) { -codigovar1=var1+2; } } En este codigo en C, la instruccion "var1=var1+2" se ejecuta 100 veces en el bucle mas interno, pero en total se ejecuta 100*100=10.000 veces. -Codigo-, representa cualquier otra linea o lineas de codigo que pueda contener el bucle. En el codigo en ensamblador, la instruccion "add ax, 2" es la que se ejecuta un numero elevado de veces. Los programas ejemplos seran todos una serie de bucles anidados en los que trabajando sobre el bucle interno, nos permitiran un analisis de tiempo de ejecucion y de la optimizacion conseguida. Como base para el primer ejemplo, trataremos las comparaciones. Pensemos en una rutina que sumara un valor a una variable, y en funcion de determinada flag, realizara una segunda suma sobre la misma variable. Codigo en C hipotetico int var = 0; boolean flag = FALSE; for (int a=0; a<1000; { for (int b=0; b<1000; { for (int c=0; c<1000; { var=var+0; //evitando if flag=TRUE { var==var+0; } } } a++) b++) c++) rebasar

Codigo en ASM del Archivo: ej1-1.asm en determinada parte del prg se setea el valor de FLAG mov bx, 0 ; flag a false ...... RUTINA: xor dx, mov cx, bucle1: ; xor ax, ax ; - variable var dx ; - 0 00ffh ; - contador push cx ; *primer bucle

mov cx, 00ffh ; bucle2: push cx ; mov cx, 0fffh ; bucle3: add ax, cmp bx,0 ; jz salta ; suma add ax, dx ; si salta: dec cx ; jnz bucle3 ; pop cx ; dec cx ; jnz bucle2 ; pop cx ; dec cx ; jnz bucle1 ; ....

; *segundo bucle *tercero bucle dx ; suma (reg+reg) reg+reg el flag esta o no a TRUE

La instruccion ADD REG+REG requiere 2 ciclos de maquina para completarse y jz consume 7+X ciclos dependiendo de la siguiente instruccion, o 3 si no se realiza el salto. Asi que tenemos entre 5(3+2) y 7 ciclos minimo, mas los 2 ciclos del CPM REG,INM ejecutandose 0fffh*0fffh*0fffh veces tanto si el flag esta o no activado. Posibles soluciones en cuanto a optimizacion mediante automodificacion hay varias. La mas sencilla seria NO comparar si el flag esta o no activado, elimimando de esta manera el CMP y el JMPS dentro de dicho bucle, que quedaria asi: Codigo en ASM hipotetico Bucle3: add ax, dx ; suma reg+reg IFFLAG: add ax, dx ; suma reg+reg si el flag esta o no a TRUE dec cx ; jnz Bucle3 ; Ahora, antes de entrar en el anillo de bucles, es cuando realizamos la comprobacion: cmp bx,0 ; jnz cont1 ; mov Word ptr [IFFLAG], 9090h ; 090h es el opcode de un NOP Lo que hace esa instruccion es sustituir el ADD AX, DX, por dos NOP, aunque puede ser sustituido por otras instrucciones que no realicen ninguna modificacion sobre el codigo, registros ni flags, y que aun disminuyan el numero de ciclos ejecutados. Codigo en ASM del Archivo: ej1-2.asm .... RUTINA: cmp bx,0 ; jnz cont1 ; mov Word ptr [IFFLAG], 9090h ; NOP & NOP cont1: xor ax, ax ; - variable var xor dx, dx ; - 0 mov cx, 00ffh ; - contador bucle1: push cx ; *primer bucle ; mov cx, 00ffh ;

bucle2: push cx ; mov cx, 0fffh ; bucle3: add ax, IFFLAG: add ax, dec cx ; jnz bucle3 ; pop cx ; dec cx ; jnz bucle2 ; pop cx ; dec cx ; jnz bucle1 ; ...

; *segundo bucle *tercero bucle dx ; suma (reg+reg) dx ; suma reg+reg

Listo. La rutina esta preparada para ser ejecutada. Si lo haceis y comparais el tiempo con la otra notareis la optimizacion. Hemos reducido la ejecucion minima aterior que son 3(CMP)+ 5(3 no salto + 2 ADD) = 8 ciclos * 0fffh*0fffh*0fffh ejecuciones que comparado con la ejecucion maxima del bucle optimizado que son 3 (NOP) + 3 (NOP) = 6, hemos reducido en parte el tiempo de ejecucion en esa parte del bucle, aunque para ello hemos aumentado el tamao del codigo debido a la inclusion de la instruccion "mov Word ptr [IFFLAG], 09090h". En mi caso, no se aprecia optimizacion si bx es 0, pero si bx es distinto de 0, la diferencia es de 4 a 5 segundos. Otra forma de llevar a cabo la misma operacion es el desplazamiento de codigo. Si tenemos en cuenta que la optimizacion que necesitamos es en tiempo y no en espacio, podemos crear una rutina aun mas compleja que en lugar de realizar la modificacion de una instruccion construya dicha funcion en base a lo solicitado. La funcion resultante sera una "funcion especifica" para ese proceso. Siguiendo con el mismo ejemplo, la diferencia entre ambas funciones es el segundo ADD del bucle interno. Todo el codigo desde el principio de la rutina hasta dicha instruccion se repite en ambos casos independientemente del valor de BX. Distinguimos tres partes claras en esta rutina. - La parte inicial (desde "RUTINA:", hasta "Bucle3:" incluida) - una segunda parte opcional en funcion de BX que es "IFFLAG:" - y una tercera parte que es fija tambien en ambos casos, desde "salta:" hasta el final de la rutina. Nuestra rutina constructora sera la encargada de copiar-pegar codigo para desplazarlo en memoria, evitando las instrucciones innecesarias. la 1 parte del codigo como es fija no se movera. Dependiendo del estado de BX, copiaremos el codigo que se encuentra desde "salta", sobreescribiendo a partir de el ADD de IFFLAG con dicho codigo. Es como si desplazaramos todo el codigo desde "salta" dos bytes hacia arriba, anulando la instruccion de IFFLAG, solo que hay un pequeo inconveniente en todo esto. Tendremos que actualizar parte del codigo ya que al modificarlo los saltos relativos del codigo dejaran de apuntar a la direccion correcta. Se podria solucionar utilizando un puntero base para relocalizar los saltos, pero eso aun ralentizaria mas la ejecucion. Conocemos la posicion en memoria por defecto de los saltos, asi que antes de mover un solo byte de sitio, modificaremos la direccion a la que se dirigira cada uno de ellos y despues moveremos el resto codigo. Como los saltos son hacia atras, es necesario decrementar en dos bytes cada uno de los saltos. Pero como el codigo se va a desplazar dos

bytes hacia atras, y los saltos son negativos (debido a su direccion) habra que sumar esos dos bytes en lugar de restarlos. Codigo en ASM del Archivo: ej1-3.asm ..... RUTINA: cmp bx,0 ; En caso necesario se jnz cont1 ;incremeta en dos la inc byte ptr[salto1+1] ;longitud de cada uno inc byte ptr[salto1+1] ;de los saltos. ; inc byte ptr[salto2+1] ; inc byte ptr[salto2+1] ; ; inc byte ptr[salto3+1] ; inc byte ptr[salto3+1] ; ; mov di, offset Init ; Bucle que mueve la mov si, offset Desde ; rutina. mov cx, longitud ; Longitud total repnz movsb ; sobreescribe ; cont1: xor ax, ax ; - variable var mov cx, 00ffh ; - contador bucle1: push cx ; *primer bucle ; mov cx, 00ffh ; bucle2: push cx ; *segundo bucle ; mov cx, 0fffh ; *tercero bucle bucle3: add ax, dx ; suma (reg+reg) Init: add ax, dx ;-------------Desde: dec cx ; Todo este bloque salto1: jnz bucle3 ;sera desplazado una pop cx ;instruccion hacia dec cx ;arriba. salto2: jnz bucle2 ; pop cx ; dec cx ; salto3: jnz bucle1 ;--------------..... En mi caso se aprecia una mejora en el tiempo de ejecucion con respecto al 1 de los ejemplos de 8 segundos dependiendo del valor de BX. Con esto no solo nos ahorramos los tiempos de ejecucion de CMP y JNZ, si no que ademas evitamos los tiempos perdidos (2 ciclos por cada una) de las instrucciones NOP. En los ejemplos ej1-2.asm y ej1-3.asm la rutina queda modificada tras la ejecucion. Tal vez, dependiendo del caso, sea necesario recomponer el codigo original para la proxima ejecucion. Otro ejemplo basico pero importante es una rutina muy similar a la anterior, que realizara la suma, en lugar de un numero de veces fija, un n variable de veces que sera definido como dato a la entrada del programa: Codigo en ASM del Archivo: ej1-4.asm Un bucle de este tipo con el valor de entrada en AX seria..

mov [contar], ax bucle1.. bucle2.. entra: mov cx, [contar] bucle3: add ax,0 dec cx jnz bucle3 ..... contar dw 00 En determinado momento usamos la variable contar dentro de la parte interna de anillo de bucles, y es cuando la cargamos en el registro. Una instruccion que accede a memoria es un freno enorme a la ejecucion de codigo directamente procesable por el micro. Con esto trato de decir que mov ax, word ptr [contar] obliga a direccionar punteros al microprocesador para obtener ese dato de la memoria. Sin embargo mox ax, 0000h es un dato que entra a los registros directamente desde el cargador de instrucciones del micro, acelerando la ejecucion. Con esto ahorramos dos bytes que ocupa la variable de tipo word "contar" y ademas.. en mi caso, aceleramos hasta cinco veces la ejecucion del programa, pasando de 35 segundos el ejemplo ej1-4 a 7 segundos que tarda en ejecutar el mismo codigo ej1-5, y aun es dos bytes menor en longitud!. Si guardamos los datos de tal manera que el programa los carga de forma "inminente" en lugar de accediendo a memoria, se consiguen muy buenos tiempos de ejecucion. ademas de ahorrarnos los bytes que ocupan las variables. La misma tecnica nuestro programa como eliminarla) setear dicho CMP puede ser utilizada en las comparaciones. si debe utilizar la instruccion CMP (ya hemos visto accediendo a un dato en memoria, lo ideal seria con un dato inminente en lugar de referenciado.

Codigo en ASM hipotetico add ax, 34h sub ax, cx mov bx, word ptr [contar] cmp ax, bx jz salto.. Aqui la comparacion se realiza a traves de registros, y es la forma mas rapida que hay. Pero se pierde demasiado tiempo cargando en bx el valor de la variable contar. Asi, que en lugar de usar una variable en toda regla usaremos esa misma instruccion como variable. Codigo en ASM del Archivo: ej1-5.asm Antes de entrar al bucle: mov word ptr [dato+1] , word ptr[contar] entramos.. ...... add ax, 34h

sub ax, cx dato: mov bx, 0000h cmp ax, bx ; cmp reg reg es mas rapido que cmp reg, mem jz salto.. Y si ahora una vez dentro del bucle es necesario acceder a dicha variable no existe ninguna diferencia en cuanto a que este almacenada en [dato+1] o en [contar]. Un poco de teoria..... Los microprocesadores 386, 486 .. etc, son micros estructurados de forma interna para una ejecucion en cadena, tambien llamada tubular, pipe-lined o segmentada, que consiste en que el proceso de ejecucion de una sola instruccion se realiza en varias fases. Busqueda de instruccion Decodificacion de instruccion Busqueda de operandos de la instruccion Ejecucion.

Las partes del micro encargadas de estas fases son: - CPU: unidad de prebusqueda, de decodificacion y de ejecucion. - MMU (unidad de gestion de memoria): unidad de segmentacion y unidad de paginac ion. - BIU (unidad de interconexion con el BUS) BIU, es la encargada de controlar el intercambio de informacion del micro con el exterior a traves de los buses cuando la unidad de prebusqueda busca la siguiente instruccion a almacenar en la cola de prebusqueda; la unidad de ejecucion pide operandos de la memoria o de las E/S o cuando entrega resultados; y cuando la MMU suministra direcciones de los datos a los que tiene que acceder. Esta unidad, la BIU esta optimizada puesto que, cuando no esta atendiendo a la ejecucion de las instrucciones, se dedica a buscar el codigo de la siguiente instruccion y lo almacena en una cola de 16 bytes de forma temporal. El tamao medio de una instruccion es de 3 bytes, asi que en esa cola aproximadamente se almacena el codigo de unas 5 instrucciones. Pero, si se realiza un salto, este buffer debe ser limpiado y vuelto a cargar ya que el micro preprocesa las instrucciones a partir de ese buffer y tras el salto la siguiente instruccion no se encuentra en el. Si dicho buffer se carga de forma lineal en la ejecucion del programa permitira la ejecucion en paralelo de varias instrucciones, optimizando el proceso. De toda esta parrafada de teoria podemos deducir que si no accedemos a un segmento de memoria a traves de MMU para coger o dejar datos, evitamos el tiempo de retraso que supone dicho acceso, y que si evitamos el uso de saltos, el buffer de cola para el preprocesado de instrucciones por la CPU hace las veces de cache sin necesidad de borrado no ralentizandose la ejecucion. Es evidente que hablamos a nivel de nanosegundos o periodos de tiempo casi despreciables, pero en bucles la la acumulacion de estos tiempos puede ser critica. A todo esto hay que unir el tiempo empleado en la comprobacion por parte de la unidad de segmentacion de que no se violen privilegios en la ejecucion a acceder a la memoria a traves de la MMU.

Por lo tanto, en la medida de lo posible en toda optimizacion basada en el tiempo de ejecucion evitaremos los saltos y accesos a memoria a traves de MMU. La dificultad aqui consiste en encontrar el punto mas interno de los bucles e incluso encontrar un medio de implementar dicha optimizacion. La forma mas rapida de ejecutar un bucle que sumara X veces el valor A en el registro RR, se conseguiria sin realizar ningun salto ni comprobacion en todo el bucle, es decir, repitiendo la instruccion suma tantas veces como fuera necesario en el bucle. add ax, 34h add ax, 34h add ax, 34h add ax, 34h .......... add ax, 34h RET Si el bucle, como suele ser lo normal consta de varias instrucciones, se podria acelerar repitiendo varias veces el codigo en cuestion. Por tanto.. mov cx, 40h <----------- 40 Bucle: mov ax, word ptr [fuente+cx] add ax, 34h mov word ptr [destino+cx], ax dec cx jnz bucle podria acelerarse de esta manera: mov cx, 10h <----------- 10 Bucle: mov ax, word ptr [fuente+cx] add ax, 34h mov word ptr [destino+cx], ax dec cx mov ax, word ptr [fuente+cx] add ax, 34h mov word ptr [destino+cx], ax dec cx mov ax, word ptr [fuente+cx] add ax, 34h mov word ptr [destino+cx], ax dec cx mov ax, word ptr [fuente+cx] add ax, 34h mov word ptr [destino+cx], ax dec cx jnz bucle Seguro que alguno os planteais el porque no usar una funcion para cada uno de los casos. Yo no digo que no se pueda usar, es mas, en determinados casos es lo mas conveniente. Tal vez este no sea el ejemplo mas adecuado para conocer realmente el grado de optimizacion que aporta la automodificacion. Pero

a veces el hecho de hacer un call para llamar a determinada funcion que puede encontrarse en otro segmento de memoria aun ralentiza mas teniendo en cuenta que se realizan operaciones con la pila, o el hecho de usar saltos de menos de 128 bytes de longitud obliga a tener el codigo desordenado e incluso a trocear funciones o repetir saltos constantemente para alcanzar la direccion deseada. En estos ejemplos se modifica codigo que esta dentro del flujo del programa. El programa lo ejecutara DE TODAS MANERAS sin realizar ninguna comprobacion, y es ahi donde se gana velocidad. si el bucle principal, RUTINA, ocupara 4 MB de ram y contamos todas las posibles variaciones, nos juntariamos con un prg que necesitaria la super machine que todos soais para ser ejecutado. Imagina un motor grafico de un juego en 3D, que se encarga de dibujar cada uno de los punto, lineas, poligonos y texturas de la pantalla unos 17-24 frames por segundo. Seguro que dicho motor soporta diferentes resoluciones graficas. La implementacion de una funcion que dibuje las lineas es necesaria, asi como la comprobacion de que no se han rebasado los bordes de la pantalla. Si se trabaja con varias resoluciones, ES absurdo tener una funcion que dibuje puntos por cada resolucion, lineas por cada resolucion, poligonos por cada .., texturice por cada ..., ordene, recorte, sombree, etc.. todo por cada una de las resoluciones. seguramente se haga una comprobacion con una variable que determine la resolucion y se guarden en otras variables los valores de ancho maximo, alto maximo numero de colores.. etc.. y despues, se acceda a ellos mediante un mov REG, word ptr[Ancho_Max]. O incluso los habra que comprueben el valor de la resolucion en cada pasada por el bucle. A esto me refiero: Codigo en ASM hipotetico AX, resolucion: 1= 320+200 2=640*400 3=960*600 4=1280*800 push ax ; mov bx, 320d mul bx ; mov word ptr pop ax ; mov bx, 200d mul bx ; mov word ptr ...... ; [Ancho_Max], AX ; ; [Alto_Max], AX

Despues, durante la ejecucion, en el dibujado de una linea horizontal por ejemplo se comprueba que no se excede del margen maximo de ancho de pantalla, comparando el valor del contador de coordenada X con ese valor directamente o bien, siendo algo previsores, cargando este en un registro antes de entrar en el bucle y comparando REG con REG directamente. Aun asi, se sigue perdiendo tiempo de ejecucion en cargar dicha variable, cuando existiendo la posibilidad de modificar el bucle al inicio del programa seteando en TODOS los lugares en donde se utiliza dicha variable Ancho_Max su valor, trabajando despues el programa con dicho valor de forma inminente: .... conti: inc di ;Coordenada actual mov byte ptr [di], Color ;setea pixel sobre RAM video cmp di, word ptr [Ancho_Max];Compara con max ja borde_salir ;salir si mayor cmp di, word ptr [X_final] ;Compara con valor final.

jbe conti ;continua Borde_salir:pop ... ;ir saliendo Este bucle podria pertenecer a un codigo fuente de un engine 3D. Su lectura es facil pero su optimizacion es excasa, asi que este analizamos un poco.. Ambas comparaciones podrian realizarse sobre datos inminentes conociendo sus valores. Ancho_Max es un valor que se conoce desde el comienzo de la ejecucion del programa, por lo que si es realmente optimizable: Asi quedaria el codigo tras una primera optimizacion push ax ; mov bx, 320d ; mul ax, bx ; mov word ptr [Ancho_Max+1], AX ; pop ax ; mov bx, 200d ; mul ax, bx ; mov word ptr [Alto_Max+1], AX .... conti: inc di ;Coordenada actual mov byte ptr [di], Color ;setea pixel sobre RAM video Ancho_Max: cmp di, 0000 ;Compara con max ja borde_salir ;salir si mayor cmp di, word ptr [X_final] ;Compara con valor final. jbe conti ;continua Borde_salir:pop ... ;ir saliendo Espero que con esto hayais entendido ya la aplicacion real de la automodificacion de codigo en cuanto a tiempo de ejecucion. Aunque los ejemplos han sido algo "cutres", son mas que suficiente como introduccion a lo que aun falta por ver. Optimizacion: Espacio Aunque mediante esta tecnica tambien se puede reducir el tiempo de ejecucion, su aplicacion en la optimizacio de espacio solo es posible cuando determinadas funciones, realicen funciones diferentes siendo muy similares entre ellas. Supongamos que nuestro programa llega a un punto de ejecucion en el que disponde de dos opeandos, AX y BX, y que debe restarlos, crearemos una funcion suma que realice la operacion devolviendo en AX el resultado. Si mas adelante, la operacion se convierte en resta, tendremos otra funcion que realizara la operacion devolviendo en AX tambien el resultado. Si la operacion a realizar la tenemos en CX, y este es mas o menos el flujo de ejecucion: Codigo en ASM hipotetico AX: operando 1 BX: operando 2 CX: operacion cmp cx, 00 ; Realiza una serie jz salir ;de comparaciones

cmp cx, 01 ;para determinar la jz suma ;operacion a realizar cmp cx, 00 ; jz resta ; cmp cx, 00 ; jz multiplicacion ; cmp cx, 00 ; jz division ; jmp volver ; ninguna de las anteriores. Suma: add ax, bx ;Operaciones. jmp volver ; resta:sub ax, bx ; jmp volver ; ... ; mul bx ; ... ; ... ; Volver: RET ; salir: .... ; Este suele ser un codigo en ASM tipico, muy ordenado y facil de leer, pero un rapido analisis ya permite una optimizacion directa sin necesidad de entrar en muchos detalles.. - que se compruebe Salir como primera operacion es un error, ya que normalmente esta instruccion solo sera ejecutada una vez en todo el programa. La mas importante pudiera ser Volver, o cualquiera de las demas operaciones en cualquier orden, aunque si conocemos cual de ellas sera ejecutada mas veces, convendria ponerla al comienzo - las operaciones ADD y SUB, si se realizan entre registros, solo consumen dos ciclos de reloj. La operacion MUL, consume entre 9 y 41 dependiendo de los operandos, y la operacion DIV entre 14 y 41. Si las operaciones a realizar van a ser de todo tipo, tal vez interesaria comprobar la DIV primero, despues MUL, y despues indistintamente ADD y SUB. Con esto se consigue mediar el tiempo de cada operacion, ya que DIV, que es la que mas ciclos consume es la que menos tiempo tarda en comenzar a ejecutarse al no realizarse el resto de comprobaciones antes. Despues MUL, que entra ya con el tiempo perdido por comprobarse si la operacion es una DIV, pero cuyo consumo de ciclos es algo menor que el de esta, y despues ADD y SUB, que solo consumen un par de ciclos, pero a las que hay que aadir el tiempo de comprobacion empleado en DIV y MUL. Si por el contrario, sabemos que por ejemplo la suma sera la operacion mas realizada en todo el bucle, La colocaremos primero, para evitar emplear tiempo en comprobar si se trata de DIV, MUL o SUB. - Otro de los puntos de optimizacion es la cola de preprocesado de instrucciones. Los saltos se estan realizando si la operacion coincide, con lo que se debe cargar el buffer si esto sucede. Optimicemos este programa suponiendo que la suma sera la operacion mas realizada. Codigo en ASM hipotetico cmp cx, 01 ; Si la operacion es suma,

jnz nosuma ;evitamos la carga del buffer add ax, bx ;de preprocesado. RET ;JMP VOLVER ;RET mejor que JMP+RET Nosuma: cmp cx, 02 ; jnz Noresta ; add ax, bx ; RET ;JMP VOLVER ;RET mejor que JMP+RET Noresta: cmp cx, 03 ; jnz NoMUL ; MUL bx ; RET ;JMP VOLVER ;RET mejor que JMP+RET NoMUL: cmp cx, 04 ; jnz NoDiv ; div bx ; RET ;JMP VOLVER ;RET mejor que JMP+RET NoDiv: jz SALIR ; Volver: RET ;Volver se ejecuta ANTES QUE EL SALIR Salir: ...... ; y sin cargar el buffer Este codigo evita en la medida de lo posible la carga del buffer de cola de prepocesado suponiendo que en la comparacion, si ES la operacion a realizar, incluso en el caso de que la operacion sea invalida y la ejecucion deba saltar a volver. La operacion mas lenta a realizar en este codigo es la de salir.. pero no es importante en la ejecucion ya que solo se realiza una vez. Optimicemos esto aun mas en cuanto a espacio se refiere, analizando las posibles opciones que podemos modificar de nuestro programa. add sub mul div ax, bx 03h 0c3h ax, bx 2bh 0c3h bx 0F7h 0e3h bx 0F7h 0F3h

Este cuadro representa los opcodes de codigo maquina de las 4 instrucciones que se pueden ejecutar. ADD y SUB mantienen un byte en comun (0c3h), y MUL y DIV, otro (0f7h). Esto nos permitira construir una misma "funcion" para cada par de opciones add/sub y mul/div. Por otro lado, nada impide que en lugar de usar los valores 1, 2, 3, 4 para determinar la operacion a realizar, usemos los propios opcodes de cada una de las instrucciones, teniendo en cuenta que son diferentes. Asi pues, la suma se determina con cx=03h, la resta con cx=2bh, la multiplicacion con cx=0e3h, y la division con cx=0f3h, dejando cx=0 para salir. Con esos opcodes construimos las 2 posibles "funciones" que pueden ser ejecutadas (add/sub, mul/div), y despues saltamos a una u otra dependiendo de si cx, es mayor que 2bh, lo que indicaria que no es ni suma ni resta, si no multiplicacion o division. Codigo en ASM hipotetico mov byte ptr [operacion1], cl mov byte ptr [operacion2+1], cl cmp cl, 2bh ja operacion2 cmp cl, 0h

jz salir operacion1: db 00h, 0c3h ;SUMA O RESTA ret operacion2: db 0F7h, 00h ;MUL O DIV ret salir: .... El valor de la operacion, y por defecto el opcode de la propia operacion se setea en operacion1 y operacion2+1, consyendo de esta manera la suma/resta, multiplicacion/division que sera ejecutada. Se realizan las comparaciones para saber si hay que salir, y cual es la operacion de las dos posibles que se ejecutara y se salta a ella. Esto es el resultado de la comparacion entre ambos codigos: Codigo en ASM hipotetico 83 F9 01 cmp cx, 01 ; Si la operacion es suma, 75 03 jnz nosuma ;evitamos la carga del buffer 03 C3 add ax, bx ;de preprocesado. C3 RET ;JMP VOLVER ;RET mejor que JMP+RET 83 F9 02 Nosuma: cmp cx, 02 ; 75 03 jnz Noresta ; 03 C3 add ax, bx ; C3 RET ;JMP VOLVER ;RET mejor que JMP+RET 83 F9 03 Noresta: cmp cx, 03 ; 75 03 jnz NoMUL ; F7 E3 MUL bx ; C3 RET ;JMP VOLVER ;RET mejor que JMP+RET 83 F9 04 NoMUL: cmp cx, 04 ; 75 03 jnz NoDiv ; F7 F3 div bx ; C3 RET ;JMP VOLVER ;RET mejor que JMP+RET 74 01 NoDiv: jz SALIR1 ; C3 Volver1: RET ;Volver se ejecuta ANTES QUE EL SALIR salir: TOTAL: 35 bytes Codigo en ASM hipotetico 88 0E 01 3D mov byte 88 0E 01 3F mov byte 80 F9 2B cmp cl, 2bh 77 08 ja operacion2 80 F9 00 cmp cl, 0h 74 2C jz salir 00 C3 operacion1: db C3 ret F7 00 operacion2: db C3 ret salir: TOTAL: 24 bytes Bien, si tenemos en cuenta que la optimizacion en este ejemplo se ha hecho sobre funciones de solamente dos instrucciones, creo que es mas que suficientemente grafico por si solo. Imaginad la ptr [operacion1], cl ptr [operacion2-1], cl

00h, 0c3h 0F7h, 00h

optimizacion de tamao sobre "funciones" de mas de dos operaciones o que incluyan bucles o llamadas. De este apartado no hay ejemplos que compilar porque su comprobacion puede hacerse a simple vista.

Y en cuanto a optimizacion, esto es lo basico para que podais poneos en marcha con el tema. Como veis, el codigo resultante se complica bastante para una rapida lectura, pero si objetivo esta cumplido. No hemos visto casi ejemplos de automodificacion que influyan en el flujo de ejecucion de un programa, Por ahora, ya que aun queda mucho tema por ver, el resto de los ejemplos iran mas orientados hacia la CREACION de funciones que hacia la optimizacion, que era el objetivo de este apartado.

Esta es la teoria: Codigo en ASM hipotetico funcion: mov byte ptr [aqui] , 0c3h ; Cambia NOP por RET Aqui: nop ; ... o bien.. funcion: inc byte ptr [aqui] ; Cambia JNZ por JZ Aqui: jnz salir ; Durante una ejecucion normal, la CPU carga la instruccion "mov byte ptr [aqui] , 0c3h" y se dispone a buscar los parametros que necesita para su ejecucion. Mientras, la siguiente instruccion es colocada en la parte mas alta de la cola de 16 octetos del preprocesador. En el siguiente paso, se ejecuta la instruccion mientras que la siguiente, el NOP, se carga en la unidad de decodificacion. El mov modifica la memoria en la posicion [aqui], pero esta instruccion ya esta cargada en el micro. En el siguiente paso, se comprueba si la intruccion NOP, necesita o no parametros y se ejecuta, mientras que la siguiente se carga en el micro, etc.. . El flujo del programa durante una ejecucion normal, es tal y como se ve en el codigo. Pero, y aqui es donde esta tecnica merece ser estudiada, si trazamos el codigo con un depurador, y trazamos precisamente sobre la instruccion MOV, el depurador coloca el OPcode de una INT3 sobre el NOP, para que se detenga la ejecucion tras la instruccion MOV. Esta instruccion ahora modifica el valor de memoria en [aqui], y eso queda guardado. El micro ahora se encuentra ejecutando una interrupcion y hasta que no retorne la ejecucion del MOV queda congelada. La interrupcion que se esta ejecutando de forma controlada por el depurador restaura la memoria en [aqui], volviendo de la interrupcion. El valor que ahora tendremos en esta posicion de memoria no sera el NOP, sino el RET. Este "bug" ha sido corregido en los nuevos modelos de microprocesador, y no funcionara en todos. En algunos de los microprocesadores nuevos,

se ha conseguido recuperar de forma aceptable (sin tiempos de procesado excesivamente largos) el contenido del buffer de preprocesado, aunque no se ha corregido del todo. la tecnica "step-forward" solo funciona con las instrucciones que no necesiten cargar datos a traves de al MMU, es decir, las de ejecucion directa, como NOP, PUSH AX, INC DI, etc.. . Un salto o una suma requieren de determinados parametros como una longitud y direccion o un operando para que la instruccion sea ejecutada. Si el microporcesador tiene que acceder a la MMU para capturar dichos datos, la instruccion se volvera a cargar y la nueva instruccion sera ejecutada. He preparado el siguiente ejemplo para que podais comprobarlo utilizando el turbodebugger. Este programa no funcionara en todos los modelos de microprocesador. Cargad el programa y dadle a F9. El programa retornara de la ejecucion con un 1. sin embargo si trazamos sobre la instruccion "mov byte ptr [aqui] , 0c3h" el programa lo hara devolviendo 0 como valor. Podreis verlo mejor si trazais instruccion a instruccion. Codigo en ASM del Archivo: ej2-1.asm xor ax, ax ; call funcion ; Salir: mov ah, 4ch ; System: exit, al:valor int 21h ; ya. funcion: mov byte ptr [aqui] , 0c3h ; Cambia NOP por RET Aqui: nop ; inc ax ; Modifica valor a ret ; Como comentaba antes, no se ha podido corregir del todo, ya que en los microprocesadores donde el programa siempre retorne con 0, es decir, se cargue de nuevo la instruccion una vez modificada, se llega a sobreescribir el valor de la memoria antes de que el micro lea la ejecute la siguiente instruccion, que depurando es un INT3. Lo que hara que, aunque pongamos un breakpoint sobre la linea "Aqui: nop" dicho breakpoint (INT3) sera sustituido por la nueva instruccion, el RET, y la ejecucion del programa no se detendra. Si bombardeamos parte del codigo, la parte que mas queramos entorpecer de instrucciones de este tipo, la desesperacion por parte de la persona que ande tras la depuracion esta asegurada. Lo se por experiencia. El problema es que deja las instrucciones a la vista. una mezcla de "step-forward" usando instrucciones validas (NOP, RET..) y no validas (JNZ XX, ADD AX, XXXX) dificultaran mucho la lectura del codigo al tener que estar determinando cuales son o no modificadas. Ojead la siguiente rutina. Codigo en ASM hipotetico xor ax, ax ; call funcion ; Salir: mov ah, 4ch ; System: exit, al:valor int 20h ; ya. funcion: inc byte ptr [funcion-1] ; Cambia 20h por 21h Aqui: ret ; inc ax ; Modifica valor a jmp aqui ; devolver.

ret ; El resultado es el mismo que la anterior, pero no aparece la INT 21 (el resultado de la ejecucion de este codigo es facil de ver). Si complicamos mas la modificacion de codigo nos vamos al siguiente punto.. Ocultacion de codigo xor ax, ax ; call funcion ; Salir: mov ah, 4ch ; System: exit, al:valor int 20h ; ya. funcion: inc byte ptr [funcion-1] ; Cambia 20h por 21h Aqui: ret ; inc ax ; Modifica valor a jmp aqui ; devolver. ret ; El resultado es el mismo que la anterior, pero no aparece la INT 21 (el resultado de la ejecucion de este codigo es facil de ver). Si complicamos mas la modificacion de codigo nos vamos al siguiente punto.. Ocultacion de codigo

Aqui: add ax, 100h push ax funcion: sub word ptr [aqui+1] , 0dfh mov bp, sp mov sp, ax sub ax, 3400h inc sp push ax mov sp, bp sub ah, byte ptr [funcion] ret

Todo este conjunto de instrucciones solo realizan la funcion (para nada optimizada) de construir un INT 21h y darle a AX el valor de 4c00h para terminar la ejecucion. Es un poco dificil de ver a simple vista que sobre el offset Aqui se construye un INT 21h usando restas y la pila. La automutacion de codigo en este aspecto esta basada en la falta de codigo formado en el ejecutable. Las rutinas se van construyendo en tiempo de ejecucion en base a los datos necesarios para acelerar el flujo, asi que no existe el codigo formado. Las ventajas que esto aporta a los metodos de ocultacion de codigo unido a tecnicas de deteccion de depuradores forman una gran defensa contra ataques a la integridad del programa.

En tiempo de ejecucion se podrian modificar funciones vitales para el funcionamiento del programa evitando asi el depurado de dicho codigo y falseando el resultado. El hecho de que determinadas rutinas se construyan (contruir, no desencriptar ni descomprimir) sobre la marcha permite una rapida comprobacion de la integridad del codigo y su recuperacion ademas de complicar muy mucho la tarea de modificacion. Quiero decir, que implementar esta tecnica en la funcion de registro de un programa, no solo implica al cracker el estudio de la funcion de registro, si no la creacion de esta, ya que es creada de forma dinamica. No basta con cambiar un salto en la rutina, si no ver como y donde se forma ese salto y modificar la funcion constructora. Como indicaciones, el mismo salto que realiza determinado bucle sobre el serial podria ser el que decide si el serial es valido o no, evitando el jz por jnz por todos conocido. Veamos alguna practica sobre este tema. Partiendo de que la tecnica principal es la automodificacion de codigo no crearemos una proteccion de serial complicada, pero la parte en la que se decide si el seria es o no valido sera costruida en funcion del serial, no existiendo hasta la total comprobacion de este. Por cada letra del nombre, se asigna un numero mediante el modulo, obteniendo dicho serial.

TERMINAR

Evasion de tecnicas automatizadas Principalmente utilizada como complemento a otras tecnicas, la automodificacion de codigo permite la no repeticion de patrones. Como aplicacion mas utilizada se encuentra la de generacion de rutinas de compresion y/o encriptacion para codigo virico. Dado que requiere del uso de otras tecnicas de muy bajo nivel, su implementacion en codigo extenso o en aplicaciones programadas en otros lenguajes es bastante complicada. Es evidente que si hablamos de automutacion de codigo estamos hablando de archivos ejecutables. Para dar como verdaderamente funcional una rutina de este tipo, esta debe ser capaz de reordenar todos los datos a su modo y restaurarlos para su lectura posteriormente, evitando, en lo posible, el que dos archivos generados de forma aleatoria por la misma rutina tengan una sola parte en comun, ya sea tamao, bloques de datos dentro del archivo, o que la localizacion de estos se repita dentro de los dos archivos. En todos los ejemplos vistos hasta ahora, siempre se repite el mismo patron, y, poniendo un ejemplo, alguien pretendiera modificar la rutina del archivo ej2-2, solo tendria que escribir sobre las dos primeras instrucciones un MOV AX, 4cH e INT 21H, para que la rutina funcionara por completo. Es mas, de hecho, le estamos cediendo hueco dentro del codigo por si necesita realizar alguna otra operacion antes de salir, ya que el resto del codigo que construia el EXIT, nunca se

volveria a ejecutar.

FIN

También podría gustarte