En pocas palabras, un día me aburrí y le pedí a mi amigo, Maximus Hackerman, que me diera algo que hacer. Inmediatamente, envió a través de un ejecutable simple llamado "Reverseme.exe" (enlace Virustotal) con un simple comentario de "Imprima el mensaje oculto en su propia aplicación". Naturalmente, los archivos de encabezado CPP no fueron proporcionados. Al ejecutar el ejecutable, simplemente se abre como una aplicación basada en la consola, imprime "envió todos los paquetes mágicos, saliendo pronto, ¡Beep Boop!" y luego casi inmediatamente sale; Supongo que "pronto" es subjetivo.
Afortunadamente, no parece haber anti-fondos, así que supongo que Hackerman se sentía bien en el día.
Después de adjuntar WindBG y un desmontador, podemos ver que la aplicación lanza, lo que inicialmente parece ser, una divide by zero excepción.
Sin embargo, en una inspección más cercana, encontramos que esta excepción se lanza justo antes de que la aplicación imprima su único mensaje visible. Es importante tener en cuenta que dos vehículos (manejadores de excepción vectores) se registran ligeramente antes de este punto, por lo que es probable que esta excepción sea intencional y se use para destrozar el flujo de control. ¡Truco barato realmente para intentar tirarnos realmente!
Olvidando los vehículos por el momento, es seguro asumir que si la aplicación está "enviando" paquetes, entonces está usando la función de envío Winsock para hacerlo (aunque, la aplicación claramente dice que son paquetes "mágicos", así que ya veremos).
Como se esperaba, resulta que no puede enviar paquetes por magia, por lo que la función send de winsock se importa.
Al colocar un punto de interrupción en la función de envío, podemos echar un vistazo y ver si hay algún datos relevantes que podamos abstraer al momento de enviar. Lamentablemente, cuando el búfer se carga en la función, los datos ya están encriptados. Sin embargo, podemos determinar que el búfer siempre tiene 3 bytes de longitud, el enchufe siempre está vinculado a un valor codificado de 0x69 ( agradable ), y que el punto de interrupción de la función de envío se golpea 14 veces en total.
Hay algunas maneras de moverse dicho cifrado. Una es revertirlo por completo, lo que podría ser mucho esfuerzo, otro es localizar los datos deseados antes del cifrado y abstraerlo antes de que se encripte. Este último es significativamente más fácil que el primero, por lo que vamos con eso; Sin embargo, no dude en revertir el cifrado, he tenido un breve aspecto y no es horrible. Alternativamente, si se siente elegante, siempre puede sobrescribir el valor del mango del socket y tener un acuerdo de aplicación de recepción con él.
Lamentablemente, no hay cadenas útiles en el segmento de datos de solo lectura, aparte del mensaje inicial, ¡así que no hay consejos útiles de ese aspecto!
Volviendo a los vehículos, podemos ver que ambos manejadores registrados se utilizan para copiar algunos objetos desconocidos en un búfer de memoria asignado. Esto parece hacerse utilizando MEMCPY, utilizando una función como el segundo parámetro, que a su vez usa un entero codificado como su segundo parámetro. Vale la pena señalar que estos valores codificados no exceden 14 en ningún momento. Solo he incluido uno de los vehículos en la captura de pantalla, ya que son básicamente idénticos en cuanto a código, excepto los valores codificados.
Al romper el punto de ruptura en una de las funciones de MemCpy e inspeccionar la subfunción en el segundo parámetro, podemos ver que el número entero codificado (13/0DH en el ejemplo a continuación) se establece en el primer byte del parámetro A1, y A1+1 contiene un carácter, justo después de que el puntero se deraference.
Si verificamos algunas de las otras 14 llamadas a esta función, podemos encontrar el mismo comportamiento repitiendo; Al organizar los caracteres del 1 al 14, utilizando el número correspondiente como un indicador de orden, podemos ver que comienzan a deletrear algunas palabras legibles. Ahora, podríamos ser súper flojos y simplemente hacer una aplicación de consola para imprimir el mensaje, una vez que sabemos qué es, pero eso realmente se siente como hacer trampa. Además, el mensaje puede cambiar en algún momento. Entonces, escribamos una cueva de código para interceptar los valores antes de estar encriptados.
Lo siento, ¡pero estamos usando C ++ para esto! Si prefiere usar C#, no dude en pasar por todo el proceso de Invok de la plataforma para 9.7 millones de funciones y regrese aquí cuando haya terminado. De todos modos, primero tenemos que decidir dónde codificar la cueva. ¡Afortunadamente, ya lo sabemos! Al analizar la función anterior, aunque brevemente, sabemos que por el punto resaltado RDX contiene el índice, y RDX+1 contiene el carácter correspondiente. A continuación se muestra el código de ensamblaje para la función discutida.
Ahora, lógicamente hablando, el mejor lugar para dar el salto sería en mov [rsp+arg_8], rdx , ya que realmente no nos importa el tercer parámetro, pero queremos interceptar el registro RDX y RDX+1 . Para hacer esto, los niños, vamos a necesitar algunos bytes: 10 bytes para la instrucción MOV , para mover la dirección de nuestra cueva de código a un registro (usaremos RAX , más sobre esto más tarde durante las noticias de las 10 en punto) y 2 bytes para la instrucción JMP , para saltar al registro. Para aquellos de ustedes que tienen TEPT de más matemáticas, eso totaliza en 12 bytes. Ahora, antes de ir a toda tía Bessie y espaguettify ese código con WriteProcessMemory, debemos considerar que no hay un lugar ideal para reemplazar 12 bytes en el ensamblaje anterior. Si queremos saltar desde el desplazamiento 0x2905 ( mov [rsp+arg_8], rdx ), y necesitamos 12 bytes para hacerlo, entonces eso nos lleva a compensar 0x2917 , que está golpeando entre dos instrucciones MOV . Desafortunadamente, si simplemente escribiéramos nuestros bytes allí, destrozaría por completo la asamblea, y probablemente causaría algunos efectos secundarios "interesantes". Como resultado, será más fácil (tal vez un poco más hacky, lo siento, no lo siento) agregar algunas instrucciones de un byte para acolchar y volver al final de una instrucción. Bienvenido a bordo, 0x90.
De todos modos, ahora que sabemos cuál es nuestro plan, así que escribamos algún código: ¡la música intensa del hacker hombre !
A continuación se muestra cómo se verá el salto inicial a nuestra cueva de código una vez que los bytes se escriban en el ensamblaje de la aplicación, completo con su propia diapositiva NOP.
Sin embargo, antes de escribir y reemplazar cualquier ensamblaje, debemos iniciar la aplicación en un estado suspendido; Esto detendrá el tiempo de ejecución de la aplicación en una etapa temprana, de modo que se puedan realizar cambios de memoria antes de que la aplicación llegue a la instrucción que nos interesa. El extracto de código a continuación muestra este proceso, y no lo superaré demasiado, ya que puede verla en los archivos fuente de este repositorio, y en su mayoría habla por sí mismo.
Ahora que el proceso está generado, necesitaremos adquirir la dirección base del proceso. Normalmente, puede usar EnumProcessModules para esto, pero como suspendimos inmediatamente el hilo del proceso principal, el PEB no contiene una estructura PEB_LDR_DATA completamente poblada, específicamente la InMemoryOrderModuleList , por lo que actualmente no podemos obtener la dirección base. Por cierto, esto no parece estar documentado en ninguna parte de MSDN. Afortunadamente, esto es relativamente fácil de eludir; Al reanudar muy rápidamente el proceso, consultar los módulos y luego resuspender el proceso que podemos obtener la información que necesitamos sin que el proceso progrese demasiado. Como con la mayoría de las cosas en Windows, a Microsoft le gusta reiterar que su sistema operativo es el superior al no documentar las funciones que necesitamos: NtSuspendProcess y NtResumeProcess . Convenientemente, mi papá es amigo de Bill Gates, y me dice que estas funciones viven junto en ntdll.dll , por lo que podemos obtenerlas usando la clase a continuación que hice anteriormente:
Ahora que tenemos las dos funciones que necesitamos, podemos reanudar el proceso, consultar los módulos y resuspender el proceso:
Tal vez se pregunte, ¿por qué el bucle While está esperando dos descubrimientos de módulos en lugar de uno? Bueno, Microsoft está buscando anotar un hattrick con una albóndiga en la parte posterior de la red de espagueti indocumentado al no mencionar que el primer módulo encontrado por EnumProcessModules será ntdll.dll, y el segundo será el ejecutable. Si bien esto suena razonable, una vez que se encuentra el ejecutable, intercambiará índices con ntdll.dll. Aquí hay un ejemplo:
El resultado después de consultar solo el primer módulo:
El resultado después de consultar dos módulos:
Antes de continuar, necesitamos escribir el código de ensamblaje que realiza el salto inicial a la cueva del código y la cueva del código en sí. Esencialmente, sobrescribiremos algo de memoria a partir del desplazamiento 0x2905 , saltaremos, dará un espionaje con espada de código de código y luego volveremos a 0x2911 para continuar el flujo normal del programa. Inicialmente, declaramos el movimiento de la dirección al registro RAX como MOV RAX, 0x0 , ya que la dirección a la que saltaremos es dinámica y aún no sabemos qué es. RAX es un registro seguro para usar, ya que es un registro volátil y se sobrescribe poco después de todos modos. Dato curioso, así es como Nintendo programó el juego de plataformas original de Mario Bros, con muchos saltos (dime que soy divertido)! A continuación se muestra cómo se ve el código en el compilador; Se puede crear de otras maneras, pero he elegido disimular las instrucciones de ensamblaje requeridas en Bytecode. Si está buscando hacer esto en casa, use este sitio web.
El código para la cueva del código real es un poco más complejo, y la lógica para él también se comenta en este archivo, pero aquí está el proceso aproximado:
R10RDX , que contiene el índice de paquetes, a R11BRDX , que contiene el personaje, a R11B+12 ) a R10 (que es 0x2911 )R10 También necesitamos reescribir el código de ensamblaje que sobrescribimos (con la diapositiva NOP) en nuestra cueva de código para preservar la pila, etc.; Este código se hace referencia en la región de " Predetermined Assembly ", excluyendo el NOP. A continuación se muestra la cueva del código en su fea gloria:Parte difícil, sobre todo.
En este punto, esencialmente tenemos todo lo que necesitamos para desviar el mensaje oculto fuera de la memoria, solo necesitamos implementarlo. Para recapitular, tenemos un mango de un proceso suspendido que generamos matrices de 2 bytes que representan la lógica de ensamblaje, la dirección base del proceso suspendido, almacenada en modules[0] y la compensación de dónde necesitamos escribir la lógica de salto. El fragmento de código a continuación crea la dirección desde la que saltará, la dirección de la cueva del código (para saltar), la dirección de nuestro almacenamiento de memoria de 3 bytes, escribe las direcciones en el código de ensamblaje y luego escribe el código de ensamblaje el proceso suspendido, antes de reanudarlo:
La fundición mágica de estilo Harry Potter para codeCaveStorageAddr es convertir la dirección en bytes, y los valores codificados en los bucles son para las ubicaciones de direcciones dinámicas en las matrices de ensamblaje. Por supuesto, esto se puede hacer de una manera mucho más limpia (no escriba niños de números mágicos), solo soy vago después de tener que escribir manualmente todos esos bytes. Los últimos bits para escribir son los bucles para leer, utilizando ReadProcessMemory, la memoria, almacenar los caracteres e índices en una matriz de bytes ordenada, indicar el byte, utilizando WriteProcessMemory , cuando se termina la lectura de la memoria actual e imprima el mensaje oculto. Sabemos que la función de envío solo ocurre 14 veces, por lo que salimos del bucle while una vez que se llena la matriz de bytes; Esto podría modificarse con más ediciones de memoria para señalar a nuestra aplicación que el proceso está "saliendo" y que nuestro bucle se puede detener, en lugar de usar un valor codificado de 14, pero esto funciona para este ejemplo.
El resultado? ¡Nuestro mensaje oculto se imprime en nuestra propia aplicación de consola! Si alguien tiene curiosidad, NPT es una referencia a otra pieza de software máxima que el hacker-lo que sea que haya llamado-him escribió.