xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



Exploit Development

Fundamentals


Antes de iniciar es necesario aprender algunos conceptos más de explotación en modo kernel, además se llevará a cabo la configuración de un entorno que se utilizará en explotaciones posteriores en el kernel de windows, además como demostración para próximas explotaciones donde se omita la instalación de otros drivers se instalará un driver cargándolo como servicio usando sc.exe en lugar de instalar la aplicación.


Virtual Memory


Iniciemos por lo básico: la memoria, aunque la arquitectura de los sistemas actuales son de 64 bits a la hora de direccionar memoria virtual solo usa 48 bits, los últimos 16 bits estarán apagados en modo de usuario haciendo que las direcciones inicien por 0x0000 y encendidos en modo de kernel haciendo que inicien por 0xffff dejando en el medio un espacio de casi 16 exabytes sin utilizar, ya que siempre los primeros 2 bytes son iguales es fácil distinguir direcciones de modo user y kernel.


Drivers & Rings


Sabemos que un driver es un software que actúa como intermediario entre los dispositivos de hardware, en windows operan en diferentes niveles de privilegio o acceso, estos son conocidos cono rings, en Windows existen 4 niveles de rings:

0 (Kernel Mode): Es el privilegio más alto en el sistema, en este modo se tiene acceso total a los recursos y se puede ejecutar cualquier instrucción.

1 y 2 (intermediate): Los rings 1 y 2 son niveles de privilegio intermedios, aunque en su mayoría solo existen teóricamente ya que windows no casi no utiliza estos niveles, requieren más privilegios que el modo usuario pero menos que kernel.

3 (User Mode): Corresponde al nivel más bajo, el código tiene acceso limitado a los recursos del sistema y para interactuar con el kernel requiere utilizar syscalls.


Integrity Levels


El control de integridad proporciona un mecanismo para controlar el acceso a objetos protegibles que evalúa el acceso antes de evaluar las comprobaciones de acceso en la lista DACL de un objeto, a los procesos se les asignan niveles de integridad que determinan sus niveles de protección, Windows define 4 niveles de integridad:

Low: Asignado a procesos que requieren un aislamiento adicional, por ejemplo navegadores web en modo protegido.

Medium: Es el nivel asignado por defecto a los procesos de usuarios estándar.

High: Usado en procesos que requieren privilegios elevados, como aquellos ejecutados por un administrador en modo elevado.

System: Es el nivél más alto, reservado para procesos críticos del sistema operativo.


Communication


La arquitectura de windows impone una separación entre el modo usuario y modo kernel, una aplicación en Ring 3 no puede acceder directamente a la memoria en Ring 0 ni ejecutar instrucciones privilegiadas, sin embargo el software necesita interactuar con el hardware y el sistema operativo para ser útil, para esto windows proporciona mecanismos que permiten transicionar de una manera controlada.

Las system calls son la interfaz fundamental entre las aplicaciones de usuario y el núcleo del sistema operativo, cuando un programa necesita realizar una operación privilegiada no llama directamente al kernel sino que pasa por varias capas:

Win32 API: Imaginemos que el programa llama a una función documentada como puede serlo CreateFile en kernel32.dll, en realidad estas funciones solo son interfaces para facilitar el desarollo y eventualmente deciende hasta ntdll.dll.

Native API: La función de Win32 que llamamos valida que los parámetros sean los correctos y entonces llama a su contraparte en la API nativa ubicada en ntdll.dll, siguiendo el ejemplo a NtCreateFile, esta es la última capa en modo de usuario.

Syacall Stub: Dentro de ntdll la función prepara los registros del procesador, coloca el SSN en el registro rax y ejecuta la instrucción que dispara la transición.

Esta transición se realiza mediante la instrucción sycall, una vez en Ring 0 el manejador de llamadas del sistema utiliza el numero en el registro rax como un índice para buscar en la SSDT la dirección de kernel que debe ejecutarse.

mov rax, 0x55 ; SSN NtCreateFile
syscall       ; Ring 0 transition


Device Drivers


Hablemos de los drivers, mientras que las syscalls nativas permiten interactuar con el kernel la comunicación con drivers permite interactuar con componentes de terceros o extensiones de kernel, se realiza principalmente a través de solicitudes de I/O.

El proceso para comunicarse con un driver inicia de la siguiente forma, desde modo de usuario usamos la API CreateFile que apunta al link simbólico que fue expuesto por el driver, un link simbólico normalmente se ve de la forma \\.\SymbolicLink.

HANDLE hDevice = CreateFileA("\\\\.\\SymbolicLink", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

if (hDevice == INVALID_HANDLE_VALUE) {
    printf("[-] Failed to get handle: 0x%x\n", GetLastError());
    exit(EXIT_FAILURE);
}

Una vez tenemos el handle del driver, la API DeviceIoControl nos permite enviar un código de control IOCTL y un buffer de entrada al driver asi como uno de salida.

DeviceIoControl(hDevice, IOCTL_CODE, data, dataSize, NULL, 0, &returned, NULL);

De forma interna el sistema empaqueta nuestra solicitud en una estructura IRP por I/O Request Packet y la envía a la función registrada por el driver para manejar IRP_MJ_DEVICE_CONTROL controla lo que pasará de acuerdo al codigo IOCTL recibido.

NTSTATUS DriverDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    ULONG IoControlCode = stack->Parameters.DeviceIoControl.IoControlCode;

    switch (IoControlCode) {
        case 0x00000001:
            Handler01(Irp);
            break;
        case 0x00000002:
            Handler02(Irp);
            break;
        default:
            break;
    }

    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}


Configuration


A diferencia de la explotación en modo usuario, para modo kernel ya que una máquina depura el kernel de la otra necesitaremos 2 máquinas virtuales:

Debugger: Esta máquina será un Windows 10 x64 por defecto que se utilizará para depurar el kernel de la máquina depurada y así desarrollar el exploit.

Debuggee: Esta máquina también será un Windows 10 x64 por defecto que se utilizará para depurar y explotar las vulnerabilidades en el driver.

Luego de instalar ambos windows deberiamos tener 2 máquinas, en este caso estan conectadas mediante un adaptador NAT y sus ips son:

• Debugger: 192.168.100.5

• Debuggee: 192.168.100.10

Iniciemos con la máquina debugger, esta de primeras tendrá instalado WinDbgX que se puede instalar desde la Microsoft Store sin problemas, como herramientas adicionales podriamos incluiir IDA Free para reversear el driver y Visual Studio para desarrollar el exploit, sin embargo lo que mas nos interesa es el debugger.

En la máquina que se va a depurar habilitaremos el modo debug y en los ajustes haremos que se conecte al debugger por el puerto 50000 con la key 1.1.1.1.

C:\Windows\system32> bcdedit /debug on  
La operación se completó correctamente.

C:\Windows\system32> bcdedit /dbgsettings net hostip:192.168.100.5 port:50000 key:1.1.1.1
Key=1.1.1.1

C:\Windows\system32>

En la máquina debugger ejecutaremos WinDbg y en la pestaña Attach to kernel, añadimos el puerto y la key que especificamos antes en la máquina a depurar.

Finalmente solo necesitamos reiniciar la máquina a depurar y automáticamente se conectara al debugger, podemos ejecutar g para dejarlo arrancar con normalidad.

Connected to target 192.168.100.5 on port 50000 on local IP 192.168.100.5.
You can get the target MAC address by running .kdtargetmac command.
Connected to Windows 10 19041 x64 target, ptr64 TRUE
Kernel Debugger connection established.

************* Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
Symbol search path is: SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
Executable search path is: 
Windows 10 Kernel Version 19041 MP (1 procs) Free x64
Edition build lab: 19041.1.amd64fre.vb_release.191206-1406
Kernel base = 0xfffff804`32c00000 PsLoadedModuleList = 0xfffff804`3382a3e0
nt!DbgBreakPointWithStatus:
fffff804`330082b0 cc              int     3

kd> g

Para finalizar solo nos queda instalar el driver, para ello usaremos sc que creará un servicio que cargará el driver .sys, indicamos que el tipo es kernel y que queremos que inicie con el sistema, luego de crearlo iniciamos el servicio asi cargándolo.

C:\driver> sc create Custom binPath=C:\driver\custom.sys type=kernel start=system
[SC] CreateService CORRECTO

C:\driver> sc start Custom

NOMBRE_SERVICIO: Custom
        TIPO               : 1  KERNEL_DRIVER
        ESTADO             : 4  RUNNING
                                (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
        CÓD_SALIDA_WIN32   : 0  (0x0)
        CÓD_SALIDA_SERVICIO: 0  (0x0)
        PUNTO_COMPROB.     : 0x0
        INDICACIÓN_INICIO  : 0x0
        PID                : 0
        MARCAS         :

C:\driver>

Con esta configuración tendriamos listo el entorno con ambas máquinas para explotar el driver, pero eso será en el siguiente post donde lo analizaremos poco a poco.