Escribiendo un sencillo bootloader

Algunos tipos de malware se guardan así mismos en el Master Boot Record (en adelante MBR) como método de persistencia arrancándose durante el proceso de inicio del sistema. Recientemente, Marco Ramilli (basándose en los trabajos de Prabir Shrestha y Martin Splitt) explicaba brevemente como funcionaba el MBR y cómo escribir un programa bootloader, skill básica que nos ayudará a analizar artefactos de malware que implementen esta característica.

¿Cómo funciona el proceso de arranque de un PC?

En realidad, el proceso de arranque es súper fácil. Cuando presionamos el botón de encendido, proporcionamos la potencia necesaria para la electrónica del PC. Una vez que se enciende la BIOS, comienza ejecutando su propio código almacenado y cuando termina de ejecutar sus rutinas de inicialización, busca dispositivos de arranque.

Un dispositivo de arranque es un dispositivo conectado físicamente que tiene 521 bytes de código al principio y que contiene el número mágico de arranque: 0x55AA como últimos 2 bytes. Si la BIOS encuentra 510 bytes seguidos de 0x55AA, toma los 510 bytes anteriores los mueve a la RAM (a la dirección 0x7c00) y asume que son bytes ejecutables. Este código es el llamado gestor de arranque.

Solo una nota al margen: el gestor de arranque se escribirá en 16 bits ya que las CPU compatibles con x86 funcionan en "modo real" debido al limitado conjunto de instrucciones disponibles.

El código asm

El siguiente código en ensamblador con la sintaxis AT&T se ejecuta en el arranque mostrando 3 strings y una especie de progresión a modo reloj. Como la BIOS está "cerca" de la memoria, podemos usar un conjunto completo de instrucciones de BIOS e interrupciones como se muestran a continuación:

1. Int_10,02 para configurar el tamaño de la pantalla
2. int_10,07 para limpiar la pantalla de las salidas de la BIOS
3. int_12a, 02 para configurar las posiciones del cursor
4. int_1a, 02 para leer el estado del reloj
5. int_10,0e para escribir caracteres en la pantalla

1:  .code16 # usa 16 bits  
2:  .global main  
3:    
4:  main:  
5:   mov $0x0002, %ax  
6:   int $0x10 #setea 80x25 modo texto  
7:    
8:   mov $0x0700, %ax  
9:   mov $0x0f, %bh  
10:   mov $0x184f, %dx  
11:   xor %cx, %cx  
12:   int $0x10 #limpia la pantalla (fondo negro)  
13:   jmp print_message  
14:    
15:    
16:  print_living_clock:  
17:    
18:   mov $0x02, %ah  
19:   mov $0x00, %bh  
20:   mov $0x012a, %dx  
21:   int $0x10 #resetea la posición del cursor  
22:    
23:   # Lee el Timer  
24:   mov $0x02, %ah  
25:   int $0x1a  
26:     
27:   # Imprime Horas   
28:   mov $0x0e, %ah  
29:   mov %ch, %al  
30:   int $0x10   
31:    
32:   # Imprime '/'  
33:   mov $0x0e, %ah  
34:   mov $0x2f, %al  
35:   int $0x10  
36:    
37:   # Imprime Minutos  
38:   mov $0x0e, %ah  
39:   mov %cl, %al  
40:   int $0x10  
41:    
42:   # Imprime '/'  
43:   mov $0x0e, %ah  
44:   mov $0x2f, %al  
45:   int $0x10  
46:    
47:   # Imprime Segundos  
48:   mov $0x0e, %ah  
49:   mov %dh, %al  
50:   int $0x10  
51:     
52:   jmp print_living_clock  
53:    
54:  print_message:  
55:   mov $0x02, %ah  
56:   mov $0x00, %bh  
57:   mov $0x0000, %dx  
58:   int $0x10 # configura la posición del cursor   
59:    
60:   mov $msg, %si # carga la dirección del msg dentro de si  
61:   mov $0x0001, %cx  
62:   mov $0xe, %ah # carga 0xe (función number para int 0x10) dentro de ah  
63:   jmp print_char  
64:    
65:  print_message_2:  
66:   mov $0x02, %ah  
67:   mov $0x00, %bh  
68:   mov $0x0100, %dx  
69:   int $0x10 # configura la posición del cursor  
70:    
71:   mov $msg2, %si # carga la dirección del msg dentro de si  
72:   mov $0x0002, %cx  
73:   mov $0xe, %ah # carga 0xe (función number para int 0x10) dentro de ah  
74:   jmp print_char  
75:    
76:  print_message_3:  
77:   mov $0x02, %ah  
78:   mov $0x00, %bh  
79:   mov $0x0200, %dx  
80:    
81:   int $0x10 # configura la posición del cursor  
82:   mov $msg3, %si # carga la dirección del msg dentro de si  
83:   mov $0x0003, %cx  
84:   mov $0xe, %ah # carga 0xe (función number para int 0x10) dentro de ah  
85:   jmp print_char  
86:    
87:  print_char:  
88:   mov $0x0e, %ah  
89:   lodsb # carga el byte de la dirección en si dentro de al e incrementa si  
90:   cmp $0, %al # compara el contenido de AL con zero  
91:   je done # if al == 0, go to "done"  
92:   mov $0xc0, %bl  
93:   int $0x10 # imprime el caracter en al a pantalla  
94:   jmp print_char # lo repite con el siguiente byte  
95:    
96:  done:  
97:   cmp $0x0001, %cx  
98:   je print_message_2  
99:     
100:   cmp $0x0002, %cx  
101:   je print_message_3  
102:    
103:   cmp $0x0003, %cx  
104:   je print_living_clock  
105:     
106:  end:  
107:   hlt # para la ejecuciónMarco Ramilli  
108:  msg: .asciz "===================================================="  
109:  msg2: .asciz "     Ejemplo de programa de arranque      "  
110:  msg3: .asciz "===================================================="  
111:    
112:  .fill 510-(.-main), 1, 0 # añade 0s hasta 510 bytes long  
113:    
114:  .word 0xaa55 # byte mágico para decirle a la BIOS que es bootable  

Las dos primeras líneas:

1: .code16
2: .global main

indican que el código se escribirá en modo de 16 bits y la función etiquetada externa (expuesta) es la etiquetada como "main" (el linker lo necesita para configurar el punto de entrada original en el espacio de direcciones adecuado).

Las dos últimas líneas:

112: .fill 510 - (.- init), 1, 0
114: .word 0xaa55

indican que el código es bootable. En la línea 112 tenemos el comando de llenado que el compilador interpretará escribiendo de nops (hasta 510 bytes) para mantener la estructura del MBR. La línea 113 tiene el código mágico en little endian.

Todo el código usa el registro %cx para el estado actual. Por ejemplo, %cx podría ser: 0x0000 si se imprime msg, 0x0001 si se imprime msg2, 0x0002 si se imprime msg3 y 0x0003 si queremos iniciar el ciclo de impresión del reloj. Se usa un comando lodsb para iterar sobre los caracteres de cadena a fin de imprimirlos hasta el byte nulo (\0).

Nota (gracias Fare9): el código al ser 16 bits, no es muy complejo de analizar y las interrupciones son de sobra conocidas, un pdf como este creo que te vienen todas o casi todas: http://www2.ift.ulaval.ca/~marchand/ift17583/dosints.pdf

Herramientas usadas

En el ejemplo se utilizará GNU Assembler (compilador y linker) que implementa la sintaxis de AT&T, que es bastante diferente a la de Intel pero funciona bien para el código sencillo que vamos a usar.

Lo primero que usaremos es el compilador GNU (as), que tomará como entrada un archivo en ensamblador y devolverá su representación binaria:  

as -o boot.o boot.asm

Luego usaremos el enlazador o linker GNU (ld) para obtener un archivo binario sencillo sin librerias ni símbolos vinculados: --oformat binary
También tenemos que decirle al linker dónde comienza el código (-e main) y agregaríamos el parámetro -Ttext 0x7c00 en caso de que el código que vamos a escribir no se ajuste a un espacio de direcciones de 16 bits, por lo que forzaremos a nuestro linker a mapear la función main en dicha dirección, que sabemos que es la dirección donde la BIOS ejecuta el cargador de arranque o bootloader.

En definitiva, suponiendo que nuestro código se llana boot.asm y nuestro entry point original es 'main', podríamos usar el siguiente comando:

ld -o boot.bin --oformat binary -e main -Ttext 0x7c00 -o boot.bin boot.o

Y para ejecutar el código compilado, usaremos qemu de la siguiente manera:

qemu-system-x86_64 boot.bin


Nota (gracias Fare9): lo bueno de qemu es que te permite especificar un flag de depuración por tcp, al que puedes conectarte con gdb en remoto (o IDA a través de gdb), poner el breakpoint en 0x7C00, y en cuanto la bios haya cargado el MBR, empezar a depurar: https://en.wikibooks.org/wiki/QEMU/Debugging_with_QEMU

Referencias:

- https://marcoramilli.com/2019/09/03/writing-your-first-bootloader-for-better-analyses/
- https://50linesofco.de/post/2018-02-28-writing-an-x86-hello-world-bootloader-with-assembly
- https://securityaffairs.co/wordpress/90733/malware/writing-bootloader.html
- https://www.codeproject.com/articles/664165/writing-a-boot-loader-in-assembly-and-c-part
- https://github.com/prabirshrestha/writing-an-os-from-scratch/blob/master/src/bootloader/6-asm/boot.S

3 comentarios :

  1. Que nota tan interesante, gracias por compartir.

    ResponderEliminar
  2. Es muy bueno el artículo pero esto no es nuevo yo tuve varias experiencias con este tipo de millares hace años atrás y el mentado caries que afectaba el sistema de entablamiento de archivos Cómo el NTFS y FAT32

    ResponderEliminar
  3. Es muy interesante. Gracias por compartirlo

    ResponderEliminar