WRITE: rajkit_.pdf
TÉCNICAS AVANZADAS EN EVASIÓN DETECCIÓN DE SISTEMAS VIRTUALIZADOS y EVASIÓN DE AV/EDR´s
Alumno: RajKit
Tutor:
TABLA DE CONTENIDO:
-
INTRODUCCIÓN
-
METODOLOGÍA
-
RAJKIT MALWARE
-
REDUCCIÓN DE LA ENTROPÍA -----------------------------------------------------------------{7-10}
-
DETECCIÓN DE VIRTUALIZACIÓN -------------------------------------------------------------{11}
-
Máquina virtual ------------------------------------------------------------------------{12-15}
-
Kernel exception hooking parte 1 -------------------------------------------------{16-17}
-
VlsVenabled -----------------------------------------------------------------------------{18}
-
Ataque de canal lateral de caché --------------------------------------------------{18-20}
-
Detección de anomalías respecto a las especificaciones del fabricante –{20-23}
-
SYSCALL DIRECT CALLING-----------------------------------------------------------------------{24-27}
-
NTDLL.DLL BASE desde PEB---------------------------------------------------------{28-30}
-
Direccion de funciones desde EAT ------------------------------------------------{30-31}
-
Obtener SYSCALL ----------------------------------------------------------------------{32-34}
-
DLL HOLLOWING “MODULE OVERLOADING” ---------------------------------------------{34-36}
-
Ejecución--------------------------------------------------------------------------------{37}
-
PTE REMAPPING---------------------------------------------------------------------------------{37-38}
-
Direcciones virtuales,físicas,paginación y bits de control----------------------------------------------------------------------------------{38-43}
-
Subversión de la memoria---------------------------------------------------------{44-45}
-
Driver-----------------------------------------------------------------------------------{46}
-
Fase 1-----------------------------------------------------------------------------------{47-48}
-
Fase 2-----------------------------------------------------------------------------------{48-50}
-
Fase 3-----------------------------------------------------------------------------------{50-51}
-
KERNEL EXCEPTION HOOKING PARTE 2---------------------------------------------------{51-60}
-
CONCLUSIÓN------------------------------------------------------------------------------------{61}
-
BIBLIOGRAFIA-----------------------------------------------------------------------------------{62-63}
ENUNCIADO-INTRODUCCIÓN:
El malware no desea ser detectado ni que su actividad sea descubierta. Es una carrera contra reloj en la que los creadores de malware invierten una gran parte de su tiempo: evitar la detección o posponer todo lo posible dicha eventualidad. Por otro lado, en el bando contrario estamos quienes tenemos como tarea justo lo contrario: discernir de la forma más rápida y fiable qué es lo que hace un código cuando este es ejecutado (ya sea en forma nativa o interpretada) No es una tarea sencilla: si se hace despacio y con calma se hace bien, pero tal vez ya sea demasiado tarde. Por el contrario, con prisas, podemos decidir sobre un mayor caudal de muestras (que llegan de forma incesante), pero corremos el riesgo de crear situaciones tanto de falsos positivos como, a veces empeorando la situación, falsos negativos. En esta carrera de obstáculos, es precisamente capital las técnicas que tienen por objeto detectar cuando un binario está siendo ejecutado en una máquina virtual. En el caso que alguna de estas contra-medidas que portan los ejecutables de positivo, el malware no se ejecutará o empleará rutinas de código que no efectúan ninguna operación sospechosa o trivial para, evidentemente, volar por debajo del radar del investigador. Es fundamental tener conocimiento de estas técnicas.
Con el constante avance en la capacidad por parte del hardware, con el tiempo los sistemas operativos han sido capaces de ir introduciendo poco a poco mitigaciones muy importantes, aprovechando esa capacidad extra de cómputo que proporciona el hardware, haciendo de esa forma viable el constante monitoreo de actividades sospechosas.
Por otra parte la virtualización tanto del hardware como del kernel aplican un extra de seguridad en muchos aspectos como contramedida al malware, sin embargo siguen existiendo muchas formas de evasión con las cuales en conjunto se puede llegar a realizar actividades maliciosas en los sistemas operativos actuales.
En este punto comprendemos que la mejor forma de detección es la evasión.
METODOLOGÍA:
Para realizar la investigación sobre la detección de entornos virtualizados y la evasión de sistemas de detección por parte del malware, se decide diseñar una pieza de malware en su fase de “loader” teniendo en cuenta todos los aspectos de dicha fase.
A lo largo de la investigación nos adentraremos en profundidad mediante reversing en cada una de las fases que se proponen para crear un hipotético malware evasor de AV/EDR y entornos virtualizados, con el cual seremos capaces de ocultar y evadir los monitoreos, consiguiendo llegar al núcleo.
1. RAJKIT MALWARE
Lo primero antes de adentrarnos en el reversing de las técnicas propuestas hago hincapié en la comprensión del comportamiento del diseño presentado en la figura 0-1, es necesario tener ciertos matices en cuenta para estudiar el contenido del trabajo:
-
Todo el diseño está probado en su totalidad en Windows 10 versión 1909.
-
El propósito de este loader es proteger a todas costa el payload de cualquier tipo de detección.
-
Se realiza el estudio de unas partes concretas del diseño total del loader, marcadas en el diagrama completo de RAJKIT mediante puntos azules:
-
Evasión de la detección de shellcode encriptada en un análisis estático por parte del software de detección mediante REDUCCIÓN DE LA ENTROPÍA
-
Detección de la virtualización
-
Evasión del monitoreo de las APIS nativas mediantes SYSCALL DIRECT CALLING
-
Ocultación mediante DLL HOLLOWING y el protector del análisis forense desde el DRIVER mediante PTE REMAPPING
-
Evasión del protector KERNEL PATCH PROTECTION para evadir al guardián del kernel y de las modificaciones realizadas desde el driver en Windows.
-
Los puntos amarillos son fases que no se explican pero se hace referencia desde aqui a repositorios probados, que son necesarias para el correcto funcionamiento, en lo cuales entrarian los siguiente puntos:
-
Elevación de privilegios mediante UAC BYPASS, la técnica que propongo es la siguiente:
-
Mapeo del driver en memoria post-elevación de privilegios mediante una técnica que hace una explotación en el driver iqvw64e.sys de INTEL.
-
Las técnicas no se programan en conjunto para un mejor análisis tanto del código como la técnica en sí de las mismas.
-
La comunicación con el command&control la cual se trata, se propone una conexión con google script desde el cual se realiza la gestión de datos con el servidor c&c de esta forma evitaríamos un monitoreo por parte de los sistemas de sniffing y proxys de conexiones sospechosas, ralentizando la obtención del dominio final.
Todos los códigos que se necesitan para probar cada aspecto que se trata a lo largo de la investigación se encuentran en el repositorio propio de github, divididos en carpetas de puntos y subpuntos de la misma forma en la que está estructurado el trabajo.
Con estos datos en mente, la propuesta RajKit se ve de la siguiente manera en cada una de sus fases de carga en el sistema mediante distintas evasiones:
Figura 0-1. Diagrama de comportamiento de RajKit |
2. REDUCCIÓN DE LA ENTROPÍA
La entropía es una medida de aleatoriedad, esto es importante por que la entropía es un reflejo directo de lo que puede o debe contener un archivo dependiendo de su codificación, por lo tanto puede mostrar ilegitimidad en el contenido del mismo, generalmente el loader tiende a mantener cifrado su payload hasta el momento de su ejecución en el sistema para tratar de evadir los análisis estáticos.
Lo que ocurre es que los datos comprimidos encriptados con un buen cifrado por lo general parecen bytes aleatorios lo que hace aumentar la entropía y desencadenar la puesta en cuarentena y un evento de seguridad en el EDR.
La entropía de la información fue propuesta por primera vez por Shannon y se refiere al valor esperado de la cantidad de información, entre un valor de 0 a 8, siendo 8 el máximo de entropía que significa que los datos son más uniformes de los esperados.
En la siguientes dos gráficas presentamos el cálculo de la entropía de un portable ejecutable con su payload sin encriptar y otro con el payload encriptado con el algoritmo RC4, mostrando la entropía también por sección del PE:
Esta gráfica nos muestra la sección .data del portable ejecutable que va desde 40-50 con una entropía de 6.29456
Figura 1-1. Payload encriptado con RC4 de alta entropía |
En este caso en el que el payload lo tenemos sin encriptar nos muestra la sección .datadel portable ejecutable desde 40-50con una entropía de 5.51430:
Figura 1-2. Payload sin encriptar |
Esto se calcula en función de los datos del archivo, siendo H (x) la entropía de la información y p (x) la probabilidad que se genera:
En el caso de RajKitse propone el conjunto como un instalador normal, sin embargo el payload en forma de shellcode que contendría la comunicación con el command&control (en RajKit únicamente se ejecutará una calculadora, pero para este ejemplo usamos un shellcode que inyecta un VNC en un proceso para realizar las pruebas de entropía) si está cifrada con un algoritmo de cifrado RC4 lo que aumenta la aleatoriedad y podría hacer saltar las alarmas, para ello utilizamos un método para reducir esa entropía de la siguiente manera:
Sabemos que el software normal suele tener una entropía de entre4.8 y 7.2, sin embargo y dependiendo del tamaño inicial del payload a integrar en el loader que diseño es realmente útil integrar una reducción de este tipo para esquivar los análisis.
Figura 1-3. Reducción de la entropía mediante introducción de patrones |
Una vez terminada la reducción de entropía el payload aumenta en tamaño por los patrones introducidos, sin embargo obtenemos un reducción considerable para el tamaño inicial del shellcode nos muestra la sección .datadel portable ejecutable desde 40-60con una entropía de 4.86912:
Figura 1-3. Payload post-reducción de entropía |
3. DETECCIÓN DE VIRTUALIZACIÓN
Existen varias formas que el malware actual integra en su código tratando de detectar un sistema virtualizado sobre todo para tratar de evadir los sandbox y así evitar un análisis de comportamiento de la pieza de malware y desarrollar una contramedida lo más eficiente y rápido posible.
Muchas de estas técnicas tienen una forma simple como contra-evasión y se pueden mitigar fácilmente con una buena configuración de dicho sistema virtualizado, iremos clasificándolas en función del tipo y nos centraremos después en el estudio realizado por nosotros en los métodos que decidí implementar en nuestro “Proof of concept” y consideramos más óptimos:
Evasiones basadas en tiempo:
- Bombas de tiempo
- Uso de API´s de retraso
- Parches para dormir
Evasiones basadas en comportamientos de usuario:
- Application.RecentFiles.Count
Evasiones basadas en VM:
- Verificación de recuento de núcleos
- Comprobación de espacio en disco y memoria fisica
- Usando instrucciones específicas (CPUID)
- Información de BIOS
- Lista negra de geolocalización
Una de las técnicas que integraremos es el manejo de la ejecución de CPUID invitado, CPUID es una instrucción que provoca incondicionalmente la salida de la Virtual Machine , se utiliza por que permite que el software descubra detalles del procesador.
También se usa para vaciar la canalización de los procesadores que no admiten instrucciones como RDTSCP y puedan usar CPUID+RDTSC y CPUID como barrera.
Pero antes de entrar en profundidad en la detección de hypervisor mediante el uso de “ataques de canal lateral de caché” vamos a ver como funciona un hypervisor, sus instrucciones de salida condicional y registros de control ya que entendemos que para el correcto funcionamiento de un hypervisor realmente necesitamos saber de antemano si ya existe una virtualización del sistema, ya que la virtualización anidada no es compatible en todos los sistemas.
Durante esta lectura trataremos este tema desde un contexto de privilegios tanto de RING3 como de RING0.
3.1 Máquina Virtual
Tanto Intel como AMD admiten ambos la tecnología de virtualización en sus procesadores modernos, nosotros nos centraremos en la tecnología VT-x de Intel principalmente por que son los procesadores que más se utilizan.
Las extensiones de máquina virtual definen la compatibilidad a nivel de procesador para máquinas virtuales y se admiten dos clases principales de software:
- Monitores de máquinas virtuales (VMM)
- Software invitado
Nos centraremos en los conceptos básicos de la arquitectura de máquinas virtuales y las extensiones de máquinas virtuales (VMX) que admiten la virtualización del hardware del procesador para múltiples entornos de software, nos interesa el funcionamiento del VMX ya que el soporte para la virtualización se proporciona mediante una forma de operación del procesador denominada operación VMX, existen 2 tipos:
- Root Operation
- Non-root Operation
El comportamiento del procesador en la operación non-root de VMX está restringido y modificado para facilitar la virtualización. En lugar de su funcionamiento normal, determinadas instrucciones y eventos provocan salidas de VM a VMM.
Debido a que estas salidas de VM reemplazan el comportamiento normal, la funcionalidad del software en la operación non-root de VMX es limitada. Es esta limitación la que permite que VMM mantenga el control de los recursos del procesador.
No existe ningún bit visible en el software cuya configuración indique si un procesador lógico está en operación VMX no root.
Por lo tanto un VMM puede permitir evitar que el software invitado determine que se está ejecutando en una máquina virtual.
Pero cómo interactúa el software invitado con un VMM? se describe de la siguiente manera:
- El software ingresa a la operación VMX al ejecutar una instrucción VMXON
- El VMM puede tomar la acción adecuada a la causa de la salida de la VM y luego puede regresar a la máquina virtual mediante una entrada de VM.
- El VMM puede decidir apagarse y dejar el funcionamiento de VMX, con la instrucción VMXOFF
Figura 3-1. Interacción entre VMM Y software invitado |
Para permitir estas interacciones primero se debe comprobar si el procesador dispone de un soporte para VMX y para que el software del sistema puede determinar si un procesador admite este tipo de operaciones mediante CPUID con la siguiente consulta:
boolVMX = false; __asm XOR EAX, EAX INC EAX
CPUID BT ECX, 0x5 JC VMXTRUE VMXFALSE : JMP NON VMXTRUE : MOV VMX, 0x1 NON : NOP }
return VMX; |
En este punto comprendemos mejor el funcionamiento básico de interacción y que tenemos unas operaciones que nos permiten obtener información del procesador con operaciones de bit, de forma sigilosa pudiendo evadir hooks en API´s del sistema.
El bit VMX se encuentra en el bit 13 del registro CR4 del procesador, el cual es posible de habilitar en función de la compatibilidad que previamente hemos obtenido desde RING3, sin embargo esta acción debemos realizarla desde el driver, pero porque nuestro procesador tiene tecnología VT-x y sin embargo no soporta VMX? y si obtenemos un fallo desde el driver al intentar habilitar ese bit? esto nos llevaría a deducir que nuestro procesador ya está realizando operaciones de virtualización?
Como ya hemos dicho al principio estamos realizando todo el proyecto bajo VMWARE con un Windows 10 en su versión 1909:
Tenemos el bit 13 de CR4 en 0 según nos muestra WINDBG:
Para nuestro propósito utilizaremos un driver con una función en ensamblador que realiza la operación OR para que el bit este en 1: 0x00000000003506f8 OR 0x2000 = 0x3526f8
PUBLIC AsmEnableVmxOperation .code _text
AsmEnableVmxOperation PROC PUBLIC PUSH RAX XOR RAX, RAX MOV RAX, CR4 OR RAX,2000h MOV CR4, RAX POP RAX RET AsmEnableVmxOperation ENDP END
|
Esto nos devuelve una excepción de instrucción privilegiada c0000096al realizar la operación MOV CR4,RAX
Lo que ha ocurrido es que cuando se ejecuta una Máquina Virtual, algunas de sus instrucciones no se pueden ejecutar directamente por el procesador, principalmente porque estas instrucciones pueden interferir con el estado del MMV o del SO anfitrión, estas instrucciones se denominan instrucciones sensitivas.
Por lo tanto en este punto tendríamos una detección bastante precisa.
3.1.1 KERNEL EXCEPTION HOOKING
Esto realmente podría implementarse de una forma eficiente y aunque no lo implementaremos aquí, sí que lo analizaré por encima.
El BSOD que desencadena termina con pantallazo azul, lo desencadena la rutina KeBugCheckEx y todo empieza con el comienzo de una excepción en nuestro caso siguiendo esta secuencia(existen más rutinas durante la secuencia)
Figura 2-1-1.1. Rutinas en Secuencia de Excepción Generada |
Nos centraremos en el reversing de ntoskrnl.exea partir de KeBugCheck y KeBugCheck2que comienza deshabilitando las interrupciones, guardando el contexto de la llamada y el estado del procesador para pasar directamente el control a KeBugCheck2que finalmente pasaría a HAL.DLL
call cs:_impHalReturnToFirmware
Una vez aquí y para poder enganchar la excepción, tenemos que obtener la dirección en la que se exporta, por suerte HalPrivateDisparchTableesta en la sección .datalejos de KPPy obtendremos así la dirección de la tabla HALL_DISPATCHque es la que contiene punteros a las funciones que implementa HAL.DLL
mov rax, cs:HalPrivateDispatchTable
Una vez accedido al puntero de HalPrepareForBugCheckse realizará el hook para después obtener la dirección de retorno de KeBugCheck2, obtener el contexto de la rutina interrumpida, continuar con la ejecución y en nuestro caso volver a deshabilitar el bit VMXdel registro CR4, ya que el contexto nos lo devolverá con el bit activo.
Figura 2-1-1.2. Expection Hook |
La evasión de excepciones del kernel como punto extra, la trato en el punto 7 para salirnos del punto 2, realizó un reversing de la pila de llamadas junto con un análisis en IDA, para comprender después la técnica de Hooking ByePG.
Sin embargo esto es solo para comprender un poco el funcionamiento de la MMV y cómo afectan algunas acciones bajo la virtualización. Nuestra técnica se basa en la recopilación de información mediante CPUIDy la posterior detección de anomalías de la unidad central de procesamiento respecto a las especificaciones del fabricante, ya que esta fase de detección en RajKitse implementa con privilegios RING3.
3.1.2 VslVsmEnabled
Haciendo reversing durante el estudio de ” Exception Hooking a KPP” a KeBugCheck2me di cuenta que una de las comprobaciones que realiza Windows durante el manejo de excepciones “pre-bsod”es la cerciorarse de que no está Hyper-Vactivo mediante este bool VslVsmEnabled:
3.1.3 ATAQUE DE CANAL LATERAL DE CACHÉ
Lo primero es crear un buffer de memoria que abarque varias páginas, entonces rdstc es ejecutado especulativamente en lugar de acceso especulativo a memoria privilegiada y el resultado se utiliza para acceder a una determinada parte del buffer creado previamente.
Las páginas del buffer a las que se puede acceder durante la ejecución especulativa son limitadas, lo que permite discernir posteriormente los accesos especulativos reales de los errores aleatorios.
Tan pronto como se completa la función que contiene la ejecución especulativa, el número de página con el tiempo de acceso más bajo se agrega a las estadísticas todo el caché en la región de memoria se flushea (Cuando la cantidad de datos no escritos en el caché alcanza un cierto nivel, el controlador escribe periódicamente los datos almacenados en caché en una unidad.)
Figura 2-1-3.1. Ejecución especulativa de RDTSC y acceso de memoria basado en su retorno |
Figura 3-1-3.2. Activación de la especulación |
Para tener éxito se utilizan 10.000 ciclos para recopilar la información, se calcula el número de errores de la región esperada.
En VM con VMEXIT en rdtsc habilitado, el porcentaje de tiempo que afectan a áreas no designadas oscila entre el 50% y 90% y en sistema nos virtualizados es del 1%.
El ataque utiliza ejecución especulativa para engañar a la CPU para revelar información sobre cómo se ejecuta rdtsc.
En un entorno no virtualizado se ejecutaría rdstc en la propia CPU y la CPU simplemente devolverá el contador.
En un entorno virtualizado donde el hypervisor establece el bit “RDTSC EXIT”en IA32_VMX_PINBASED_CTLSMSR, de hecho ejecutar es un cambio de contexto, lo que llevaría demasiado tiempo.
3.2 DETECCIÓN DE ANOMALÍAS RESPECTO A LAS ESPECIFICACIONES DEL FABRICANTE
Esta técnica que implementamos se basa en la obtención de ciertos datos del fabricante que integraremos en una base de datos sqliteo tinydbencriptada dentro de la pieza de malware para posteriormente una vez ejecutado el loader en la máquina virtual obtener el modelo de procesador mediante instrucciones de bit CPUIDy por último mediante la API GetLogicalProcessorInformationobtendremos la información sobre los procesadores lógicos y hardware relacionado, accediendo a la estructura o estructuras devueltas SYSTEM_LOGICAL_PROCESSOR_INFORMATION .
Esta API que hemos elegido es importante ya que después de realizar un barrido por los diferentes EDR´s del mercado no hemos obtenido un positivo en hooksa la misma, lo que nos permitirá una libre consulta. En el capítulo 4 de este trabajo entramos más en detalle y nombramos todas las API´s monitoreadas por este tipo de software.
3.2.1 RECOPILACIÓN DE DATOS
Lo primero es ponernos en el contexto de un VMM VMware con un Windows 10 1909 y un procesador Intel Core i9-9880H , con el que se realizará la prueba.
Los datos tanto en la máquina como del fabricante que buscamos son los siguientes:
-
Número del procesador
-
CPU,LCPU
-
Capacidad total de caches L1,L2 y L3
Según Intel el procesador en cuestión, cuenta con las siguientes especificaciones dentro de los campos que precisamos:
- Nº del procesador: i9-988H
- CPU: 8
- LCPU: 16
- L1: 64KB
- L2: 256KB
- L3: 16MB
La máquina virtual con la que trabajamos tiene la siguiente arquitectura respecto a sus nucleos fisicos y lógicos que maneja el hypervisor:
Figura 3-2-1.1. Arquitectura VM de pruebas |
Sabiendo todos los datos necesarios ahora debemos obtenerlos desde la máquina virtual para poder cruzarlos y obtener las anomalías que buscamos para realizar una detección precisa, como nota es necesario comentar que esta técnica sería inviable si nuestro hypervisor trabajase con una sola máquina con todas sus CPU físicas asignadas a la misma.
Para recopilar el nombre completo del modelo de CPU debemos ejecutar CPUID con determinados valores en el registro EAX concretamente:
- CPUID EAX = 0x80000002
- CPUID EAX= 0x80000003
- CPUID EAX= 0x80000004
Esto nos devuelve un total de 16 bytes en formato little-endian en los registros EAX, EBX, ECX, EDX, de tal forma que concatenando los nos debería devolver el procesador usado por la máquina:
Figura 3-2-1.2. Nombre Procesador mediante CPUID |
Para terminar la recopilación de datos y posterior comparación respecto a los datos del fabricante nos faltaría obtener, CPU/LPCU y CACHES para ello como hemos nombrado anteriormente nos valdremos de la API GetLogicalProcessorInformation y para obtener el tamaño de las caché L1,L2 y L3 SYSTEM_LOGICAL_PROCESSOR_INFORMATIONtiene una subestructura _CACHE_DESCRIPTOR en la que contiene un campo DWORD llamado SIZE.
Si ejecutamos nuestro código obtenemos la siguiente información:
Figura 3-2-1.3. Datos de las CPU/LCPU y CACHE L1,L2,L3 |
La realidad es que nos está devolviendo 2 procesadores lógicos por núcleo, es decir estaría detectando 4 procesadores lógicos.
Con la información necesaria recopilada podemos empezar a cruzar los datos para encontrar anomalías y considerar después que estamos bajo un sistema virtualizado:
INFORMACIÓN DEL FABRICANTE |
INFORMACIÓN MÁQUINA VIRTUAL |
Nº del procesador |
i9-988H |
Nº del procesador |
i9-988H |
CPU |
8 |
CPU |
2 |
LCPU |
16 |
LCPU |
2*2=4 |
L1 |
65536 |
L1 |
32768 |
L2 |
262144 |
L2 |
262144 |
L3 |
16777216 |
L3 |
16777216 |
Podemos observar cómo de esta manera obtendremos una confirmación fiable de que el sistema en el que estamos ejecutando está bajo gestión de hardware de un hypervisor.
4. SYSCALL DIRECT CALLING
Es bien sabido por los desarrolladores de malware que los software de detección simples y EDR´s actuales como Crowdstrike, SentinelOne, Cylance, Sophos, Symantec, CarbonBlack,DeepInstick,Attivo monitorizan mediante hook´s las APIS sensibles que permiten ciertas acciones que consideran sospechosas.
Este tipo de monitorización se realiza dentro del flujo de ejecución nativo de Windows (Nt,Zw) que se encuentran en NTDLL.DLL la cual representa la última capa de abstracción antes de hacer SYSCALL y ceder el control al kernel:
Figura 4-1. Flujo de ejecución nativo sin monitorización EDR |
Podemos seguir ese flujo de ejecución de VirtualAllocExsin HOOK por EDR a través de IDA hasta llegar a su SYSCALL, empezando por kernel32.dll :
Figura 4-2. Apuntando a KERNELBASE.DLL |
Figura 4-3. Apuntando desde KERNELBASE a ZwAllocateVirtualMemory |
Figura 4-4. Registro apuntador en NTDLL llegando a SYSCALL |
Como podemos observar en la secuencia de ejecución todas las APIS serán interceptadas y darán un aviso por parte del software de detección.
Para tener mayor conocimiento sobre cuales, después de investigar e ido obteniendo las API´s que monitorean todos los EDR´s actuales del mercado y los vuelco en esta lista:
KiUserApcDispatcher LdrLoadDll NtAllocateVirtualMemory NtAlpcConnectPort NtFreeVirtualMemory NtMapViewOfSection NtProtectVirtualMemory NtQueueApcThread NtReadVirtualMemory NtSetContextThread NtUnmapViewOfSection NtWriteVirtualMemory RtlInstallFunctionTableCallback ZwAllocateVirtualMemory ZwAlpcConnectPort ZwFreeVirtualMemory ZwMapViewOfSection ZwProtectVirtualMemory ZwQueueApcThread ZwReadVirtualMemory ZwSetContextThread ZwUnmapViewOfSection ZwWriteVirtualMemory NtCreateProcess NtCreateProcessEx NtCreateThread NtCreateThreadEx NtCreateUserProcess NtQueueApcThreadEx NtSetInformationProcess ZwCreateProcess ZwCreateProcessEx ZwCreateThread ZwCreateThreadEx ZwCreateUserProcess ZwQueueApcThreadEx ZwSetInformationProcess NtLoadDriver is hooked NtMapUserPhysicalPages NtOpenProcess NtQuerySystemInformation NtOpenKey NtOpenKeyEx NtRenameKey
|
NtSetInformationFile NtSetValueKey NtTerminateThread ZwCreateFile ZwCreateKey ZwDeleteFile ZwDeleteKey ZwDeleteValueKey ZwOpenFile ZwOpenKey ZwOpenKeyEx ZwRenameKey ZwSetInformationFile ZwSetValueKey ZwTerminateThread NtAlertResumeThread NtGetContextThread RtlCreateUserThread ZwAlertResumeThread ZwGetContextThread NtQuerySystemInformationEx NtResumeThread NtSetInformationThread NtTerminateProcess RtlAddVectoredExceptionHandler RtlGetNativeSystemInformation ZwLoadDriver ZwMapUserPhysicalPages ZwOpenProcess ZwQuerySystemInformation ZwQuerySystemInformationEx ZwResumeThread ZwSetInformationThread ZwTerminateProcess LdrOpenImageFileOptionsKey NtCreateSection ZwCreateSection NtCreateFile NtCreateKey NtDeleteFile NtDeleteKey NtDeleteValueKey NtOpenFile
|
En este punto tenemos algunas opciones para tratar de evadir esto, podríamos obtener una copia de NTDLL.DLL desde el disco sin los HOOK´s de los EDR ya que se implantan en memoria en tiempo de ejecución una vez mapeada en memoria la librería, por lo tanto una opción inteligente sería sobreescribir la librería en nuestro proceso mapeando la copia del disco.
Figura 4-4. Evasión de monitorización mediante sobrescritura de NTDLL
|
Sin embargo en RajKit voy hacer uso de una técnica que evita esta secuencia de ejecución de Windows y evade las capas de abstracción que tenemos hasta la instrucción SYSCALL, obteniendo en tiempo de ejecución la llamada al sistema asociada a la rutina nativa de la que queremos hacer uso.
Para ello lo que haremos será usar códigoasm compilado dentro de nuestro binario actuando como una API de Windows, pero realizar esto sin ser detectado tiene cierta complejidad, en siguiente diagrama muestro un poco los paso que vamos a seguir con nuestra técnica en concreto, ya que se podría implementar de diferentes formas, pero esta es la óptima:
Figura 4-5. Flujo de ejecución Syscall Direct Calling |
4.1 NTDLL.DLL BASE DESDE PEB
Lo primero que tenemos que hacer es obtener la dirección base de la librería dinámica NTDLL, pero tendremos que hacerlo sin usar ninguna API, para ello tendremos que ir desde el Thread Environment Block (TEB)al Process Enviroment Block (PEB)y continuar escalando a través de las estructuras y listas enlazadas hasta DllBase, como?
Figura 4-6. Dirección Base a través de Process Environment Block |
Como explico en el diagrama tendremos que recuperar la dirección de PPEB dentro de TEB para ello tendremos que acceder a través del registro GS para sistemas x64 en la compensación 0x60:
PSW2_PEB Peb =(PSW2_PEB)__readgsqword(0x60);
Desde PEB en el desplazamiento 0x18 tenemos Ldrdel tiempo _PEB_LDR_DATA, accedemos:
PSW2_PEB_LDR_DATALdr = Peb->Ldr;
Si volcamos _PEB_LDR_DATA vemos en el desplazamiento 0x20 una lista _LIST_ENTRY InMemoryOrderModuleList que en realidad es el encabezado a una lista doblemente enlazada que entre otras cosas contiene los módulos cargados y su dirección base, lo que significa que tendremos que iterar todas las estructuras, es decir una estructura por cada módulo cargado hasta encontrar NTDLL.DLL:
En este punto ya tendríamos localizada NTDLL.DLL y su dirección base DllBase.
4.2 DIRECCIÓN DE FUNCIONES DESDE EAT
La EXPORT_ADDRESS_TABLE (EAT)funciona como la IAT solo que la biblioteca exportará las funciones al ejecutable de la imagen, en el que el programa se importará al IAT.
Para ello tenemos que investigar la estructura de datos IMAGE_EXPORT_DIRECTORYdel formato PE y acceder al desplazamiento donde se encuentra AddressOfFunctions, qué es el índice de todas las funciones, utilizaremos la dirección base 7ff9ad420000de NTDLL.DLL como ejemplo, desde WINDBG se requiere seguir el siguiente diagrama de desplazamientos con alguna conversión a HEX por el camino:
Figura 4.2-1. Estructuras y desplazamientos hasta IMAGE_EXPORT_DIRECTORY |
Una vez en la estructura, a través de los desplazamiento de la VirtualAddressde IMAGE_DATA_DIRECTORY tendríamos lo siguiente:
- 0x7FFFCD4A0000+0xD8+0x18+0x70+0x14c500
Con esos 3 campos obtenidos, que son los que nos interesan de esa estructura tenemos que hacer un pequeño calculo por que cuando queremos obtener la dirección de una función en código mediante la API de Windows, realmente se obtiene buscando por ejemplo el nombre “AlpcFreeCompletionListMessage” en AddressOfNames del cual se extrae la posición en la matriz dentro de AddressOfNameOrdinalsque a su vez nos devuelve el índice de la función en AddressOfFunctions y esa dirección que obtenemos en realidad es una Relative Virtual Address (RVA)que tendremos que sumar a la DllBase que obtuvimos previamente!
Figura 4.2-2. Obtención de RVA de AlpcFreeCompletionListMessage |
Mostramos en WINDBG el mismo proceso:
DllBase+AddresOfFunctions[AddressOfNames[AddressOfNameOrdinals]] = Dir. función
4.3 OBTENER SYSCALL
En este último paso tenemos que extraer de las llamadas nativas su correspondiente SYSCALL, para ello se realiza una búsqueda de funciones que empiezan por Zwy luego se crea una matriz de llamadas al sistema almacenando el nombre cambiado por Nt, de esta forma es más eficiente ya que no requiere verificar la presencia de Ntdll al comienzo del nombre.
Por lo general cada rutina de servicio del sistema nativo tiene 2 versiones parecidas con prefijo diferente y son atendidas por la misma rutina del sistema en modo kernel, mostramos como ejemplo: NtAllocateVirtualMemoryy ZwAllocateVirtualMemory comparten la SYSCALL 18
Iremos iterando NumerOfNames en busca del prefijo para ir almacenando en la estructura un HASH del nombre para evadir análisis de EDR/AV.. junto con la dirección de la función.
Lo bueno de esta técnica que implementa SysWhispers2 es que después ordena de forma ascendente esas direcciones y resulta que coincide también con la llamada correspondiente a su nombre!!
Genial porque de esa forma la llamada al sistema en nuestra matriz corresponderá a la posición del nombre dentro de esa matriz!
4.4 EJECUCIÓN
Por último nos quedaría manejar los registros del procesador y controlar el contexto de los registros para poder mantener los parámetros que requieren las funciones y mientras usar la función que nos devolverá la llamada al sistema que necesitamos, como?
Tenemos que tener en cuenta la convención de llamadas __fastcall x64 que utiliza determinados registros del procesador, en el caso de argumentos enteros son los registros RCX,RDX,R8y R9 y a partir del quinto se pushean al stack, por lo tanto primero guardaremos los registros que enviaron al llamar a nuestra propia rutina nativa:
Lo siguiente es preparar el registro ECXcon el HASH correspondiente a NtAllocateVirtualMemory= 015882105h y llamar a la función propia:
Volvemos a restaurar los registros:
Y por último invocar SYSCALL:
5. DLL HOLLOWING
En esta fase hablaremos de la sobre carga de módulos en un proceso dado, al cual despues se le inyectara una shellcode que se ejecutará como un thread de esta forma no se asignan páginas de memoria RWX ni se cambian sus permisos el proceso en ningún momento.
El código malicioso se inyecta en una DLL legítima de Windows por lo tanto se entorpece la detección, tanto por análisis estáticos como por sistemas de detección como los EDR/AV.
El thread con el código malicioso, está asociado con un módulo legítimo de Windows.
Figura 5-1. DLL HOLLOWING “Sobrecarga de módulos” |
Para realizar esta operación y aunque nosotros no lo implementemos en el código, el diseño de nuestro malware requiere realizar las llamadas a API mediante la técnica descrita en el punto 4 del trabajo, syscal direct calling para evitar el monitoreo de EDR.
- OpenProcess
- LoadLibrary
- VirtualAllocEx
- WriteProcessMemory
- CreateThread
En este ejemplo realizaremos la inyección de la DLL benigna en un notepad.exe, para comprender más fácil la técnica, pero en nuestro malware se propone hacerlo en el propio proceso de nuestra pieza de malware para después realizar la ocultación desde el driver RajKit.sysdesde la siguiente fase de PTE REMAPPING.
Lo primero es obtener un manejador del proceso a través del ID:
Cargamos la DLL benigna propia de la biblioteca de windows , system32:
Buscamos el Base Addressdel la dll inyectada en el proceso, para ello iteramos todos los módulos y simplemente comparamos con el nombre del modulo amsi.dll en hasta el acierto:
Extraemos el AddressOfEntryPoint de la dll escalando a través de la estructura portable ejecutable:
Escribimos la shellcode en la memoria en el EntryPoint de la dll benigna para después crear un thread hacia el:
5.1 EJECUCIÓN
Trazaremos la ejecución del programa en busca del contenido del thread en el modulo amsi.dll, de tal forma que deberíamos obtener nuestra shellcode al volcar el thread del módulo benigno de windows:
Podemos observar como el EntryPointde la dll beigna amsi.dll contiene nuestra shellcode.
6. PTE REMAPPING
Antes de entrar en esta técnica, es importante para el lector el planteamiento correcto del conjunto de la fase de “DLL HOLLOWING+PTE REMAPPING“ , la implementación conjunta como bien se describe al principio del trabajo no se va llevar a cabo pero es indispensable para el lector conocer la propuesta, ya que no e visto ninguna implementación parecida que abarque este conjunto de técnicas.
Se propone de la siguiente manera:
- Desde la fase “DLL HOLLOWING”se copia 0x1000desde el EntryPoint del código en memoria de amsi.dll en un espacio de memoria reservado previamente dentro de amsi.dll
- Se inyecta la shellcode a partir del EntryPoint de amsi.dll en memoria
Fase de ocultación desde el driver:
- Mediante la técnica que explico desde el punto 7.1, se modifican los pfn de las PTE que en realidad son las direcciones físicas reales de cada página de memoria, y se intercambian.
- De esta forma nuestro código quedaría oculto, cuando se haga un volcado del EntryPoint la traducción de direcciones virtuales a físicas, devolverá el volcado del EntryPoint original en lugar de la shellcode maligna.
Figura 6-1. DLL HOLLOWING + PTE REMAPPING |
IMPORANTE!! Por lo tanto no solo inyectamos el código en una dll benigna del propio sistema Windows, sino que además en un análisis forense de la misma ese código quedaría oculto aprovechando la propia traducción de direcciones virtuales a físicas que realiza el propio sistema.
7.1 DIRECCIONES VIRTUALES, FÍSICAS, PAGINACIÓN Y BITS DE CONTROL
Todo lo que hacemos en este punto se basa en PAE, que se habilita a través de uno de los bits de control del registro CR4 del procesador, concretamente el sexto bit empezando de la derecha:
Ahora pasemos a despiezar lo que conocemos como dirección virtual, que es lo que representa cada parte y como se accede desde la base de la tabla PML4 a través de la dirección de memoria física que contiene el registro CR3 a la diferentes estructuras principales de paginación para llegar a la PTE y obtener la dirección física de la página correspondiente:
Figura 6-1. Traducción Dirección Virtual a Dirección Física |
En el diagrama que e hecho se explica un poco el recorrido de la traducción, de tal forma que cada 9 bits desde el bit 47 se realiza un cálculo con el offset de la estructura de paginación y el registro de esa estructura se indexa para acceder a la siguiente estructura de paginación y terminar en la dirección física lineal correspondiente a esa dirección virtual lineal.
De esta forma tenemos 4 estructura de paginación responsables de esta traducción:
- PML4→ bits 47-39 → 2^9=512 posibles indexaciones
- PDPE→ bits 38-30 → 2^9=512 posibles indexaciones
- PDE→ bits 29-21 → 2^9=512 posibles indexaciones
- PTE → bits 20-12 → 2^9=512 posibles indexaciones
Con lo que terminaríamos obteniendo la dirección física de la página correspondiente la cual en 64BITS seria
- 2^12=4096 bytes → 4K
Siempre y cuando en los bits de control de la estructura PDPTE no tengamos activado page_size, lo que permitiría crear Large Pages de 1GB y cambiar un poco la transición de la traducción, ya que se prescinde de las PTE y se accedería directamente desde PDE
Visto muy por encima el proceso de traducción y antes de explicar la técnica que trataremos desde el driver vamos a pasar al Windbg que mediante un ejemplo obtendré los flags de control de una entrada PTE para modificarlo y ver qué ocurre, que en este caso será la shellcode que inyectamos en un espacio de direcciones reservado por nosotros, para ello tenemos este código:
- VirutalAlloc → Reservamos espacio con permisos 0x40 (PAGE_EXECUTE_READWRITE)
- MoveMemory → [payload] ('\x90')
- VirtualProtect → Cambiamos permisos a solo lectura → (PAGE_READONLY)
- MoveMemory → [payload2] ('\x00')
Por lo tanto cambiaremos los permisos mediante la modificación del bit de control R/Wde la PTE correspondiente a las entradas de página de la dirección virtual del espacio reservado, para permitir RtlMoveMemory() del segundo payload.
Ejecutamos el programa en el GUEST y desde WinDBG nos ponemos en el contexto del proceso para hacer un volcado de la dirección del espacio reservado:
Tenemos nuestro espacio reservado y los NOP´s escritos en la dirección virtual 0x18000:
En este punto de la ejecución, nos encontramos con los permisos en PAGE_READONLYdespués de ejecutar VirtualProtect(), podemos comprobarlo mediante el comando !pte:
Cada estructura de paginación nos proporciona unos flags de control, en nuestro caso solo nos interesan los de la Page Table Entry:
- BIT 1→ READ/WRITE
- BIT 2→ USER/SUPERUSER
- BIT 61 → NX (NO EXECUTE)
P |
Present |
G |
Global |
R/W |
Read/Write |
AVL |
Available |
U/S |
User/Supervisor |
PAT |
Page Attribute |
PWT |
Write-Through Table |
M |
Maximum |
PCD |
Cache Disable |
PK |
Protection Key |
A |
Accessed |
D |
Dirty |
PS |
Page Size |
XD |
Execute Disable |
Comprobamos traduciendo a binario el contenido de esta Page Table, si el segundo bit se encuentra desactivado significa que solo es de lectura:
Activamos ese BIT y sobre-escribimos el puntero que contiene la dirección de nuestro PTE en FFFFDA8000000C00:
Figura 7-2. Activamos el bit 2 {READ} en binario |
Conseguiremos escribir en ese espacio de memoria? Continuamos con la ejecución del programa en RING3 y volvamos a hacer un volcado de esa dirección, deberíamos tener un slide de '\x00':
6.2 SUBVERSIÓN DE LA MEMORIA
Si bien existen varias técnicas que nos permiten ocultar partes seleccionadas de la memoria de un proceso en la aplicación de espacio de usuario, solo hablare de una ellas que será la que implementaremos en nuestro driver será el “PTE REMAPING”.
Qué es lo que conseguimos con esta técnica? Antes hemos visto que una entrada PTE contiene un marco de página llamado pfn, que sin entrar en detalles básicamente los PTE obtienen el pfn para la siguiente estructura de paginación, por los tanto en un contexto de x64 donde las páginas físicas son de 4096 bytes es decir 0x1000, y multiplicando ese pfn por el tamaño de la página física nos daría una dirección de memoria física!!
Comprobemos que es cierto en WinDBG y dentro del contexto del programa del ejemplo anterior, tenemos una shellcode de '\x00' cargada en la dirección 0x18000:
-
Extraemos el marco de página de PTE y lo multiplicamos por 0x1000
-
0x1a2f7e → dirección física
-
0x18000 → dirección virtual
Realizando un dumpeo de las 2 direcciones deberíamos obtener los mismos datos, ya que en realidad estaríamos accediendo al mismo espacio físico, bien mediante traducción o bien de forma directa.
Por lo tanto, realmente podemos calcular la página física de la dirección virtual en tiempo de ejecución, y si aprovechamos para que el marco de página de la PTE de 2 direcciones virtuales diferentes apuntarán al mismo pfn??:
- Reservamos 2 espacios de memoria en user
- En uno de ellos lo rellenamos de nuestro payload y en el otro de código benigno
- Desde el driver obtenemos los correspondientes pfn de las PTE de las VA
- Y sobre-escribimos para el pfn de la página con el payload por el pfn del código benigno
Figura 6.2-1. Diagrama Subversión de la memoria |
Trato de explicar en el diagrama anterior como sería la técnica que tratamos, de tal forma que “des-referenciamos” esa página física de su PTE, lo cual requerirá el recuperarla cuando se quiera acceder a ella.
6.3 DRIVER
Lo primero que haremos es reservar memoria para escribir nuestra shellcode en memoria y reservar otro espacio de memoria de las mismas características con un sleed de 0x42 como zona de memoria benigna, después obtendremos la PTE con su PFN correspondiente de la misma forma que explique con el diagrama del punto 2 del write.
Si bien existe una API en nstoskrnl.exellamada nt!MiGetPteAddress que en el desplazamiento 0x13 contiene la base de los PTE:
Nosotros llegaremos extrayendo el valor CR3 del EPROCESS y escalando hasta PTE:
- PML4E → PDPT → PD → PDE → PTE [PFN]
6.4 FASE 1
Reservamos 2 espacios en memoria en uno de ellos escribimos la shellcode descifrada y en el otro lo rellenamos de 0x42. Obtenemos la dirección virtual de la shellcode del tamaño 0x1000 que en nuestro caso se reserva en 0x18000 y setemaos su PTE a 0000000000000000 , y la dirección de la memoria limpia en 0x19000 con un tamaño también de 0x1000
Figura 6.4-1. Contenido PTE seteado a 00000000000000000 |
Intento representar en el diagrama la primera fase, recordar que el PFN de la PTE multiplicado por 0x1000 nos devuelve la dirección física real de tal forma que podemos volcar el contenido y mostramos con windbg:
- 0x18000 → (0x6a1cb*0x1000) = DIR.FISICA
Podemos observar como el volcado de la dirección física 0x6a1cb000que es la dirección virtual 0x18000contiene la shellcode descifrada con la clave RajKit mediante XOR, lo podemos ver en el debugger en el mapa de memoria:
- shellcode[i]^[RajKit(i)]
6.5 FASE 2
En la segunda fase asignamos un PFN a la PTE de la dirección virtual que apunta a la página que contiene código benigno 0x42 y mantendremos oculta la shellcode:
Figura 6.5-1. Seteamos el contenido de PTE y PFN para ocultar la shellcode |
Lo vemos desde el windbg como el volcado del PFN 0x35815que en realidad es la dirección física 0x35815000 no contiene la shellcode:
Vemos como volcamos la dirección virtual de la shellcode que si obtenemos su PFN nos devuelve la PTE y si traducimos esa PTE nos devuelve la dirección 0x18000 que a su vez haciendo el volcado en realidad contiene un sleep de “0x42”:
6.6 FASE 3
En la fase 3 revertimos la ocultación de la shellcode, apuntaremos con un hilo de ejecución para ejecutarla y volvemos a ocultar en la memoria de la misma forma
Figura 6.6-1. Reversión de la ocultación para posterior ejecución de shellcode |
Esto nos ejecutara una shellcode que abrirá calc.exe para después volver a ocultarla:
7. KERNEL EXCEPTION HOOKING
Como punto extra introduzco un reversing y posterior evasión de Kernel Patch Protection mediante una técnica llamada Kernel Exception Hooking, que comencé explicando en el punto 2 de este trabajo continuamos:
KiExceptionDispatchy KiBugCheckDispatchvan rellenando una estructura KEXCEPTION_FRAME, guardando los registros volátiles.
Nos centraremos en el reversing de ntoskrnl.exea partir de KeBugCheckEx y KeBugCheck2que comienza deshabilitando las interrupciones, guardando el contexto de la llamada y el estado del procesador para pasar directamente el control a KeBugCheck2:
Se guarda completamente el contexto de la ejecución antes de la excepción (RtlCaptureContext):
El bit de característica 0x00020000 para Windows de 64 bits tiene el nombre de lenguaje ensamblador conocido KF_BRANCH. Está configurado para procesadores que el kernel reconoce que tienen registros específicos del modelo para mantener un registro de última rama (LBR). PRCB
El sistema operativo habilita la función de ramificación para tareas del kernel (por ejemplo, después de un BSOD causado por algún controlador, puede obtener LastBranchFrom/ To del archivo de volcado de fallas). Si dicha tarea interrumpe su grabación e intenta continuar grabando después de reprogramar su tarea, tendrá un MSR_LASTBRANCH_TOSdiferente: (KiSaveProcessorControlState)
Después KeBugCheckExincrementa KiHardwareTriggery cede el control a KeBugCheck2:
Una vez en KeBugCheck2:
- Prepara y escribe la información del crashdump
- Congela la ejecución en las CPU
- KiDisplayBluescreen
- Reinicio
Recibimos 4 argumentos, el primero de ellos BugCheckCode, a partir del cual se realizan varias comprobaciones en función del código:
v11 = *&BugCheckCode
Recibimos en v66 el argumento 1:
Comprueba si estamos bajo Hyper-V:
Comprobaciones NMI :
Se verifican los proceso congelados con IoInitilizeBugCheckProcess desde KeBugCheck2:
Se llama a KiDisplayBlueScreen para mostrar el famoso pantallazo azul BSOD:
Reinicio del sistema HalReturnToFirmware:
En Hal.dll desensamblando vemos HalPrivateDispatchTable (que se encuentra en la sección .idata lejos de KPP)obtendremos así la dirección de la tabla HALL_DISPATCHque es la que contiene punteros a las funciones que implementa HAL.DLL y necesitamos enganchar:
Se hace un hook a HalPrepareForBugcheck:
Se extrae el contexto de la rutina interrumpida por KeBugCheck2:
Probamos el driver realizando el HOOK:
Interceptamos y no vemos BSOD:
Sin HOOK:
9. BIBLIOGRAFÍA
https://www.iberlibro.com/9781449626365/ROOTKIT-ARSENAL-ESCAPE-EVASION-DARK-144962636X/plp
https://dl.ebooksworld.ir/motoman/No.Starch.Press.Rootkits.and.Bootkits.www.EBooksWorld.ir.pdf
https://www.amazon.es/Practical-Reverse-Engineering-Reversing-Obfuscation-ebook/dp/B00IA22R2Y
https://books.google.es/books/about/Rootkits.html?id=fDxg1W3eT2gC&redir_esc=y
https://rvsec0n.wordpress.com/2019/09/13/routines-utilizing-tebs-and-pebs/
https://calcifer.org/documentos/librognome/glib-lists-queues.html
https://learn.microsoft.com/es-es/cpp/build/x64-calling-convention?view=msvc-170
https://github.com/jthuraisamy/SysWhispers2
https://github.com/can1357/ByePg
https://learn.microsoft.com/es-es/troubleshoot/windows-client/performance/nmi-hardware-failure-error
https://windows-internals.com/hyperguard-secure-kernel-patch-guard-part-1-skpg-initialization/
https://www.geoffchappell.com/studies/windows/km/ntoskrnl/structs/kprcb/featurebits.htm
https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/ntos/hal/hal_private_dispatch.htm
https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/ntos/amd64_x/ktrap_frame.htm
https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/ntos/ktrap_frame.htm?tx=138&ts=0,4
https://www.geoffchappell.com/studies/windows/km/bugchecks/index.htm
https://www.bleepingcomputer.com/forums/t/762974/bsod-using-windbg-windows-debugger-and-analyze-v/
https://en.wikipedia.org/wiki/Deferred_Procedure_Call
https://empyreal96.github.io/nt-info-depot/Windows-Internals-PDFs/WindowsSystemInternalPart1.pdf
https://stackoverflow.com/questions/35670045/accessing-user-mode-memory-inside-kernel-mode-driver
https://github.com/TheCruZ/kdmapper
https://github.com/AzAgarampur/byeintegrity8-uac
https://merlin-c2.readthedocs.io/en/latest/quickStart/agent.html