EJEMPLOS PRÁCTICOS DE FRIDA
Frida VS Técnicas Anti-Debug en Windows (II)

En este segundo post de la serie de «EJEMPLOS PRÁCTICOS DE FRIDA: Frida vs Técnicas Anti-Debug en Windows« empezaremos a mostrar ejemplos de evasión de técnicas anti-debug. Tal y como se indicaba en el primer post, en esta entrada trataremos el primer grupo de técnicas: «Técnicas basadas en Llamadas al Sistema».

Este tipo de técnicas usan llamadas al sistema para determinar si el proceso está siendo depurado ya sea utilizando funciones que nos proporcionan esa información de forma directa, como IsDebuggerPresent, o, bien, funciones que nos proporcionan esa información de forma indirecta, como FindWindow. Un programa puede ingeniárselas de formas distintas para, mediante estas llamadas, determinar si el proceso es susceptible de estar siendo depurado. Este tipo de funciones tienen en común que pueden ser encontradas por Frida para su interceptación utilizando el método Module.findExportByName(null, exportName), facilitando de esa forma su instrumentación.

En este post trataremos técnicas basadas en las llamadas al sistema IsDebuggerPresent, NtQueryInformationProcess y CreateToolhelp32Snapshot. Cada una de ellas nos permite ver cómo usar Frida de formas distintas con el objetivo de evadir estos controles.

IsDebuggerPresent

El primer control que trataremos de evadir es el basado en la llamada al sistema IsDebuggerPresent. Tal y como indica la documentación de Microsoft, esta función no recibe parametro alguno y devuelve un valor boleano en función de si el proceso está siendo depurado: «True» indica que el proceso está siendo depurado y «False» indica lo contrario. Para ilustrar este método hemos desarrollado un código básico que permite detectar si existe un depurador utilizando esta llamada al sistema:

 

En el primer terminal, vemos cómo la aplicación, al ser ejecutada desde Visual Studio, indica que está siendo depurada. En cambio, si la aplicación es ejecutada desde un terminal directamente, indica que no está siendo depurada. Otra forma de comprobarlo, sería ejecutarlo con algún otro depurador como, por ejemplo, x64dbg. El único valor que interviene en la decisión es el valor de retorno de la función IsDebuggerPresent, por tanto, deberemos desarrollar un script de Frida que intercepte dicha llamada y modifique dicho valor, devolviendo siempre Falso (0x0). Un ejemplo de script que realiza lo descrito anteriormente es el siguiente:

  • En primer lugar, busca la dirección de la función «IsDebuggerPresent» mediante la ejección de Module.findExportByName (línea 3).
  • Una vez obtenida dicha dirección, intercepta las llamadas a esa función mediante Interceptor.attach(…) (línea 8).
  • Por último, justo antes antes de que la función finalice su ejecución (OnLeave), se parchea el valor de retorno, reemplazando dicho valor con el valor 0x0, que indica Falso (línea 13).

 

 

NtQueryInformationProcess

La segunda llamada al sistema que estudiaremos a continuación es NtQueryInformationProcess. Esta función nos permite obtener distinta información relacionada con un proceso. Es una función más compleja que la anterior, dado que nos permite seleccionar la información a consultar mediante el parámetro ProcessInformationClass, y nos devolverá dicha información mediante el parámetro ProcessInformation.

En este ejemplo se mostrará como realizar la evasión de 4 comprobaciones, cada una, utilizando un valor distinto en de ProcessInformationClass. Estos valores son: ProcessDebugPort, ProcessDebugFlags, ProcessDebugObjectHandle, ProcessBasicInformation.

  • ProcessDebugPort (0x7)
    Se utiliza esta clase para obtener el número de puerto del depurador, en caso de que este se encuentre activo. Este valor deberá ser distinto de 0 si existe un depurador activo.
  • ProcessDebugFlags (0x1F)
    Nos permite obtener otro flag que nos permite determinar si existe un depurador activo. En este caso, si el valor devuelto en ProcessInformation es 0, nos indicará que la aplicación está siendo depurada.
  • ProcessDebugObjectHandle (0x1E)
    Al indicar este valor como ProcessInformationClass, esta llamada al sistema devolverá un HANDLE si el proceso está siendo depurado.
  • ProcessBasicInformation (0x0)
    Utilizando esta clase se obtiene en el parámetro ProcessInformation la estructura PROCESS_BASIC_INFORMATION en la que se incluye, entre otra información: un puntero a la estructura PEB (offset 0x4), el PID del proceso (offset 0x16) y el PID del proceso padre (offset 0x20). Una de las técnicas anti-debug usando esta información es obtener el nombre del proceso padre y comprobar que no sea un depurador conocido.

 

Igual que en el caso anterior, hemos desarrollado una pequeña aplicación en C++ que ilustra el uso de estas técnicas. Para evadir los controles, deberemos desarrollar un script de Frida que realice las siguientes operaciones:

  • En primer lugar, busca la dirección de la función «NtQueryInformationProcess» mediante la ejección de Module.findExportByName.
  • Intercepta las llamadas a esa función mediante Interceptor.attach(…)
  • Cada vez que se llame a la función (OnEnter), el script debe:
    • Guardar el parámetro ProcessInformationClass, de forma qe nos permitirá seleccionar al salir que información debe ser modificada (línea 40).
    • Guardar el puntero a la información a retornar, es decir, el parámetro ProcessInformation (líneas 44, 48, 52 y 57).
    • Guardar, también, para cada caso, los argumentos necesarios que vayan a ser devueltos y deban ser modificados.

  • Por último, justo antes antes de que la función finalice su ejecución (OnLeave), el script puede utilizar la información guardada con anterioridad para determinar el valor a retornar en el puntero guardado en el parámetro ProcessInformation. De esta forma, si this.ProcessInformationClass tiene el valor:
    • 0x7 (ProcessDebugPort), ProcessInformation se sobrescribirá con el valor 0x0 (línea 63).
    • 0x1F (ProcessDebugFlags), ProcessInformation se sobrescribirá con el valor 0x1 (línea 68).
    • 0x1E (ProcessDebugObjectHandle), se comprobará el valor de retorno y, si es satisfactorio, se sobrescribirá ProcessInformation por 0x0. También se adecuará el valor de retorno ReturnLength. (línea 72 – 81).
    • 0x0 (ProcessBasicInformation), en este caso, se debe conocer la estructura PROCESS_BASIC_INFORMATION para sustituir el PID del padre (InheritedFromniqueProcessId, offset 0x20) por el PID de otro proceso que no sea sospechoso, por ejemplo el PID de ‘explorer.exe’.

 

 

Para obtener, mediante el script de Frida, el PID del proceso ‘explorer.exe’ se puede realizar utilizando llamadas a la API del sistema, utilizando, por ejemplo, las funciones GetShellWindow y GetWintowThreadProcessId. Se declararán como funciones dentro del script de JS, utilizando NativeFunction del API de Frida,  y, una vez declaradas, se utilizarán para obtener el PID del proceso como se muestra a continuación:

var addr_GetShellWindow = Module.findExportByName(null,’GetShellWindow’);
var addr_GetWindowThreadProcessId = Module.findExportByName(null,’GetWindowThreadProcessId’);

var fun_GetShellWindow = new NativeFunction(addr_GetShellWindow, ‘pointer’, []);
var fun_GetWindowThreadProcessId = new NativeFunction(addr_GetWindowThreadProcessId, ‘pointer’, [‘pointer’, ‘pointer’]);

var explorerWindow = fun_GetShellWindow();
var explorerPID = Memory.alloc(0x4)
fun_GetWindowThreadProcessId(explorerWindow, explorerPID);
console.log(‘[+] Explorer PID: ‘ + explorerPID.readInt());

Al ejecutar nuestra aplicación de ejemplo con el depurador de Visual Studio obtenemos el siguiente resultado:

 

Si ejecutamos esta misma aplicación con el mismo depurador, pero inyectando el script de Frida desarrollado en este apartado, el resultado es el siguiente:

 

CreateToolhelp32Snapshot

Para finalizar este posts, hablaremos de la función del API de Windows CreateToolhelp32Snapshot. En el ámbito de las ténicas anti-debug, esta función se utiliza comunmente para verificar el nombre del proceso «padre» de nuestro proceso, de forma que podamos identificar si el proceso padre tiene el nombre de un debugger conocido. No obstante, esta función nos permite realizar algunas cosas más. En primer lugar, esta función crea una imagen (snapshot) que contiene información del sistema. La información que contiene cada snapshot puede ser seleecionada miediante el primer argumento, que puede ser uno de los siguientes valores: TH32CS_INHERIT, TH32CS_SNAPALL, TH32CS_SNAPHEAPLIST, TH32CS_SNAPMODULE, TH32CS_SNAPMODULE32, TH32CS_SNAPPROCESS, TH32CS_SNAPTHREAD.

En el método anti-debug más básico, utilizaríamos TH32CS_SNAPPROCESS para obtener una lista de procesos, recorreríamos esta lista de procesos en busca de nuestro proceso, identificaríamos el PID del padre de nuestro proceso, volveríamos a recorrer la lista buscando la información del Padre y, por último, verificaríamos la información de dicho padre. Sin embargo, toda la información proporcionada por los diferentes flags (o bién usando TH32CS_SNAPALL) nos permite realizar otras comprobaciones. Algunos ejemplos son:

  • Verificación de Procesos (información obtenida TH32CS_SNAPPROCESS ):
    • Buscar procesos prohibidos (como VsDebugConsole.exe, devenv.exe, x32dbg.exe…) en toda la lista de procesos, independientemente de si están relacionados, o no, con el propio proceso.
  • Verificación de los Módulos (información obtenida TH32CS_SNAPMODULE y TH32CS_SNAPMODULE32):
    • Buscar módulos prohibidos (como frida-agent.dll…) entre los módulos que componen mi proceso.
  • Verificación de los Threads (información obtenida TH32CS_SNAPTHREAD):
    • Verificación de que todos los threads del sistema tienen un proceso asociado en la lista de procesos, para verificar procesos ocultos.
    • Verficación del número de threads de mi aplicación.
    • Verificación de que los Threads de mi proceso no hacen referencias a módulos prohibidos.

Como vemos, una aplicación, usando solo esta función, puede verificar distintos resultados obtenidos con la misma y verificar la coherencia entre ellos. Nuestra tarea, entonces, para evadirlo, será encontrar un mecanismo por el cual podamos evitar todas estas comprobaciones y mantener la coherencia entre la información obtenida. Con este objetivo debemos estudiar cómo la función devuelve esta información y cómo la podemos manipular.

CreateToolhelp32Snapshot nos devuelve un HANDLE de tipo SECTION. Si analizamos la sección de memoria a la que hace referencia este HANDLE encontramos una estructura donde se pueden identificar diferentes partes:

Snapshot Header
* Los nombres y tipos de esta sección han sido asignados por el redactor dado que no se ha encontrado documentación al respecto.
DWORD dwHeapListCount; Número de elementos que contiene la Heap List.
DWORD dwProcessListCount; Número de elementos que contiene la Process List.
DWORD dwModuleListCount; Número de elementos que contiene la Module List.
DWORD dwThreadListCount; Número de elementos que contiene la Thread List.
DWORD dwHeapListOffset; Offset (respecto a la base de la sección) donde se encuentra el inicio de la Heap List.
DWORD dwProcessListOffset; Offset (respecto a la base de la sección) donde se encuentra el inicio de la Process List.
DWORD dwModuleListOffset; Offset (respecto a la base de la sección) donde se encuentra el inicio de la Module List.
DWORD dwThreadListOffet; Offset (respecto a la base de la sección) donde se encuentra el inicio de la Thread List.
DWORD dwHeapListIndex; Indices utilizado por las funcione Heap32ListFirst y Heap32ListNext.
DWORD dwProcessListIndex; Indices utilizado por las funcione Process32First y Process32Next.
DWORD dwModuleListIndex; Indices utilizado por las funcione Module32First y Module32Next.
DWORD dwThreadListIndex; Indices utilizado por las funcione Thread32First y Thread32Next.
Heap List
Array de elementos HEAPLIST32.
Module List
Array de elementos MODULEENTRY32 (o MODULEENTRY32W).
Process List
Array de elementos PROCESSENTRY32 (o PROCESSENTRY32W).
Thread List
Array de elementos THREADENTRY32.

Utilizando las funciones NtQuerySection y NtMapViewOfSection se puede modificar la memoria asociada al handle, de forma que podemos modificar el contenido del snapshot realizado con la función CreateToolhelp32Snapshot. Siendo así, el script de Frida que se desarollará a continuación tendrá como objetivo modificar las listas obtenidas, manteniendo su coherencia, pero ocultando aquellos Procesos, Módulos y Threads que no queramos que el programa analizado sea capaz de detectar.

En primer lugar, tendremos que definir las listas de Procesos y Módulos que queramos ocultar. Para ello, nos valdrá solo con el nombre. En nuestro caso, queremos ocultar el Depurador de Visual Studio y los ejecutables y librerías relativas a Frida. Por ello definimos las siguientes listas:

  • Lista de Procesos Prohibidos: VsDebugConsole.exe, devenv.exe, frida-winjector-helper-32.exe
  • Lista de Módulos Prohibidos: frida-agent.dll

Una vez tenemos claro qué procesos y módulos queremos ocultar, deberemos realizar un hook de la función CreateToolhelp32Snapshot como ya hemos hecho en los dos métodos anteriores. En este caso, no modificaremos ni los parámetros ni el valor de retorno. Debemos interceptar el handle que devolverá la función (utilizando onLeave) y ahí empezaremos a trabajar. Como hemos dicho antes, utilizando las funciones NtQuerySection y NtMapViewOfSection, accederemos a la memoria referenciada por el handle y realizaremos las siguientes acciones.

  • Modificaciones sobre la Lista de Procesos:
    • Se eliminan de la lista aquellos procesos que tienen como nombre uno de los indicados en la lista de procesos prohibidos.
    • Para mantener la coherencia, se eliminan, también, la referencia de otros procesos (o sub-procesos) a los procesos prohibidos.
      Por ejemplo, el programa que estemos depurando tendrá como padre, probablemente, un proceso prohibido. Por tanto, se deberá cambiar este valor por un valor no sospechoso (como el PID de explorer.exe).
  • Modificaciones sobre la Lista de Módulos:
    • Se eliminan de la lista aquellos Módulos de nuestro proceso que tengan como nombre uno de los indicados en la lista de módulos prohibidos.
  • Modificaciones sobre la Lista de Threads:
    • Se deberán eliminar de la lista aquellos Threads que tienen como propietario un proceso eliminado de la Lista de Procesos.
    • Se deberán eliminar de la lista aquellos Threads de nuestro proceso que referencian a un módulo sospechoso.
      Este dato lo podemos obtener accediendo a la información del Thread, utilizando OpenThread, con permisos de acceso THREAD_QUERY_INFORMATION, y NtQueryInformationThread, solicitando ThreadQuerySetWin32StartAddress. Comparando la dirección de memoria obtenida, con la dirección de memoria de los módulos prohíbidos, podemos decidir si el Thread debe ser eliminado de la lista.