Imaginemos la siguiente situación: Usted tiene la intención de realizar
un ataque de fuerza bruta sobre una contraseña aplicando un algoritmo
dado. Entonces debe de tomar algunas decisiones importantes que
influirán en el rendimiento y el tiempo de ejecución que su programa de
crackeo tardará en completar el proceso. A pesar de que puede escribir
su aplicación ayudado por las facilidades de algún lenguaje interpretado
(como
Perl o
Python), se da cuenta de que escribir el código en
C y obtener un binario compilado puede brindarle algunas mejoras significativas en lo que a performance se refiere.
Finalmente, y antes de correr el programa, cierra todas las aplicaciones
y procesos que pudiesen estar consumiendo recursos del
sistema: el
navegador web, el procesador de textos y quizás hasta su preciado
antivirus. Ahora ya puede dejar que su ordenador haga todo el trabajo y
sentarse en el sofá a la espera de resultados, al fin y al cabo su
procesador se encuentra ocupado únicamente con el algoritmo de
bruteforce, ¿verdad?
La cruda realidad no muestra una cara tan bonita, por desgracia. Puede
regresar a su equipo y abrir el monitor de procesos/administrador de
tareas, tranquilamente otros 20 procesos podrían estar ejecutándose
además del suyo, y algunos de ellos ni siquiera pueden “matarse” sin
cargarse el
sistema. Pero esto no es más que la punta del iceberg, su
SO está gestionando por detrás múltiples hilos de
kernel, interrupciones, mecanismos de entrada/salida (
I/O), y si todavía no se ha desconectado de Internet, atendiendo a todos los paquetes que entran y salen por la interfaz de red.
Y hay más, muchísimo más, su querido
sistema multitarea realiza controles de colas, intercambio de stacks entre procesos,
IPC,
y está haciendo verdaderas virguerías con la paginación y gestión de
memoria virtual ayudado por el hardware subyacente. Digamos que cada
100ms
una interrupción se genera en el procesador (ticks de reloj) y el
núcleo del sistema retoma el control e invoca el scheduler para
comprobar cuál es el próximo proceso que merece su tiempo (slice). Desde
luego, esto es lo que hace un sistema
operativo moderno, y otra
infinidad de tareas que de forma intencionada nos dejamos en el tintero,
pero usted solo quería crackear una contraseña, todo lo demás es
“tiempo perdido”.
Bajo esta premisa decidí realizar la siguiente demostración. Cree un pequeño programa que aplicara la
conjectura de Collatz para los primeros cincuenta millones de números. Al igual que un algoritmo de
bruteforce,
esto proporciona alimento suficiente para el procesador. El cálculo es
sencillo, se selecciona un número, si es par se divide entre 2 y si es
impar se multiplica por 3 y se suma 1, y se vuelve a aplicar el mismo
proceso sobre el resultado. La conjetura dice que para todos los números
naturales la secuencia siempre termina en 1. Observe el siguiente
programa:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
void print_time(void)
{
char buff[32];
time_t now;
memset(buff, 0, 32);
now = time(0);
strftime (buff, 32, "%H:%M:%S", localtime(&now));
printf("%s
", buff);
}
int main(int argc, char *argv[])
{
unsigned int i, n;
print_time();
for ( i = 1; i < 50000000; i++ ) {
n = i;
while ( n != 1 ) {
if ( n % 2 == 0 )
n = n / 2;
else
n = n * 3 + 1;
}
}
print_time();
return 0;
}
Luego lo ejecutamos en una distribución
Ubuntu 12.04 (3.5.0-40) sobre un procesador
Intel(R) Core(tm)2 Duo T8300 2.40 GHz con
3GB de memoria
RAM. La imagen muestra el resultado.
|
Figura 1: Tiempo de ejecucion del programa de ejemplo |
El cálculo se ha prolongado por
50 segundos del reloj. Y
ahora viene la pregunta clave, ¿qué ocurriría si pudiésemos hace que el
procesador dedicase todo su tiempo a nuestro algoritmo? La solución
pasaba por crear un bootloader en el primer sector de un disquete o
USB que simplemente crease una
GDT básica para pasar del modo real al modo protegido (facilitando así la posibilidad de ejecutar código
C) y luego aplicar el mismo algoritmo.
No tenemos la intención de mostrar aquí el código completo, solo lo
suficiente para que comprenda la explicación. He aquí la primera parte
del proceso de
boot en ensamblador. En lo que nos
concierne, no hace falta que lo comprenda, digamos que simplemente pasa
al modo protegido y luego llama a una función
bootmain().
.code16
.globl start
start:
cli
xorw %ax,%ax
movw %ax,%ds
movw %ax,%es
movw %ax,%ss
clear_scr:
movb $0x06,%ah
movb $0x07,%bh
xorw %cx,%cx
movb $24,%dh
movb $79,%dl
int $0x10
seta20.1:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.1
movb $0xd1,%al # 0xd1 -> port 0x64
outb %al,$0x64
seta20.2:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.2
movb $0xdf,%al # 0xdf -> port 0x60
outb %al,$0x60
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE, %eax
movl %eax, %cr0
ljmp $(SEG_KCODE<<3), $start32
.code32
start32:
movw $(SEG_KDATA<<3), %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %ss # -> SS: Stack Segment
movw $0, %ax # Zero segments not ready for use
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movl $start, %esp
call bootmain
spin:
jmp spin
.p2align 2
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg
gdtdesc:
.word (gdtdesc - gdt - 1) # sizeof(gdt) - 1
.long gdt # address gdt
Y ahora la función
bootmain() en
C que finalmente ejecuta la
Conjetura de Collatz e imprime el rango de tiempo:
#include "types.h"
static ushort *crt = (ushort*)0xb8000; // CGA memory
void
bootmain(void)
{
unsigned int n, i;
unsigned short segundos = 0x00, minutos = 0x00, horas = 0x00;
asm volatile ("xorb %%al,%%al;"
"out %%al, $0x70;"
"in $0x71, %%al": "=a"(segundos));
asm volatile ("movb $0x02,%%al;"
"out %%al, $0x70;"
"in $0x71, %%al": "=a"(minutos));
asm volatile ("movb $0x04,%%al;"
"out %%al, $0x70;"
"in $0x71, %%al": "=a"(horas));
crt[160] = ((horas >> 4) + '0') | 0x0a00;
crt[161] = ((horas & 0x0f) + '0') | 0x0a00;
crt[162] = ':' | 0x0a00;
crt[163] = ((minutos >> 4) + '0') | 0x0a00;
crt[164] = ((minutos & 0x0f) + '0') | 0x0a00;
crt[165] = ':' | 0x0a00;
crt[166] = ((segundos >> 4) + '0') | 0x0a00;
crt[167] = ((segundos & 0x0f) + '0') | 0x0a00;
for ( i = 1; i < 50000000; i++ ) {
n = i;
while ( n != 1 ) {
if ( n % 2 == 0 )
n = n / 2;
else
n = n * 3 + 1;
}
}
asm volatile ("xorb %%al,%%al;"
"out %%al, $0x70;"
"in $0x71, %%al": "=a"(segundos));
asm volatile ("movb $0x02,%%al;"
"out %%al, $0x70;"
"in $0x71, %%al": "=a"(minutos));
asm volatile ("movb $0x04,%%al;"
"out %%al, $0x70;"
"in $0x71, %%al": "=a"(horas));
crt[240] = ((horas >> 4) + '0') | 0x0a00;
crt[241] = ((horas & 0x0f) + '0') | 0x0a00;
crt[242] = ':' | 0x0a00;
crt[243] = ((minutos >> 4) + '0') | 0x0a00;
crt[244] = ((minutos & 0x0f) + '0') | 0x0a00;
crt[245] = ':' | 0x0a00;
crt[246] = ((segundos >> 4) + '0') | 0x0a00;
crt[247] = ((segundos & 0x0f) + '0') | 0x0a00;
return;
}
En el centro de este código observamos el mismo bucle
for() que en el programa original que ejecutamos en
Linux,
todo lo demás son las virguerías que hay que hacer para interactuar con
los puertos y obtener la hora de la máquina. Recuerde que lo que
estamos haciendo en realidad es programar un
“mini sistema operativo”
que únicamente ejecuta nuestro algoritmo y luego entra en un bucle
infinito. Una vez que compilamos todo el tinglado y lo insertamos en un
disquete (obviamos este proceso en el artículo), accedemos a la
BIOS para indicarle que arranque desde el
floppy. En la imágen el resultado:
|
Figura 2: Resultado obtenido prescindiendo del sistema operativo |
El proceso ha durado tan solo
30 segundos frente a los
50 invertidos por el sistema operativo. Sorprendentemente hemos realizado la misma tarea en un
60% del tiempo inicial, lo cual quiere decir, de forma aproximada, que un ataque de
bruteforce que se prolongase por
24 horas, podría realizarse en unas
14 horas “si prescindimos del sistema operativo”.
Otra prueba sobre
Windows XP con un procesador
AMD Athlon(tm) 64 3000+ 1.81GHz y 512 MB de RAM, proporcionó un resultado de
46 segundos frente a los
68 que tardaba la aplicación en la
shell del sistema operativo.
¿Qué es un sistema operativo? Pues esos 22 segundos “fantasmas” de diferencia que usted no sabía que podía ahorrarse.
Obviamente, esta demostración y el artículo que está leyendo no son más
que una demostración curiosa. Usted necesita un sistema operativo para
trabajar y créame que hoy en día estos invierten ese tiempo fantasma de
una forma más económica y elegante que hace algunos años.
Las pruebas se han realizado sobre sistemas operativos de
32 bits, con un procesador
x86_64 y un
Windows 7 de 64 bits (por poner un ejemplo), seguramente habría que trabajar sobre
long mode para poder realizar comparativas fiables...
Entienda que estamos programando la máquina desde cero, y no disponemos
de ninguna de las facilidades que un sistema operativo le ofrece al
programador, no existen librerías del sistema y todo debe hacerse a bajo
nivel, pero no sería descabellado crear un sencillo
framework con un
bootloader básico que cargase un
kernel
mínimo en el que usted pueda insertar su algoritmo. Funciones de manejo
de cadenas y otras de salida por pantalla pueden ser creadas de
antemano sin mucho esfuerzo y proporcionadas por anticipado. No es más
que una idea... interactuar directamente con la
GPU de la tarjeta gráfica siempre parece más atractivo.
Toda esta teoría también podría aplicarse a un dispositivo
Raspberry Pi si usted es capaz de crear un
bootloader para
ARM, prescindiendo así de la distribución
Raspbian de
Linux. Estos aparatos pueden realizar algunas tareas costosas si se combinan en un potente
cluster,
pero si usted no es un gurú o un ninja de la programación, será
realmente complicado comunicar entre sí todos los dispositivos y
realizar cualquier tipo de procesamiento paralelo.
Por lo demás…
Happy Hacking!
by blackngel (blackngel@blackbycode.com)