Para acompañar un poco el verano hice 3 retos de reversing muy fáciles y para todo el público. Los retos son del maestro Ricardo Narvaja donde cada fin de semana realiza «juntadas» o reuniones en streaming donde se aplica la resolución de los retos semanales que sube. En este grupo de Telegram es donde encontrarán toda la información de los retos, resoluciones y futuros ejercicios. Comenzaremos por el primer reto, denominado «DesktopApp Crackme». Es ideal para comenzar la ingeniería inversa ya que podemos hacer uso de las API’s de Windows y así poner Breakpoints y obtener como referencias de código de usuario que nos interese para localizar el input.
Colocamos un breakpoint en las siguientes API’s, is DebuggerPresent, MessageBoxW, MessageBoxA y GetMessageW. En esta última para antes de ir a erróneo cuando introducimos un serial incorrecto. Usaremos por tanto esa API como referencia ya que el programa es un bucle y si falla pues saltara a usar esa API antes mencionada. Pero eso no es todo, se ejecuta varias veces hasta que llega a la API MessageBoxA donde muestra el output finalmente erróneo.
Gracias a esta traza con el debugger si ejecutamos hasta la última API mencionada hasta return y seguidamente hacemos step, localizamos la parte del código de la aplicación que nos interesa.
Es obvio y sencillo localizar en la parte de código en ensablador donde esta la zona de «Felicitaciones» y la de «Sigue participando».
Como ya sabemos las Strings, hacemos uso de las referencias de cadena en el código, aun asi, podriamos haber usado ese método, mucho más rápido que el hacer uso de las API’s, aunque quería mostrar este porque en analisis de malware por ejemplo para hacer unpacking es muy util poner breakpoints en las API’s objetivo. Incluso en el tercer reto que vamos a realizar en este post, veremos que el hacer uso de una búsqueda por Strings no será válido y tendremos que mejorar nuestro esfuerzo de «recon».
Localizamos una instrucción CMP y atrás un ADD de un valor hardcodeado con nuestro input. Por tanto, la algoritmia es muy básica, tan básica que simplemente es una simple resta.
INPUT+1680151780 = 2909951527
Se transforma de HEX a ASCII y ya tenemos el Serial.
Segundo reto «GUISITO»
En vez de usar el procedimiento anterior, realizaremos una búsqueda por Strings. Tenemos GENIAL, asi que será lo correcto? Colocaremos un breakpoint en GetDlgItemTextA. Si introducimos «AAAA» vemos en el registro EAX como 0x41414141 y es comparado por 0x49500003.
Lo cual tenemos un problema. No podemos introducir un valor NULL, ya que no sigue leyendo el resto de input. Si fuese carácteres que no están en el teclado podríamos realizar un scriptcillo en Rust para ello, pero como esta la existencia de un valor nulo, imposible. Por tanto, esta claro que esa no es la solución. Si fuese un programa y queremos saltar esa protección, tan simple como modificar en memoria usando el debugger y listo, pero los tiros no van por ahí. Tenemos que conseguir resolverlo sin tocar o editar nada en memoria.
Se da la existencia en el código de otra String denominada «SI». Efectivamente esa deberá de ser la solución, no puede existir otra. El programa en primera instancia nunca pasa por allí, ¿por qué?. Es posible que se tenga que introducir «algo» para que pase por allí.
Viendo el penúltimo video de la «juntada», la del 10 de Julio vemos que el binario es parecido ya que hace uso de mensajes de Windows WM. Por ejemplo, WM_PAINT pintará el estilo del Registrame. Aun asi antes de hacer uso con IDA de poner las constantes, ya que cada constante en hexadecimal corresponde a un mensaje de Windows. Es decir, WM_GETTEXT es 0xD, por poner un ejemplo.
Usaremos breakpoints condicionales usando x64dbg, muy útiles para así saber cuando para, según una condición.
La única opción para llegar a la localización renombrada por nosotros «REGISTRAME» es que la resta de EAX con 0xF sea 0x0 para que así tome el salto condicional JZ. Como la función WndProc esta en bucle, usaremos un breakpoint condicional tal que EAX == 0.
Ejecutamos el debugger, y observamos como el registro ECX=0x111. Por tanto, ahora si, esta constante corresponde a WM_COMMAND.
0x111 - 0x102 = 15
15-0xf = 0 => JZ salto.
Y podemos corroborar que efectivamente si solo hacemos un solo click en el cuadro para introducir texto, se para en el breakpoint condicional.
Usando mismo procedimiento, podremos un breakpoint condicional justo en el anterior salto JZ, donde se dirige a la dichosa String «SI».
Haciendo testing random, nunca llega. Entonces usemos la lógica más aplastante de todas. Sabiendo que 0x102 es WM_CHAR, veamos en Don Google que tipo de mensaje de Windows captura exactamente. Captura las pulsaciones de teclado, pero obviamente no en el cuadro de dialogo donde se supone que se pueden ingresar por teclado una cadena de caracteres.
Por tanto, se supone que si presionamos cualquier tecla debería de parar en el breakpoint condiconal. Y es cierto, así sucede, por fin llegamos a la parte de código objetivo!!!.
Ahora es muy sencillo, se trata de unas operaciones aritméticas básicas hasta poder entrar en la estructura de control donde mostrará usando SetDlgItemTextA la cadena de texto «SI». Simplemente el puntero dword_B94024 contiene el numero 0x9. Entonces tendremos que pulsar en el teclado el número 9, para que asi sea 0x39 en hexadecimal (0x39-48 = 0x9). Seguidamente se decrementa el puntero dword_B9024 en 2, siendo la serie buscada como 9,7,5,3 y 1. Como son 5, nos bastará para entrar en la zona de la API antes mencionada, tal y como el programa ha sido prediseñado, y mostrará la string «SI», siendo la solución final del reto. Haciendo uso de código en Rust de un fuzzer para Windows creado por gamozolabs, me parece muy interesante haciendo una serie de modificaciones y así usar las API’s de Windows para identificar la ventana del objetivo, obtener el handle y enviar el input deseado.
use std::io; use std::io::Error; use std::cell::Cell; use std::io::prelude::*; use std::convert::TryInto; //SIZES IN DOCS.MICROSFOT API's == RUST /* UINT == u32 LPINPUT (ARRAY OF INPUT STRUCTS) == *mut Input int == i32 WORD == u16 DWORD == u32 ULONG_PTR == usize LONG == i32 */ fn pause() { let mut stdin = io::stdin(); let mut stdout = io::stdout(); // We want the cursor to stay at the end of the line, so we print without a newline and flush manually. write!(stdout, "Press ENTER...").unwrap(); stdout.flush().unwrap(); // Read a single byte and discard let _ = stdin.read(&mut [0u8]).unwrap(); } #[link(name="User32")] //DLL necesaria para el uso de las API's extern "system" { //Retrieves a handle to the top-level window whose class name and window name match the specified strings DOCS.MICROSOFT WINDOWS APIS fn FindWindowW(lpClassName: *mut u16, lpWindowName: *mut u16) -> usize; //Retrieves a handle to the foreground window (the window with which the user is currently working) fn GetForegroundWindow() -> usize; //Synthesizes keystrokes, mouse motions, and button clicks. fn SendInput(cInputs: u32, pInputs: *mut Input, cbSize: i32) -> u32; } //https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-input //Differents types of inputs for the 'typ' field on 'Input' #[repr(C)] #[derive(Clone,Copy)] enum InputType { Mouse = 0, Keyboard = 1, Hardware = 2, } //https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-keybdinput #[repr(C)] #[derive(Clone,Copy)] struct KeyboardInput { vk: u16, scan_code: u16, flags: u32, time: u32, extra_info: usize, } //https://docs.microsoft.com/en-us/windows/desktop/api/winuser/ns-winuser-mouseinput #[repr(C)] #[derive(Clone,Copy)] struct MouseInput { dx: i32, dy: i32, mouse_data: u32, flags: u32, time: u32, extra_info: usize, } //https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-hardwareinput #[repr(C)] #[derive(Clone,Copy)] struct HardwareInput { msg: u32, lparam: u16, hparam: u16, } //https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-input #[repr(C)] #[derive(Clone,Copy)] union InputUnion { mouse: MouseInput, keyboard: KeyboardInput, hardware: HardwareInput, } //https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-input #[repr(C)] #[derive(Clone,Copy)] struct Input { typ: InputType, union: InputUnion, } //Devuelve un array con los valores que se le introduce. //Convert a Rust UTF-8 'string' into a NUL-terminated UTF-16 vector fn str_to_utf16(string: &str) -> Vec<u16>{ let mut ret: Vec<u16> = string.encode_utf16().collect(); print!("{:x?}\n",ret); ret.push(0); ret } //an active handle to a window struct Window { // Handle to the window which we have opened hwnd: usize, } impl Window { //Find a window with 'title' and return a new 'Window' object fn attach(title: &str) -> io::Result<Self> { //convert the title to UTF-16 let mut title = str_to_utf16(title); //finds the window with 'title' let ret = unsafe { //DOCS.MICROSOFT If lpClassName is NULL, it finds any window whose title matches the lpWindowName parameter. FindWindowW(std::ptr::null_mut(), title.as_mut_ptr()) //First argument Creates a null mutable raw pointer. Second arg name of Calculadora tittle }; if ret != 0 { //Successfully got a handle to the window return Ok(Window { hwnd: ret, }); } else { //FindWindow() failed, return out the corresponding error Err(Error::last_os_error()) } } fn keystream(&self, inputs: &[KeyboardInput]) -> io::Result<()> { //Generate an array to pass directly to 'SendInput()' let mut win_inputs = Vec::new(); //Create inputs based on each keyboard input for &input in inputs.iter() { win_inputs.push(Input { typ : InputType::Keyboard, union: InputUnion { keyboard: input } }); } let res = unsafe { SendInput( win_inputs.len().try_into().unwrap(), win_inputs.as_mut_ptr(), std::mem::size_of::<Input>().try_into().unwrap()) }; if (res as usize) != inputs.len() { Err(Error::last_os_error()) } else { Ok(()) } } fn press(&self,key: u16) -> io::Result<()> { self.keystream(&[ KeyboardInput{ vk: key, scan_code: 0, flags:0, time:0, extra_info:0, }, KeyboardInput{ vk: key, scan_code: 0, flags:KEYEVENTF_KEYUP, time:0, extra_info:0, }, ]) } } //https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-keybdinput const KEYEVENTF_KEYUP: u32 = 0x0002; fn main() -> io::Result<()> { let window = Window::attach("Crackme Sencillito")?; print!("Opened handle!\n"); loop { //Filter out sending inputs when we're not looking at 'window' if unsafe { GetForegroundWindow() } != window.hwnd { //std::thread::sleep_ms(10); continue; } // Solved window.press(0xd as u16)?; //Press ENTER virtual key //https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes window.press(0x39 as u16)?; window.press(0x37 as u16)?; window.press(0x35 as u16)?; window.press(0x33 as u16)?; window.press(0x31 as u16)?; break; } Ok(()) }
Tercer reto «MFC DURO»
Como no podía ser de otra manera, el último tenía que ser duro y más difícil que los dos anteriores. Tenemos nuevamente una app escritorio, esta vez haciendo uso de MFC (Microsoft Foundation Class). Los binarios de MFC se dividen en 5 tipos: Aplicaciones Windows estándar, cuadros de diálogos, aplicaciones basadas en formularios etc…
Recon
Mi idea es resolver este reto sin tener ni idea de MFC, realizar ingeniería inversa, sin conocimiento del desarrollo de aplicaciones MFC en C++.
Como no puede ser de otra manera, abrimos la app e interactuamos con ella para saber como funciona a nivel de usuario. Cuando introducimos 8 caracteres la app se cierra. Existe un label que quizás sea donde se muestre el output de «chico bueno» o «chico malo».
Hacemos lo típico localizando alguna string de algo que sea bueno o malo para saber la password. No hay nada. Así de primeras no tengo ni idea de donde esta mi input usando un debugger. Usaremos Cheat Engine para ello. Configuramos para que realice un primer escaneo para localizar en memoria en hexadecimal 0x41, siendo en ASCII «A». Introducimos otro carácter, esta vez «B» y «next scan». Localizamos los dos chars, separados por NULL bytes.
En la dirección de memoria le damos a botón derecho y encontrar las veces que se accede a esa dirección. De manera dinámica lo mejor que podemos hacer es introducir más caracteres y ver donde escribe y que instrucciones en ensamblador hacen que en algún registro contenga el input y luego se mueva a una dirección de memoria.
Vemos más información, y tenemos la instrucción responsable de mover el contenido del registro RDX, al registro R11, siendo RDX la dirección de memoria que previamente vimos.
En el desensamblador parece ser una instrucción de la función memcpy de la DLL msvcrt. Creo bajo mi punto de vista, y sin tener ni idea de MFC, que deberíamos de saber el «stacktrace», y así poner un breakpoint en esa instrucción del memcpy, e introducir un nuevo carácter como input. De esta manera podemos obtener como información que API de Windows es la responsable de interactuar con nuestro input, porque recordamos que de momento vamos a ciegas.
Una vez después de varias pasadas, y cuando nuestro nuevo carácter esta en el cuadro de dialogo, localizamos en el «stacktrace» dos API’s interesantes.
Para corroborar dicha información sobre el uso de SendMessageW y GetWindowTextW, usaremos la herramienta Microsoft Spy++. Hacemos click a encontrar ventanas y arrastramos el prismático para enfocar al cuadro de texto donde introducimos el input. Nos localiza el Handle y seguidamente hacemos click en mostrar mensajes. Ahora que ya tenemos el handle, introducimos algun caracter y paramos el semaforo para localizar que mensaje de Windows es el encargado del input. Incluso si le damos al botón check repetidamente en la aplicacion objetivo usa siempre el mensaje WM_GETTEXT.
Este mensaje de Windows copia el texto que corresponde a una ventana a un buffer proporcionado cuando se llama. El parámetro que recibe son dos. wParam, siendo el máximo números de caracteres que van a ser copiados, incluyendo el carácter NULL de finalización. El otro parámetro es lParam, el puntero del buffer que recibirá el texto como contenido.
Bueno con esta información nos vamos acercando. Ahora con una rápida búsqueda en San Google, veremos cuando se usa o con que API interactúa con este mensaje de Windows, cuya constante es 0xD al definirse en el código fuente. En apenas 10s, localizamos un foro cuyo hilo o tema se denomina «Handle the WM_GETTEXT».
¿Finalmente lo tenemos? GetWindowText es nuestra API objetivo, constatado haciendo uso de dos aplicaciones diferentes, y buscando en Google. Pondremos un breakpoint en la API antes mencionada. Finalizado el «recon», abrimos el debugger x64dbg.
Resolución
Buscamos en todos los símbolos, y ponemos un breakpoint en la API.
Es simplemente genial, cuando introducimos el carácter «a», se para en el breakpoint, no íbamos tan mal encaminados.
Ejecutamos hasta código de usuario. Finalmente entre varias ejecuciones terminamos en el código propio de la aplicación. Por fin buenas noticias, como analistas, identificamos algo valioso «unsigned_int65 good1». Tiene como nombre «Good», por tanto si es good es en teoría el «mensaje» o el output deseado una vez introducimos la password. Esta era la String en una primera instacia queriamos obtener de manera sencilla y rápida, y para nada lo ha sido.
Ahora toca resolver y obtener el password de la aplicación, además aun no hemos identificado el input usando el debugger x64dbg. Únicamente hemos obtenido la parte de código de la aplicación donde tenemos que terminar. Como ya sabemos que parte de código nos interesa, podemos quitar el breakpoint en la API. ¿Pero que tenemos que conseguir? Comencemos haciendo reversing del código ensamblador. El salto condicional JNE, salta si no es igual o no es cero, entonces tenemos que conseguir que int_flag sea igual a 8. Ese 8, ¿es posible que sea la longitud de los caracteres de la password? Tiene pinta, porque solo esta en una parte del código que resulta ser «testeado» (instrucción TEST EAX, EAX) si el input que se introduce es correcto. Pero esto es una hipótesis, hay que comprobarlo.
Ponemos de input «d». En la instrucción LEA de la imagen anterior, en el registro RDX contiene el carácter «a». Al llamar a la función &ordinal#2903, no introdujimos el carácter correcto porque al hacer la instrucción TEST EAX, EAX, no es 0, y por tanto no hace el salto condicional y no realiza el incremento con la instrucción INC de int_flag. ¿Será el primer carácter «a»? Probemos. Correcto es «a», y se hizo el incremento. Entonces la hipótesis era valida, tenemos que tener que int_flag sea igual a 8, para que así imprima el output Good y de tal forma también sabemos que la longitud de la password es 8.
Desgraciadamente para el segundo input va a otro sitio ya que ahora int_flag no es igual a 0. Se me olvido mencionar que tenemos 0xA oportunidades para que no se cierre el programa, ya que cuando int_flag2 sea igual a 0xA, el programa hace exit.
Ponemos «aa», pero inspeccionando el registro RDX, en su dirección de memoria tiene como contenido «a.j», entonces «j» será el segundo carácter.
Efectivamente, pues ya se puede resolver. La password es: ajoooooo