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
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
¿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
Que nota tan interesante, gracias por compartir.
ResponderEliminarEs 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
ResponderEliminarEs muy interesante. Gracias por compartirlo
ResponderEliminarYo solo quiero usar hck en mi juego yo tengo 20 años
ResponderEliminarque buena llevo años buscando algo como esto.
ResponderEliminar