Fuerza bruta mecánica: Ganando al Mezcladitos jugando de verdad

//

Este artículo lo publiqué originalmente en Security By Default.


Jhonny5

El mes pasado, nuestro amigo Lorenzo Martínez nos enseñó como ganar siempre a Mezcladitos interceptando las comunicaciones entre el juego y el servidor.

Como soy de ciencias, y en este tipo de juegos llevamos las de perder contra los de letras, y como no me gusta hacer trampas para ganar, decidí buscar una forma de jugar el juego utilizando mis conocimientos técnicos:



Bueno, quizá si sean trampas, pero al menos no estamos hackeando el sistema ;)

Esto es solo una inocente demostración de lo que es la fuerza bruta mecánica. Cuando escuchamos hablar sobre fuerza bruta, la mayoría de las veces nos viene a la cabeza el "ensayo y error" software para atacar contraseñas cifrando y comprobando si son válidas. Sin embargo podemos aplicarla mecánicamente a todo tipo de cosas: teclados virtuales, teclados físicos, roscas, etc.

En esta demostración he jugado al Mezcladitos de Facebook, sin embargo me hubiese gustado disponer de unos cuantos servos y un arduino para jugar directamente sobre la pantalla del iPhone (haciendo un sencillo "tecleador" mecánico).

Lo primero fue crear un algoritmo que resuelve el casillero, buscando por fuerza bruta todas las palabras que contiene basandome en un diccionario con todas las palabras del idioma español.

Dado que tenemos solo dos minutos para jugar, necesitamos calcular las palabras en el menor tiempo posible, así que ademas de optimizar el algoritmo, lo lanzamos en 16 threads (uno por cada casilla) para aprovechar todos los núcleos de nuestra CPU. Dicho algoritmo recorre todos los caminos posibles desde una casilla y comprueba si la palabra resultante en cada uno de ellos existe en el diccionario.


using MezcladitosPWNer;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading;
using System.Linq;

namespace MezcladitosPWNer {

    public class FuerzaBruta {
        private ManualResetEvent _doneEvent;
        private string _letra;
        private int _x;
        private int _y;
        private List<string> _ruta;
        private string _palabra;

        private int[,] adyacentesA = { { -1, -1 }, { 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 }, { 0, 1 }, { -1, 1 }, { -1, 0 } };
        private int[,] adyacentesB = { { 1, 0 }, { 1, 1 }, { 0, 1 }, { -1, 1 }, { -1, 0 } };
        private int[,] adyacentesC = { { 1, 0 }, { 1, 1 }, { 0, 1 } };
        private int[,] adyacentesD = { { 0, 1 }, { -1, 1 }, { -1, 0 } };
        private int[,] adyacentesE = { { -1, 0 }, { -1, -1 }, { 0, -1 }, { 1, -1 }, { 1, 0 } };
        private int[,] adyacentesF = { { 0, -1 }, { 1, -1 }, { 1, 0 } };
        private int[,] adyacentesG = { { -1, 0 }, { -1, -1 }, { 0, -1 } };
        private int[,] adyacentesH = { { 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 }, { 0, 1 } };
        private int[,] adyacentesI = { { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 }, { 0, -1 } };



        public FuerzaBruta(int x, int y, List<string> ruta, string palabra, ManualResetEvent doneEvent) {
            _doneEvent = doneEvent;
            _x = x;
            _y = y;
            _ruta = ruta;
            _palabra = palabra;
        }


        public void ThreadPoolCallback(Object threadContext) {
            int threadIndex = (int)threadContext;
            ady(_x, _y, _ruta, _palabra);
            _doneEvent.Set();
        }


        private void ady(int x, int y, List<string> pRuta, string palabra) {
            if (_doneEvent.WaitOne(0)) {
                return;
            }

            List<string> ruta;
            ruta = new List<string>(pRuta);
            Regex regPart;
            int palMaxTam;
            Match m;

            if (ruta.Capacity > Globales.maxLength - 1) {
                return;
            }

            ruta.Add(x.ToString() + y.ToString());
            palabra += Globales.cas[x][y];

            if (palabra.Length == 1) {
                _letra = palabra;
            }

            if (palabra.Length >= Globales.minLength) {
                palMaxTam = Globales.maxLength - palabra.Length;

                if (Globales.arrays[_letra].Contains(palabra)) {
                    Globales.frm.pwnWord(palabra, new List<string>(ruta));
                    Globales.frm.AsyncWriteLine(palabra + " ");
                }

                regPart = new Regex(@"#(" + palabra + "\\S{" + palMaxTam + "})#");
                m = regPart.Match(Globales.cadenas[_letra]);

                if (!m.Success) {
                    return;
                }
            }

            if (y > 0 && y < 4 - 1 && x > 0 && x < 4 - 1) {
                for (int i = 0; i < adyacentesA.GetLength(0); i++) {
                    profundizar(x + adyacentesA[i, 0], y + adyacentesA[i, 1], ruta, palabra);
                }
            }
            if (y == 0) {
                if (x > 0 && x < 4 - 1) {
                    for (int i = 0; i < adyacentesB.GetLength(0); i++) {
                        profundizar(x + adyacentesB[i, 0], y + adyacentesB[i, 1], ruta, palabra);
                    }

                } else if (x == 0) {
                    for (int i = 0; i < adyacentesC.GetLength(0); i++) {
                        profundizar(x + adyacentesC[i, 0], y + adyacentesC[i, 1], ruta, palabra);
                    }
                } else if (x == 4 - 1) {
                    for (int i = 0; i < adyacentesD.GetLength(0); i++) {
                        profundizar(x + adyacentesD[i, 0], y + adyacentesD[i, 1], ruta, palabra);
                    }
                }
            }
            if (y == 4 - 1) {
                if (x > 0 && x < 4 - 1) {
                    for (int i = 0; i < adyacentesE.GetLength(0); i++) {
                        profundizar(x + adyacentesE[i, 0], y + adyacentesE[i, 1], ruta, palabra);
                    }
                } else if (x == 0) {
                    for (int i = 0; i < adyacentesF.GetLength(0); i++) {
                        profundizar(x + adyacentesF[i, 0], y + adyacentesF[i, 1], ruta, palabra);
                    }
                } else if (x == 4 - 1) {
                    for (int i = 0; i < adyacentesG.GetLength(0); i++) {
                        profundizar(x + adyacentesG[i, 0], y + adyacentesG[i, 1], ruta, palabra);
                    }
                }
            }
            if (x == 0 && y != 0 && y != 4 - 1) {
                for (int i = 0; i < adyacentesH.GetLength(0); i++) {
                    profundizar(x + adyacentesH[i, 0], y + adyacentesH[i, 1], ruta, palabra);
                }
            }
            if (x == 4 - 1 && y != 0 && y != 4 - 1) {
                for (int i = 0; i < adyacentesI.GetLength(0); i++) {
                    profundizar(x + adyacentesI[i, 0], y + adyacentesI[i, 1], ruta, palabra);
                }
            }
        }

        private void profundizar(int x, int y, List<string> ruta, String palabra) {
            if (ruta.LastIndexOf(x.ToString() + y.ToString()) == -1) {
                ady(x, y, ruta, palabra);
            }
        }
    }
}

Una vez tenemos todos los resultados, hacemos un hook a bajo nivel del ratón con SetWindowsHookEx, de ese modo puedo moverlo y hacer clicks automatizados para cada palabra.

Como se puede ver en el video, antes de iniciar se hace una calibración haciendo click sobre las dos primeras casillas y el "Enter", para conocer las coordenadas de todas las casillas del tablero.


public static IntPtr mouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
         if (nCode >= 0 && InputHooks.MouseMessages.WM_LBUTTONDOWN == (InputHooks.MouseMessages)wParam && Globales.ctrlDOWN == true) {
             InputHooks.MSLLHOOKSTRUCT hookStruct = (InputHooks.MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(InputHooks.MSLLHOOKSTRUCT));
            if (Globales.siguiente == 0) {
                 Globales.coordCasilla1 = hookStruct.pt;
             } else if (Globales.siguiente == 1) {
                 Globales.coordCasilla2 = hookStruct.pt;
             } else if (Globales.siguiente == 2) {
                 Globales.coordBtnIntroducir = hookStruct.pt;
                 Globales.startPWN = true;
             }
             Globales.siguiente++;
         }
             return InputHooks.CallNextHookEx(mouseHookID, nCode, wParam, lParam);
     }
while (Globales.palabrasEncontradas.Count > 0) {
             palabra = Globales.palabrasEncontradas.Pop();
             ruta = Globales.rutasEncontradas.Pop();

             for (int i = 0; i < ruta.Count; i++) {
                 InputHooks.ClickLeftMouseButton(Globales.coordCasilla1.X + (Globales.calibrado * Convert.ToInt32(ruta[i][1].ToString())),
                                            Globales.coordCasilla1.Y + (Globales.calibrado * Convert.ToInt32(ruta[i][0].ToString())));
                 System.Threading.Thread.Sleep(50);
             }
             System.Threading.Thread.Sleep(50);
             InputHooks.ClickLeftMouseButton(Globales.coordBtnIntroducir.X, Globales.coordBtnIntroducir.Y);
             System.Threading.Thread.Sleep(50);
         }

Hemos hecho fuerza bruta para ganar a un juego, pero podría utilizarse para cosas más serias como ganar acceso no autorizado tanto a sitios web como a lugares físicos, si no utilizan medidas extra de seguridad.

En este caso estamos utilizando el ratón como método de entrada automatizado. Algunos sitios web emplean teclados virtuales para hacer login (incluso algunos bancos) utilizando el ratón. A veces incluso cambian aleatoriamente el orden de las letras o números (nada que no se pueda saltar con un algoritmo OCR). Con ese método muchas veces creen equivocadamente que añaden una capa de seguridad, pero si no limitan el número de intentos se puede hacer fuerza bruta fácilmente (y si te están pidiendo un PIN de 4 números, no tardaría más que unos minutos).

Sin embargo como comentaba antes, disponiendo de un arduino y unos cuantos motores, podemos hacer fuerza bruta sobre prácticamente cualquier cosa de forma muy sencilla y barata siempre y cuando no nos limíte en número de intentos, lo que nos hace preguntarnos cuán seguros son los dispositivos que utilizamos habitualmente, y como muchas veces obvian ciertas medidas de seguridad al pensar que obligando a alguien a introducir datos manualmente no probarán todas las combinaciones. O cuantas veces quitamos nosotros mismos ese número de intentos "por miedo a bloquear nuestro dispositivo nosotros mismos".

En plena era digital, existe mucha gente (especialmente gente mayor, o con pocos conocimientos informáticos) que piensa que lo analógico es más seguro que lo digital, sobretodo cuando cada vez hay más noticias en la prensa generalista sobre incidentes de hacking; así que aquí van unos cuantos ejemplos para romper esa leyenda urbana (Nota: Algunos de estos ejemplos se bloquean despues de un número de intentos, pero los pongo para hacer un muestrario del tipo de cosas que se pueden atacar físicamente):


01
02
03
04
05
06
07
08
09


Moraleja: Si estais implementando algun tipo de mecanismo para restringir accesos, nunca olvideis limitar el número de intentos (el cual en muchas ocasiones es visto como "innecesario" o engorroso), y procurad incluir algún elemento extra como un tarjeta, una llave,...

Si disponeis de algún mecanismo de acceso que limite el número de intentos y os permita deshabilitarlo, nunca lo hagais por comodidad o miedo.

Os dejo el código fuente del programa para los que querais echarle un vistazo (no incluyo los diccionarios). https://github.com/moebiuz/mezclaPWNr