Protecciones contra debuggers/desensambladores
Sea cual sea la opción elegida de las dos mostradas, se suelen implementar distintas medidas contra el desensamblado del código o contra debuggers.
Protección de packers:A día de hoy, los softwares para empaquetar aplicaciones están muy avanzados y no hace falta decir que existen empresas que dedican su tiempo en seguir investigando estos métodos. De este modo, y cada vez más a menudo, los nuevos packers que aparecen son obras de arte muy difíciles de romper y seguramente toda protección que se te ocurra ya ha sido investigada hasta la saciedad.
Sin embargo, cada protección, sea hecha por el usuario o sea utilizando software especializado es un mundo diferente y cada una tiene su complejidad.
Por otro lado, como los software de protección han avanzado tanto, pues también los que depuran las aplicaciones han avanzado tanto o más camino; conocen la mayoría de protecciones utilizadas y cada vez aparecen más herramientas de ayuda que hacen la depuración de programas una tarea muy sencilla. No hace falta decir que hay personas con unos conocimientos muy elevados y no solamente en el ámbito donde se mueven generalmente los software (ring3) sino a niveles donde trabaja el Sistema Operativo o incluso ¡por debajo de él!.
Desde hace muchos años se vienen estudiando formas para que sea más complicado acceder al código y poder utilizar dicha información; sin ir más lejos recuerdo perfectamente juegos de aventuras conversacionales de mi antiguo ZX Spectrum a 128K donde se encriptaban las cadenas de texto para que con un debugger no fuera tan sencillo encontrar la "palabra clave".
Actualmente, según mi opinión y mi experiencia, los software para empaquetar aplicaciones no se preocupan tanto en evitar que se pueda desensamblar/debuggear el código, sino lo que hacen es:
-Uso de
Máquinas Virtuales para la creación del código
-Creación de
zonas dinámicas de memoria donde se ejecutará parte de código.
-
Código ofuscado: con mucho código inútil de más para evitar que se entienda el desensamblado. Esto hará aumentar de tamaño tu aplicación.
-
Emulan la funciones y por consiguiente las llamadas a las mismas, por ejemplo si en nuestro código hemos hecho una llamada a MessageBox, pues la API MessageBox será totalmente emulada y posiblemente ofuscada.
-El mismo packer ejecuta las primeras instrucciones de tu programa y devuelve el control al mismo después, por lo tanto, conocer todo ese código que ya se ha ejecutado es un problema grandísimo. Si son sólo unos bytes los que han desaparecido se denomina
Stolen Bytes, si por el contrario es gran parte de código se denomina
Stolen Code.
-Suele ser muy fácil llegar a la comprobación de Nombre-Serial, pero han creado tantísimo código y tal complejidad a la hora de analizar un nombre-serial válido que es dificilísimo encontrarlo y por tal motivo se prefiere reparar el programa.
-Y algunas cosas más que iré añadiendo...
El lector dirá, y ¿qué consiguen con todo eso? Pues está muy claro:
1º- Hacen tan difícil encontrar un nombre-serial que el cracker seguramente desestimará esta opción porque requiere muchas horas y días de análisis.
2º-Imaginemos que al cabo solamente de 2 minutos encontramos un salto (que en ensamblador puede ser "je") que salta cuando estamos registrados pero que no salta cuando no lo estamos. Bien, sabiendo esto podríamos modificar el salto y se puede pensar que el asunto está arreglado... Aquí está la dificultad, que como la aplicación está empacada tú no puedes modificar ese "je" por un "jne" porque ese "je" todavía no existe y probablemente sea ejecutado en una memoria dinámica (que con cada ejecución varía) que el packer crea a la hora de descomprimirse.
3º-Al emular la funciones (
IAT - Tabla de Importaciones), es muy difícil saber cuáles son y por lo tanto al intentar reconstruirlo puede resultar extremadamente complicado.
4º-El uso de Máquinas Virtuales hace muy difícil saber el camino que el packer está tomando.
5º-Como cada packer es un mundo diferente pues también sus protecciones.
En resumen de todo esto:
Para descomprimir un programa empacado, es necesario llegar y pararse en el punto de entrada del programa original (
OEP), arreglar la
IAT, reparar los
Stolen Code o
Stolen Bytes, el código que se ejecuta fuera de la memoria de la aplicación introducirlo dentro etc... Se trata de quitar el packer de la aplicación y dejar solamente esta última.
Es decir, un trabajo muy laborioso pero que en determinados casos como es un trabajo repetitivo con la práctica lo puedes resolver en unas cuantas horas.
Seguro que después de haber metido toda esta parrafada algunos no saben ni de lo que he hablado, bueno solamente hay que quedarse con lo más importante y si de verdad quieres indagar en el tema releer todo lo que he escrito o preguntar en el foro. Todo lo comentado suelen ser las protecciones habituales de packers comerciales. Y... a nivel de implementar uno mismo su protección ¿cómo se puede evitar el desensamblado/debugger?
Proteger uno mismo su aplicación:Después de lo que se ha comentado, alguno pensará... pero si está todo estudiado ¿qué protección puedo yo poner a mi aplicación?
Aquí viene la invención de cada persona. Hay gente que tiene muy buenas e innovadoras ideas.
Siempre se han utilizado técnicas muy conocidas por todo el mundo como pueden ser:
-Uso de la API
IsDebuggerPresent que se encuentra en la librería kernell32.dll. Es la más conocida por todo el mundo, simplemente se llama a esta API y si el resultado de la misma es 1 hay debugger, si es 0 no hay debugger.
-Retardo en la ejecución de código: es decir, si por ej. en cargar tu aplicación tiene que pasar solamente 1 segundo y han pasado 45 segundos --> ¡hay debugger!. Esto se suele hacer de varias formas: usando la función
GetTickCount o usando funciones que te devuelven la fecha actual, incluídos segundos:
GetLocalTime,
GetSystemTime. Seguro que tú conoces muchas más formas. Te puedo indicar una muy curiosa que yo he usado de prueba y funciona perfectamente y es en ensamblador con la instrucción
RDTSC. Esta última instrucción es interesante ya que si se ejecuta dos veces una detrás de otra, verás que el resultado de ambas es prácticamente igual, sin embargo, si ha pasado determinado tiempo entre la ejecución de la primera
RDTSC y la segunda los valores son totalmente diferentes.
-En Window, el registro FS apunta hacia una estructura llamada
Thread Information Block (TIB). Esta compleja estructura tiene otras y más complejas estructuras y en ella podemos tener información de si un Debugger está depurando la aplicación o no.
Ejemplos:
GlobalFlags sería en ensamblador implementado de la siguiente forma:
mov eax, dword ptr fs:[30h]
mov eax, dword ptr ds:[eax+68h]Si el resultado es cero --> no hay debugger
Si el resultado es distinto de cero --> hay debugger
Del mismo modo hay otras formas similares:
ProcessHeap Flag,
PEB_LDR_DATA etc...
-Podemos hacer uso de distintas API:
ZwQueryInformationProcess,
ZwSetInformation Thread,
ZwQuerySystemInformation... Hay cosas muy curiosas incluso utilizando la función
CsrGetProcessId que se encuentra ni más ni menos que en
ntdll.dll. De este último ejemplo tengo uno hecho en ASM por si alguien quiere analizarlo.
-Podemos también investigar si se está usando algún debugger examinando los procesos que se están ejecutando. Esto se suele hacer de dos formas:
*Utilizando la API
CreateToolhelp32Snapshot desde el primer proceso
Module32First y después yendo uno por uno con
Process32Next.
*Utilizando
EnumProcesses que devuelve un array con el PID de cada uno de los procesos. Hay que observar que podemos también ver los procesos que cada proceso está ejecutando.
-Podemos buscar por el nombre o clase de ventana. Para esto utilizamos la API
FindWindow.
-Evitar el uso de
Breakpoints. Los Breakpoints, utilizados por un debugger, son lugares en donde el debugger para porque el usuario ha modificado una instrucción por INT3 (interrupción). Esto es una ayuda considerable, ya que se pueden poner muchos BP (breakpoints) en lugares adecuados para el análisis del código. En tu código si programas en ASM, puedes comprobar en sitios determinados, si se ha modificado la instrucción por un INT3. También es posible llamar a la API VirtualProtect y modificar las propiedades de determinada sección. Los BP serán borrados.
-Detección de
Hardware Breakpoints. Un debugger puede poner infinidad de BP, pero solamente puede poner 4 HBP (Hardware Breakpoints). Son muy útiles, más cuando los BP normales fallan. Muchos packers lo que hacen es detectar estos HBP para saber si hay debugger o no. Para esto hay que entender antes las SEH o manejo estructurado de excepciones. De este tema tan interesante, hice un tutorial bastante completo para entender las SEH:
http://foro.elhacker.net/index.php/topic,173855.msg823053.htmlDespués de que hayas leído ese tute, ya puedo seguir con la explicación de cómo se detectan los HBP. Un packer normalmente hace lo siguiente:
*Crea una excepción cualquiera
*Se ejecuta el manejador de excepciones que él mismo tiene preparado
*Desde el manejador de excepciones puede acceder directamente a los HBP.
*Si hay HBP --> hay debugger
-Bugs. Este tema es obvio. Al igual que hay bugs en grandes proyectos, pues también hay bugs en herramientas para la depuración de programas. Determinados programas hacen uso de estos bugs para que el programa crashee.
Sin ir más lejos, OllyDBG 1.10 tiene un bug archiconocido por todo el mundo que es el siguiente: Utilizar la API
OutputDebugStringA y pasarle como parámetro una cadena "%s....." de tamaño igual a 100, es decir, cien %s. OllyDBG 1.10 no puede continuar. Como véis hay gente que también estudia todas estas cosas.
Bueno, estas son las más conocidas técnicas antidebugging. Realmente existen muchísimas, que seguro ni yo conoceré, pero para entender un poco todo este misterio con las anteriores sobra.
Estas técnicas son las más habituales y que todo el mundo más o menos conoce. Entonces, ¿no es bueno ponerlas en nuestra aplicación? ¿van a ser siempre detectadas? En el siguiente apartado lo veremos...