KERNEL-SHELLCODE-HIDING-BY-PTE
|
RajKit |
1.- Introducción
Aprovecharemos la posibilidad de monitorear y proteger nuestro código en user desde RING0.
Para ello me voy a basar en la arquitectura de paginación de windows, la traducción de direcciones virtuales y los registros de control que proporciona Intel.
El proceso se realiza de la siguiente manera:
- Reservaremos 2 espacios en Ring3 y inyectamos la shellcode en uno de ellos
- Usaremos DeviceIoControl para comunicarnos con el driver.
- En el driver iremos escalando desde el registro CR3 y la dirección virtual hasta obtener la Page Table Entry y su marco de página.
- Des-referenciamos el espacio de memoria “shellcode” asignándole otro pfn a la PTE de su dirección virtual como la de el espacio benigno.
De esta forma mantendremos oculto ese espacio de memoria reservado dentro del proceso que queramos
2.- Dirección virtuales, físicas, paginación y WinDBG
Todo lo que hacemos 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 pagina correspondiente:
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:
- VirtualAlloc→ 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)
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 PTEen FFFFDA8000000C00:
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':
3.- 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 pfnpara la siguiente estructura de paginación, por los tanto en un contexto de x64 donde las páginas físicas son de 4096bytes 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:
<span style="overflow: hidden; display: inline-block; margin: 0.00px -0.00px; border: 1.33px solid #000000; transform: rotate(0.00rad) translateZ(0px); -webkit-transform: rotate(0.00rad) translateZ(0px); width: 616.20px; height: 78.20px;"
-
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
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.
5.-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!MiGetPteAddressque en el desplazamiento 0x13 contiene la base de los PTE:
Nosotros llegaremos extrayendo el valor CR3del EPROCESS y escalando hasta PTE:
- PML4E → PDPT → PD → PDE → PTE [PFN]
5.1-TÉCNICA ANTI-FORENSE 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 0x1000que en nuestro caso se reserva en 0x18000y seremos su PTE a 0000000000000000, y la dirección de la memoria limpia en 0x19000con un tamaño también de 0x1000
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)]
5.2-TÉCNICA ANTI-FORENSE 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 codigo benigno 0x42 y mantendremos oculta 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”:
5.3-TÉCNICA ANTI-FORENSE 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:
Esto nos ejecutara una shellcode que abrirá calc.exe para después volver a ocultarla:
6.-FUENTES
- https://lsi.vc.ehu.eus/pablogn/docencia/manuales/SO/TemasSOuJaen/ADMINISTRACIONDELAMEMORIA/5.1Paginacion.htm
- https://www.microsoft.com/en-us/security/blog/2020/07/08/introducing-kernel-data-protection-a-new-platform-security-technology-for-preventing-data-corruption/
- 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://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/kernel-mode-extensions