Obteniendo el binario "raw" de un programa en C

Anteriormente tenia aqui una pagina que no termine por falta de tiempo sobre compiladores, decidi reemplazarla por una más completa sobre un tema relacionado; veremos como implementa un compilador (GCC) algunas estrucutas fundamentales y como lo podemos ver en su forma "cruda" o "raw", empezaremos con la estructura básica de una función:

	  
	  
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ cat -n main.c
     1  int  main()
     2  {
     3  }
	  

Cuando programamos nunca pensamos en como el compilador crea el archivo objeto, damos por hecho las cosas y nunca nos sentamos a pensar en como las implementariamos si quisieramos programar un lenguaje o algo similar:

Veamos entonces como se implementan las funciones en lenguaje C ( compilador GCC, aunque la mayoria es igual ) Veamos el binario raw del archivo main.o:

	  
 kb@cobalt:/mnt/hda7/labs/www/escritos/code$ cat main.o | od -x
offset | ( cada columna tiene 2 bytes )
----------------------------------------------   
0000000 457f 464c 0101 0001 0000 0000 0000 0000  |cada numero se guarda(ceros tambien)
0000020 0001 0003 0001 0000 0000 0000 0000 0000  |cada numero ocupa 1 nibble
0000040 00b4 0000 0000 0000 0034 0000 0000 0028  |1 nubble son 4 bits
0000060 0009 0006 8955 83e5 08ec e483 b8f0 0000  |2 nibbles =1 bytes = 8 bits
0000100 0000 c429 c3c9 0000 4700 4343 203a 4728  |2 bytes   =16 bits
0000120 554e 2029 2e33 2e33 2035 4428 6265 6169  
0000140 206e 3a31 2e33 2e33 2d35 3331 0029 2e00
0000160 7973 746d 6261 2e00 7473 7472 6261 2e00
0000200 6873 7473 7472 6261 2e00 6574 7478 2e00
0000220 6164 6174 2e00 7362 0073 6e2e 746f 2e65
0000240 4e47 2d55 7473 6361 006b 632e 6d6f 656d
0000260 746e 0000 0000 0000 0000 0000 0000 0000
0000300 0000 0000 0000 0000 0000 0000 0000 0000
0000320 0000 0000 0000 0000 0000 0000 001b 0000
0000340 0001 0000 0006 0000 0000 0000 0034 0000
0000360 0012 0000 0000 0000 0000 0000 0004 0000
0000400 0000 0000 0021 0000 0001 0000 0003 0000
0000420 0000 0000 0048 0000 0000 0000 0000 0000
0000440 0000 0000 0004 0000 0000 0000 0027 0000
0000460 0008 0000 0003 0000 0000 0000 0048 0000
0000500 0000 0000 0000 0000 0000 0000 0004 0000
0000520 0000 0000 002c 0000 0001 0000 0000 0000
0000540 0000 0000 0048 0000 0000 0000 0000 0000
0000560 0000 0000 0001 0000 0000 0000 003c 0000
0000600 0001 0000 0000 0000 0000 0000 0048 0000
0000620 0026 0000 0000 0000 0000 0000 0001 0000
0000640 0000 0000 0011 0000 0003 0000 0000 0000
0000660 0000 0000 006e 0000 0045 0000 0000 0000
0000700 0000 0000 0001 0000 0000 0000 0001 0000
0000720 0002 0000 0000 0000 0000 0000 021c 0000
0000740 0080 0000 0008 0000 0007 0000 0004 0000
0000760 0010 0000 0009 0000 0003 0000 0000 0000
0001000 0000 0000 029c 0000 000d 0000 0000 0000
0001020 0000 0000 0001 0000 0000 0000 0000 0000
0001040 0000 0000 0000 0000 0000 0000 0001 0000
0001060 0000 0000 0000 0000 0004 fff1 0000 0000
0001100 0000 0000 0000 0000 0003 0001 0000 0000
0001120 0000 0000 0000 0000 0003 0002 0000 0000
0001140 0000 0000 0000 0000 0003 0003 0000 0000
0001160 0000 0000 0000 0000 0003 0004 0000 0000
0001200 0000 0000 0000 0000 0003 0005 0008 0000
0001220 0000 0000 0012 0000 0012 0001 6d00 6961
0001240 2e6e 0063 616d 6e69 0000
0001251

	  
	  

De lo anterior podemos ver que existe mucha información solo para crear una función, cosa que no creo, entonces que es tanta información si solo defini una función ? Veamos que pasa si sacamos los strings del archivo:

	  

kb@cobalt:/mnt/hda7/labs/www/escritos/code$ cat main.o | strings
GCC: (GNU) 3.3.5 (Debian 1:3.3.5-13)
.symtab
.strtab
.shstrtab
.text
.data
.bss
.note.GNU-stack
.comment
main.c
main

	  
	  

Ahh, entonces el compilador tambien escribe los nombres de sección, versión del compilador, nombre de funciones y del archivo. Veamos el mismo archivo pero ahora visto como instrucciones en ensamblador, lo veremos mediante un desensamblador del paquete nasm:

	 
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ ndisasm -b 32 main.o
Offset    opcode            instruccion en ensamblador
------------------------------------
00000000  7F45              jg 0x47
00000002  4C                dec esp
00000003  46                inc esi
00000004  0101              add [ecx],eax
00000006  0100              add [eax],eax
00000008  0000              add [eax],al
0000000A  0000              add [eax],al
0000000C  0000              add [eax],al
0000000E  0000              add [eax],al
00000010  0100              add [eax],eax
00000012  0300              add eax,[eax]
00000014  0100              add [eax],eax
00000016  0000              add [eax],al
00000018  0000              add [eax],al
0000001A  0000              add [eax],al
0000001C  0000              add [eax],al
0000001E  0000              add [eax],al
00000020  B400              mov ah,0x0
00000022  0000              add [eax],al
00000024  0000              add [eax],al
00000026  0000              add [eax],al
00000028  3400              xor al,0x0
0000002A  0000              add [eax],al
0000002C  0000              add [eax],al
0000002E  2800              sub [eax],al
00000030  0900              or [eax],eax
00000032  06                push es
00000033  005589            add [ebp-0x77],dl
00000036  E583              in eax,0x83
00000038  EC                in al,dx
00000039  0883E4F0B800      or [ebx+0xb8f0e4],al
0000003F  0000              add [eax],al
00000041  0029              add [ecx],ch
00000043  C4                db 0xC4
00000044  C9                leave
00000045  C3                ret
00000046  0000              add [eax],al
00000048  004743            add [edi+0x43],al
0000004B  43                inc ebx
0000004C  3A20              cmp ah,[eax]
0000004E  28474E            sub [edi+0x4e],al
00000051  55                push ebp
00000052  2920              sub [eax],esp
00000054  332E              xor ebp,[esi]
00000056  332E              xor ebp,[esi]
00000058  3520284465        xor eax,0x65442820
0000005D  626961            bound ebp,[ecx+0x61]
00000060  6E                outsb
00000061  2031              and [ecx],dh
00000063  3A33              cmp dh,[ebx]
00000065  2E332E            xor ebp,[cs:esi]
00000068  352D313329        xor eax,0x2933312d
......mas codigo 
0000009D  7465              jz 0x104
0000009F  2E47              cs inc edi
000000A1  4E                dec esi
000000A2  55                push ebp
000000A3  2D73746163        sub eax,0x63617473
000000A8  6B002E            imul eax,[eax],byte +0x2e
000000AB  636F6D            arpl [edi+0x6d],bp
000000AE  6D                insd
000000AF  656E              gs outsb
000000B1  7400              jz 0xb3
00000299  0001              add [ecx],al
.......lo mismo
0000029B  0000              add [eax],al
0000029D  6D                insd
0000029E  61                popa
0000029F  696E2E63006D61    imul ebp,[esi+0x2e],dword 0x616d0063
000002A6  69                db 0x69
000002A7  6E                outsb
000002A8  00                db 0x00

	 
	 
	 

Vemos que un archivo objeto tiene más información que el propio código ejecutable. Para poder ver el unicamente código binario generado por las instrucciones que escribimos ( en este caso int main(){} ) podemos hacerlo de varias formas, una es con objdump ).

	 
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ objdump -d main.o
main.o:     file format elf32-i386

Disassembly of section .text:

00000000 
: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c9 leave 11: c3 ret ----------------------------------------------------- offset | opcode |instruccion en asm en byte| | ======================================================== Vea que la seccion .comment contiene el nombre de la version de compilador GCC, ( en la columna derecha se ve en assci) ========================================================= kb@cobalt:/mnt/hda7/labs/www/escritos/code$ objdump -s -D main.o main.o: file format elf32-i386 Contents of section .text: 0000 5589e583 ec0883e4 f0b80000 000029c4 U.............). 0010 c9c3 .. Contents of section .comment: _____________________ES lo mismo / \ 0000 00474343 3a202847 4e552920 332e332e .GCC: (GNU) 3.3. <----version de compilador 0010 35202844 65626961 6e20313a 332e332e 5 (Debian 1:3.3. 0020 352d3133 2900 5-13). Disassembly of section .text: 00000000
: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c9 leave 11: c3 ret Disassembly of section .comment: 00000000 <.comment>: //note que aqui la seccion .comment se tomo como codigo pero no lo es 0: 00 47 43 add %al,0x43(%edi) //son los assci de la version de GCC 3: 43 inc %ebx 4: 3a 20 cmp (%eax),%ah 6: 28 47 4e sub %al,0x4e(%edi) 9: 55 push %ebp a: 29 20 sub %esp,(%eax) c: 33 2e xor (%esi),%ebp e: 33 2e xor (%esi),%ebp 10: 35 20 28 44 65 xor $0x65442820,%eax 15: 62 69 61 bound %ebp,0x61(%ecx) 18: 6e outsb %ds:(%esi),(%dx) 19: 20 31 and %dh,(%ecx) 1b: 3a 33 cmp (%ebx),%dh 1d: 2e 33 2e xor %cs:(%esi),%ebp 20: 35 2d 31 33 29 xor $0x2933312d,%eax ...

Objdump es una utileria para tratar con archivos ejecutables disponibles en cualquier distribución de linux, la sintaxis del ensamblador es At&t a diferencia de ndisasm que es sintaxis intel. Sin embargo puede checar que los opcodes son los mismo ya que se trata del mismo archivo.

Para generar un archivo binario "raw" o sin formato el cual contenga únicamente el código ejecutable de TU programa y nada más, lo puedes crear de la siguiente manera:


kb@cobalt:/mnt/hda7/labs/www/escritos/code$ gcc main.c
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ ld -o main -Ttext 0x0  -e main main.o
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ objcopy -R .comment -R .note  -S  -O binary  main main.raw
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ ndisasm -b 32 main.raw

00000000  55                push ebp
00000001  89E5              mov ebp,esp
00000003  83EC08            sub esp,byte +0x8
00000006  83E4F0            and esp,byte -0x10
00000009  B800000000        mov eax,0x0
0000000E  29C4              sub esp,eax
00000010  C9                leave
00000011  C3                ret


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

El contenido en hexadecimal "puro" lo podemos ver mediante
la siguiente orden:
------------------------------------
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ cat main.raw  | od -x
0000000 8955 83e5 08ec e483 b8f0 0000 0000 c429
0000020 c3c9
0000022

==================================================================
Recordando que el archivo original era:
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ cat main.c -n
     1  int  main()
     2  {
     3  }



Entonces, la creacion de una funcion consiste en guardar la base de la pila antigua en la propia pila, despues se utiliza el tope de la pila como la nueva base, esto es lo que hacen las dos primeras instrucciones:


00000000  55                push ebp          --el registro ebp apunta a la base de la pila
00000001  89E5              mov ebp,esp       --el reg. esp se copia en ebp
                                              --ahora ebp=esp inicializando la nueva pila

Lo anterior es comunmente llamado un "stack-frame" en donde se ve la entrada a una funcion como una entidad independiente capaz de mantener información local mediante su pila, ya que cada base de la pila se mantiene independiente de la anterior.

Recordemos que la pila crece de manera inversa en la arquitectura intel, entonces para crear una pila más grande debemos "restar" al tope de la pila para que apunte a direcciones más "bajas", esto significa que si queremos tener 1 byte más de espacio en la pila debemos restar 1 al registro esp

Tambien es de notar que los valores de retorno de la función se realizan a traves del registro eax el cual es revisado por el compilador cuando una funcion devuelve un valor. En el anterior ejemplo la definición de la función main era


int main()
{
}

-----------------
GCC introduce por defecto devolver un valor de 0 

00000009  B800000000        mov eax,0x0

lo cual significa que main devuelve un entero, entonces el sistema operativo devolvera un valor entero al programa que tome lea el valor devuelto por main, el cual podría ser el propio shell (bash o cualquiera), internamente donde se consultará?, claro en el registro eax.

Con lo anterior ya sabemos como implementar funciones en un lenguaje de programacion.

Usando el mismo procedimiento podemos ver como GCC implementa variables locales, globales, estructuras, estructuras if, while for, etc. Por el momento podemos ver como genera gcc el código para variables locales dentro de funciones:



Original main2.c

kb@cobalt:/mnt/hda7/labs/www/escritos/code$ cat main2.c
int main()
        {
                char variable=0xef;
        }

====================================================


kb@cobalt:/mnt/hda7/labs/www/escritos/code$ gcc main2.c
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ ld -o main2  -Ttext 0x0 -e main  main2.o
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ objcopy -R .note -R .comment -S -O binary main2 main2.raw


kb@cobalt:/mnt/hda7/labs/www/escritos/code$ ndisasm  -b 32 main2.raw
00000000  55                push ebp
00000001  89E5              mov ebp,esp
00000003  83EC08            sub esp,byte +0x8
00000006  83E4F0            and esp,byte -0x10
00000009  B800000000        mov eax,0x0
0000000E  29C4              sub esp,eax
00000010  C645FFEF          mov byte [ebp-0x1],0xef
00000014  0FBE45FF          movsx eax,byte [ebp-0x1]
00000018  C9                leave
00000019  C3                ret


Vemos la misma rutina de inicializacion del stack-frame, ademas vemos tambien como el compilador utiliza al registro ebp para referenciar las variables locales:


1)--00000010  C645FFEF          mov byte [ebp-0x1],0xef
2)--00000014  0FBE45FF          movsx eax,byte [ebp-0x1]

======================

1)                     char variable=0xef;
2)      		return variable;


Como vemos la inicializacion "variable=0xef" se hace referenciando a [ebp-0x1], aqui el compilador utilizo un desplazamiento o "resto" 1 a la base de la pila (registro ebp) debido a que la "varianle" esta declarada como "char" que tiene 1 byte de tamaño.

Como habiamos mensionado el mandato "return = varible" hace que el contenido de la variable sea copiado en el registro eax inciso 2)

No he mensionado que la salida de la funcion esta a cargo (como ya se habran dado cuenta de la instrucciones:


00000018  C9                leave
00000019  C3                ret
=================
la instruccion leave ( opcode C9) hace un pop del stack-frame
mientras que ret (opcode C3) saca de la pila la dirección de retorno para despues "saltar"(jmp)
a la dirección mensionada.
 

Con el siguiente programa veremos como las funciones y los operadores como '=' se comunican pasando informacion atraves del registro eax. Veremos tambien como manipular este bloque de informacion de manera tal que simulemos como esta mapeada en el registro mediante una estructura.


cobalt:/mnt/hda7/labs/src/labs1/regs# cat chkreg.c
#include 
int main(){


typedef         union _reg32 {
                        unsigned int       eax ;
                        union _reg16 {
                                unsigned short  ax ;
                                struct _reg8 {
                                        unsigned char al ;
                                        unsigned char ah ;
                                } reg8;

                        } reg16 ;

                } reg_t   ;

        reg_t _reg;

           _reg.eax = scanreg();
        printf("%d bits  o %d bytes \n" ,  sizeof( double )* 8 , sizeof( double) );
        printf("------>Contenido de EAX :  %x con un tamaño de %d bits  |  %d bytes \n ", _reg.eax  , sizeof(unsigned int)*8 , sizeof(unsigned int) );
        printf("------>Contenido de AX :  %x  con un tamaño de %d bits  |  %d bytes \n ", _reg.reg16.ax , sizeof(short)*8  , sizeof(short  )     );
        printf("------>Contenido de Ah :  %x  con un tamaño de %d bits  |  %d bytes \n ",  _reg.reg16.reg8.ah , sizeof(unsigned char) *8 , sizeof(unsigned char ) );
        printf("------>Contenido de Al:  %x \n ", _reg.reg16.reg8.al  );
        return 0 ;
}

cobalt:/mnt/hda7/labs/src/labs1/regs# ./printreg
64 bits  o 8 bytes
------>Contenido de EAX :  ffefff0 con un tamaño de 32 bits  |  4 bytes
 ------>Contenido de AX :  fff0  con un tamaño de 16 bits  |  2 bytes
 ------>Contenido de Ah :  ff  con un tamaño de 8 bits  |  1 bytes
 ------>Contenido de Al:  f0



---------------codigo de scanreg.s
cobalt:/mnt/hda7/labs/src/labs1/regs# cat scanreg.s

global  scanreg
section .text

scanreg:
        push ebp
        mov ebp,esp
        mov edx , 0x0ffefff0
        mov eax, edx
        pop  ebp
        ret



El codigo anterior muestra mediante la funcion scanreg ( declarada en scanreg.s) guarda el valor 0xffefff0 en el registro eax. Vemos entonces como sacar la informacion que necesitamos sobre los bits que se encuentran en cada uno de los registros dento de eax, confirmando el hecho de que eax es usado por el compilador para devolver los resultados de las funciones en el caso del return.

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