Escribiendo en código máquina

Cuando estudiaba los principios de la carrera (anteriormente tambien) me preguntaba como eran los programas por dentro y como se hacian. Despues de aprender a programar sigues y sigues avanzando pero ciertos pequeños detalles son muy escurridisos para ser tomados directamente en clase.

Escribir código "maquina" siempre fue medio en broma una variante del dicho "los verdaderos hombres programan con la orden cat > programa.exe", ahora que estamos de humor veamoslo un poco más en serio y por que tal ves no llevemos algunas sorpresas algunos de nosotros que no sabemos como son los opcodes por dentro.

Queda entonces advertido que lo siguiente es simplemente una pequeña investigación para adentrarnos al pequeño mundo de los bits que forman a los inmensos programas de computo de hoy en dia.

Tambien para los super expertos en ingenieria inversa, data recovery o hackers sabedores de las oscuras artes subterraneas les prometo aburrirlos ya que de ninguna manera soy experto en estos temas, si quieren leer algo escrito por expertos lean un libro y no esta página dedicada a mis ratos libres llenos de poesia oscura.

Como dicen nuestros vecinos norteamericanos : enjoy!

Cuando programamos en ensamblador se dice que programamos a bajo nivel debido a que cada existe una relacion 1-1 a cada intruccion binaria en el conjunto de instrucciones del procesador.

Lo anterior no es completamente la verdad debido a que existe un "microprograma" integrado en algunos microprocsadores los cuales interpretan cada opcode apuntado por CS:EIP ( claro despues de hacer la referencia real en el caso de estar en modo protegido y paginado por el MMU), cada opcode ( el codigo de operacion binario) es traducido a un conjuto de microinstrucciones que el hardware del microprocesador si podra realizar como abrir y cerrar compuertas de operadores logicos.

Para conocer mas sobre arquitectura de computadores les recomiendo el libro de "Organizacion de computadoras, un enfoque estructurado" del Maestro Andrew. S. Tanenbaum, o el libro de "Arquitectura de Computadoras" de M. Morris Mano.


Usted debería tener una copia del volumen 2 del manual de la arquitectura intel IA32 para conocer la estructura del la instrucción MOV que analisaremos aquí.

Supongamos que tenemos un pequeño "programa" como el siguiente:

	  
# cat bin.s

[BITS 32 ]
db 10001011b ; mov de 32bits fuente= REG destino= MODRM
;        ||_BIT W
;        |__BIT D

db 11001000b
;  | |  |_RM = Registro [EAX] si MOD=00
;  | |_REG = ECX si W=1 CL si W=0
;  |_MOD =  sin desplazamiento =00  reg a reg =11



mov eax,ecx ; importante esta es la  unica instruccion que realmente traduce NASM.

---------------------
Sin comentarios el contenido seria:

[BITS 32 ]
db 10001011b 
db 11001000b
mov eax,ecx ; importante esta es la  unica instruccion que realmente traduce NASM.


Ensamblamos el programa con la orden nasm bin.s -o bin.bin y luego lo desensamblamos obteniendo lo siguiente:


bin# ndisasm -b 32 bin.bin
00000000  8BC8              mov ecx,eax
00000002  89C8              mov eax,ecx


Disculpa por lo brusco de esta presentación del código ya que no he explicado mucho. Lo realmente importante es que la orden: ndisasm -b 32 bin.bin mostro 2 instrucciones mov en lugar de la unica mov eax,ecx.

El comando 'db' es utilizado en asm para definir variables o mejor dicho para definir una etiqueta y guardar un valor en la posición del archivo del programa donde se encontro. Lo que nadie dice es que 'db' puede tener o no un nombre o etiqueta de forma que bien el programa pudo ser:


[BITS 32 ]
variable1 db 10001011b 
variable2 db 11001000b
mov eax,ecx 


Si lo hacemos el archivo de salida no cambia en lo absoluto debido a que los nombres solo son usados en el proceso de ensamble o compilacion en el caso de compiladores pero no es guardado el archivo objeto final.

Finalmente los valores guardados en variable1 y variable2 ( si lo hubieramos declarado claro) son guardados en el archivo objeto exactamente al principio del archivo final tal como habian sido explicitamente escrito. Esos datos en realidad son en realidad el binario de una intruccion mov ecx,eax como lo podemos ver en lo siguiente:


En hexadecimal 

# cat bin.bin | od -x
0000000 c88b c889

-------------------------------------------------
En binario 

# cat bin.bin| xxd -b
0000000: 10001011 11001000 10001001 11001000                    ....


---------------------------------------------------

Sobreponiendo los valores podemos hacer una relacion para 
ver los valores juntos  en hexadecimal y en binario :

0000000: 1000 1011 1100 1000 1000 1001 1100 1000                   
          8     c   b    8    8     9   c     8   

Recuerda que los valores aqui estan en la forma en que se guardan en el archivo
por lo cual los valores estan ordenados de manera inversa, sin embargo esto es normal
debido a que asi es realmente como estan organizados en disco, cuando pasan  a memoria 
estos son automaticamente copiados de manera "normal".




Ya con la informacion anterior, sabemos los valores hexadecimales de las 2 instrucciones mov que habiamos codificado en el programa en ensamblador anterior.

Veamos que nosotros podriamos introducir esos valores en un vector de "chars" en un programa en C, utilizando el siguiente codigo y redirigiendo su salida a un archivo:


$ cat mov.c
#include 

int main()
{
        unsigned char codigoExe[]={'\x8b','\xc8','\x89' ,'\xc8' ,'\0'};

        printf("%s",codigoExe);
        return 0;
}

kb@cobalt:$ gcc mov.c -o mov.eje
kb@cobalt:$ ./mov.eje > mov.bin

kb@cobalt:$ ndisasm -b 32 mov.bin
00000000  8BC8              mov ecx,eax
00000002  89C8              mov eax,ecx


#> Buala!, tenemos exactamente el mismo archivo que habiamos sacado 
anteriormente, pero ahora lo tenemos en un vector en un codigo en C, el cual 
podria ser manipulado, cifrado, mandado por un socket, puesto en otro 
programa, etc. 

--->NOTA: Para los que pusieron atencion al codigo; veran un '\0' que no es
parte del codigo, este solo lo use para detener a printf y asi no tener que 
hacer un for y utilizar putc() o printf(%c).



------------------------------------------------------------------

Veamos entonces que es perfectamente posible guardar un archivo binario ejecutable
en un vector dento del codigo fuente de otro programa; utilizando  codigos
hexadecimales de la siguiente manera:

# cat bin.bin | od -x
0000000 c88b c889
         | \->Byte menos significativo
	 |
	 |->Byte mas significativo

unsigned char codigoExe[]={'\x8b','\xc8','\x89' ,'\xc8' ,'\0'};

---------> Se escriben primero los bytes menos significativos, aunque tendriamos que
analizar si esto tambien se aplica a las palabras ( cada 16 bits en ves de cada 8).


La parte realmente importante fue el haber guardado los codigos hexadecimales en un vector de caracteres utilizando caracteres '\x00' donde 00 puede ser cualquier numero hexadecimal, esto nos asegura que cada valor puesto sera exactamente de 1 bytes de longitud y que los bits seran ordenados en la forma especifica que escribimos.

Recuerden tambien que algunas veces (muchas en realidad) el compilador introduce codigo de "relleno" ya sea introduciendo codigos de no operacion (en ensamblador: NOP => 0x90) los cuales sirven para que otras variables se encuentren en direcciones de memoria pares o multiplos de 4 ( dependiendo del bus de datos en intel es 4 por ser el numero de bytes del bus: 32 bits)

Veamos entonces que esos datos que tambien son codigo ejecutable pueden ser mandado como informacion a un programa que este escuchando a un puerto, en este caso utilizamos netcat ( nc) para enviar y recibir los datos, pero en realidad podria ser cualquiera, viendo entonces una de las bases de en las cuales se apoya la explotacion de programas en internet.


# nc -p 1000 -l -o fileTraffic  ====>Escuchar en el puerto 1000 del host local


# ./mov.eje |nc localhost 1000 ====> mandar la salida de mov.eje al puerto 1000 del host local
--------------------
# cat fileTraffic
< 00000000 8b c8 89 c8                                     # ....




De lo anterior se pueden relacionar muchos temas que no son tan triviales como lo presentado aqui, un tema muy interesante es la escritura de compiladores, interpretes o ensambladores que generen codigo ejecutable, ya vimos un ejemplo del codigo binario necesario para formar una instruccion en codigo maquina; un compilador es un programa que automatiza la creacion de dicho codigos ( y muchas otras cosas).

Solo quisiera recalcar la absoluta posibilidad de haberme equivocado en lo anteriormente expuesto, si tienes algo que agregar, comentario o equivocacion espero me puedas hacer llegar lo que piensas, ya sea a mi correo en jorge.garcia.gonzalez (en) gmail.com o posteando en mi blog.

Saludos!

Bytes y bits son manajados e interpretados para darles un "significado" que depende del contexto, esto es probablemente una de las bases más importantes para desarrollar programas.



Intel IA32 Arch. Manuals

correo electronico : jorge.garcia.gonzalez@gmail.com