R0 CREW

Gray Hat Python: Глава 10 - Фаззинг драйверов Windows (Перевод: Prosper-H)

Перейти к содержанию

Intro

Атаковать драйверы Windows становится привычным делом для баг-хантеров и разработчиков эксплойтов. Хотя, за прошлые несколько лет, были некоторые удаленные атаки на драйверы, более распространенны локальные атаки, которые нацелены на повышение привилегий в скомпрометированной системе. В предыдущей главе, мы использовали Sulley для поиска переполнения буфера в WarFTPD. То, чего мы не знали об этом демоне, так это то, что он был запущен пользователем с ограниченными правами. Если бы нам нужно было атаковать этот север, то в конечном итоге у нас были бы только ограниченные права, которые в некоторых случаях сильно мешают тому, какую информацию мы можем украсть или к каким сервисам мы можем получить доступ на скомпрометированной системе. Если бы мы знали, что в системе есть драйвер уязвимый к переполнению буфера [1] или другим атакам [2], мы могли бы использовать его, как средство получения системных привилегий и иметь свободный доступ к системе и всей вкусной информации на ней.

Для того, чтобы взаимодействовать с драйвером, нам нужен переход из пользовательского режима в режим ядра. Делается это, путем передачи информации драйверу, используя управляющие коды ввода/вывода «Input/Output Controls (IOCTLs)», которые являются специальными шлюзами позволяющими приложениям и сервисам пользовательского режима получать доступ к устройствам и компонентам режима ядра. Как и с любыми средствами передачи информации от одного приложения другому, мы можем использовать не безопасные реализации обработчиков IOCTL, чтобы получить повышение привилегий или полностью обрушить целевую систему.

Сначала мы рассмотрим, как соединиться с локальным устройством, которое поддерживает IOCTL, а также рассмотрим вопрос отправки IOCTL кодов устройству. Затем рассмотрим использование отладчика Immunity Debugger для изменения (mutate) IOCTL-кодов до их отправки драйверу. Далее мы воспользуемся встроенной библиотекой статического анализа отладчика, driverlib, чтобы получить более подробную информацию о целевом драйвере. Мы также заглянем под капот driverlib и научимся расшифровывать важные управляющие потоки, имена устройств и IOCTL-коды из скомпилированного файла драйвера. И под конец, используем результаты из driverlib, чтобы создать тесты-кейсы для автономного драйвер-фаззера. Давайте начнем.

10.1 Взаимодействие с драйвером

Почти каждый драйвер, в Windows, регистрируется в операционной системе с определенным именем и символьной ссылкой, которая позволяет режиму пользователя получить дескриптор к драйверу так, чтобы он мог общаться с ним. Мы используем вызов CreateFileW [3], экспортируемый из kernel32.dll, чтобы получить этот дескриптор. Прототип функции выглядит следующим образом:

HANDLE WINAPI CreateFileW(
    LPCTSTR lpFileName,
    DWORD dwDesiredAccess,
    DWORD dwShareMode,
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD dwCreationDisposition,
    DWORD dwFlagsAndAttributes,
    HANDLE hTemplateFile
);

Первым параметром является имя файла или устройства, для которых мы хотим получить дескриптор; это будет значение символьной ссылки, которую экспортирует наш драйвер. Параметр dwDesiredAccess определяет тип доступа к устройству (чтение, запись или то и другое); для наших целей мы хотели бы иметь доступ GENERIC_READ (0x80000000) и GENERIC_WRITE (0x40000000). Мы установим параметр dwShareMode в нуль, что означает, что к устройству нельзя получить доступ, пока мы не закроем дескриптор, возвращаемый из CreateFileW. Мы также установим параметр lpSecurityAttributes в NULL, что означает, что дескриптор безопасности по умолчанию применяется к дескриптору и не может быть унаследован никакими дочерними процессами, которые мы можем создать. Параметр dwCreationDisposition установим в OPEN_EXISTING (0x3),что означает, что мы будем открывать устройство, только если оно существует, в противном случае CreateFileW не будет работать. Последние два параметра мы установим в нуль и NULL, соответственно.

Как только был получен действительный дескриптор от вызова CreateFireW, мы можем использовать его, чтобы передать IOCTL устройству. Для отправки IOCTL будем использовать вызов DeviceIoControl [4] , который экспортируется из kernel32.dll. Он имеет следующий прототип:

BOOL WINAPI DeviceIoControl(
    HANDLE hDevice,
    DWORD dwIoControlCode,
    LPVOID lpInBuffer,
    DWORD nInBufferSize,
    LPVOID lpOutBuffer,
    DWORD nOutBufferSize,
    LPDWORD lpBytesReturned,
    LPOVERLAPPED lpOverlapped
);

Первым параметром является дескриптор, который возвращает вызов CreateFileW. Параметр dwIoControlCode это IOCTL-код, который будет передаваться драйверу устройства. Этот код определит, какие действия должен предпринять драйвер, сразу после обработки нашего IOCTL-запроса. Следующий параметр lpInBuffer является указателем на буфер, который содержит информацию, которую мы передает драйверу устройства. Этот буфер представляет наибольший интерес для нас, поскольку мы будем фаззить (изменять) все его содержимое, прежде чем передать его драйверу. Параметр nInBufferSize это просто число, которое сообщает драйверу размер буфера, который мы передаем ему. Параметры lpOutBuffer и lpOutBufferSize идентичны предыдущим двум параметрам, но используются для передачи информации из драйвера в user mode. Параметр lpBytesReturned это дополнительное значение, которое говорит нам, сколько данных было возвращено нашим вызовом. Последний параметр lpOverlapped мы просто установим в NULL.

Теперь, когда у нас есть базовая информация о взаимодействии режима пользователя с драйверами устройств, давайте, используем отладчик Immunty Debugger, чтобы перехватить (hook) вызовы DeviceIoControl и изменить входящий буфер до того, как передать его целевому драйверу.

10.2 Фаззинг драйвера с помощью Immunity Debugger

Мы можем использовать Immunity Debugger для перехвата вызовов DeviceIoControl прежде, чем они достигнут тестируемого драйвера. Мы напишем простую команду PyCommand, которая будет перехватывать все вызовы DeviceIoControl, изменять (mutate) входящий буфер, сохранять всю необходимую информацию на диск и отдавать управление обратно целевому приложению. Мы будем сохранять информацию на диск, потому что успешное выполнение фаззинга, при работе с драйверами, определенно приведет к крашу системы; поэтому мы хотим иметь историю тест-кейсов нашего последнего фаззинга, до обрушения системы, чтобы мы могли воспроизвести результаты снова.

Давайте перейдем к коду! Откройте новый Python-файл, назовите его ioctl_fuzzer.py и введите следующий код:

ioctl_fuzzer.py

import struct
import random
from immlib import *

class ioctl_hook( LogBpHook ):
    
    def __init__( self ):
        self.imm = Debugger()
        self.logfile = "C:\ioctl_log.txt"
        LogBpHook.__init__( self )

    def run( self, regs ):
        """
        We use the following offsets from the ESP register to trap the arguments to DeviceIoControl:
        ESP+4 -> hDevice
        ESP+8 -> IoControlCode
        ESP+C -> InBuffer
        ESP+10 -> InBufferSize
        ESP+14 -> OutBuffer
        ESP+18 -> OutBufferSize
        ESP+1C -> pBytesReturned
        ESP+20 -> pOverlapped
        """
        in_buf = ""

        # read the IOCTL code 
         (#1): ioctl_code = self.imm.readLong( regs['ESP'] + 8 )
        
        # read out the InBufferSize
         (#2): inbuffer_size = self.imm.readLong( regs['ESP'] + 0x10 )
        
        # now we find the buffer in memory to mutate
         (#3): inbuffer_ptr = self.imm.readLong( regs['ESP'] + 0xC )
        
        # grab the original buffer
        in_buffer = self.imm.readMemory( inbuffer_ptr, inbuffer_size )
         (#4): mutated_buffer = self.mutate( inbuffer_size )
        
        # write the mutated buffer into memory
         (#5): self.imm.writeMemory( inbuffer_ptr, mutated_buffer )
        
        # save the test case to file
         (#6): self.save_test_case( ioctl_code, inbuffer_size, in_buffer, mutated_buffer )

    def mutate( self, inbuffer_size ):

        counter = 0
        mutated_buffer = ""

        # We are simply going to mutate the buffer with random bytes
        while counter < inbuffer_size:
            mutated_buffer += struct.pack( "H", random.randint(0, 255) )[0]
            counter += 1

        return mutated_buffer

    def save_test_case( self, ioctl_code,inbuffer_size, in_buffer, mutated_buffer ):

        message = "*****\n"
        message += "IOCTL Code: 0x%08x\n" % ioctl_code
        message += "Buffer Size: %d\n" % inbuffer_size
        message += "Original Buffer: %s\n" % in_buffer
        message += "Mutated Buffer: %s\n" % mutated_buffer.encode("HEX")
        message += "*****\n\n"

        fd = open( self.logfile, "a" )
        fd.write( message )
        fd.close()

def main(args):

    imm = Debugger()

    deviceiocontrol = imm.getAddress( "kernel32.DeviceIoControl" )

    ioctl_hooker = ioctl_hook()
    ioctl_hooker.add( "%08x" % deviceiocontrol, deviceiocontrol )

    return "[*] IOCTL Fuzzer Ready for Action!"

Мы не будем рассматривать уже известные нам приемы и функции Immunity Debugger; LogBpHook – был рассмотрен в Главе 5. Тут мы просто перехватываем IOCTL-код, который передается драйверу (#1), длину входящего буфера (#2) и указатель на входящий буфер (#3). Затем создаем буфер, состоящий из случайных байтов (#4), но той же длины, что и оригинальный буфер. Затем перезаписываем оригинальный буфер – нашим видоизмененным буфером (#5), после чего сохраняем наш тест-кейс в log-файл (#6) и возвращаем управление программе режима пользователя.

После того, как вы написали код, убедитесь, что файл ioctl_fuzzer.py находится в директории PyCommnads отладчика Immunity Debugger. Далее вам нужно выбрать жертву – любую программу, которая использует IOCTL для общения с драйвером (например, снифер пакетов, фаервол или антивирусная программа будут идеальными целями). Запустите жертву в отладчике и выполните команду PyCommand – ioctl_fuzzer. Возобновите отладчик и начнется фаззинг! Листинг 10-1 показывает некоторые зарегистрированные тест-кейсы, выполненные во время фаззинга, против программы Wireshark [5], которая является снифером сетевых пакетов.

Листинг 10-1: Фаззинг выполненный против Wireshark

*****
IOCTL Code:      0x00120003
Buffer Size:     36
Original Buffer:
000000000000000000010000000100000000000000000000000000000000000000000000
Mutated Buffer:
a4100338ff334753457078100f78bde62cdc872747482a51375db5aa2255c46e838a2289
*****
*****
IOCTL Code:      0x00001ef0
Buffer Size:     4
Original Buffer: 28010000
Mutated Buffer:  ab12d7e6
*****

Вы можете видеть, что мы обнаружили два поддерживаемых IOCTL кода (0x0012003 и 0x00001ef0) и сильно изменили входящие буферы, которые были отправлены драйверу. Можно продолжать взаимодействовать с программой режима пользователя, продолжая изменять входящие буферы и надеяться, в какой-то момент, вызвать сбой в драйвере!

Хотя в использовании это простая и легкая методика, она все же имеет свои ограничения. Например, мы не знаем имени устройства, которое мы фаззим (хотя, мы могли бы перехватить CreateFileW и подсмотреть его, сопоставив возвращаемый дескриптор, с дескриптором используемым в DeviceIoControl – оставляю это вам в качестве упражнения), и мы знаем только те IOCTL-коды, которые были перехвачены во время фаззинга, а это значит, что мы можем пропустить некоторые тест-кейсы. Кроме того, было бы намного лучше, если бы мы могли продолжать фаззинг до момента пока бы нам это не надоело или мы не нашли уязвимость.

В следующем разделе мы узнаем, как использовать инструмент статического анализа driverlib, который поставляется вместе с Immunity Debugger. Используя driverlib, можно перечислить имена всех возможных устройств, которые предоставляет драйвер, так же как и все IOCTL-коды, которые он поддерживает. Опираясь на эти данные, мы сможем создать очень эффективный автономный генерирующий фаззер, который можно оставить работающим на неопределенное время и который не требует взаимодействия с программой режима пользователя. Давайте приступим к делу!

10.3 Driverlib— Инструмент статического анализа для драйверов

Driverlib – это библиотека Python, предназначенная для автоматизации некоторых нудных задач реверсинга, которые требуют обнаружения ключевой информации из драйвера. Обычно для того, чтобы определить какие имена устройств и IOCTL-коды использует драйвер, нужно загрузить исследуемый драйвер в IDA Pro или Immunity Debugger и вручную найти интересующую информацию, пройдясь по дизассемблерному коду. Мы рассмотрим немного кода из библиотеки driverlib, чтобы понять, как она автоматизирует этот процесс, а затем воспользуемся ею, чтобы предоставить IOCTL-коды и имена устройств для нашего драйвер-фаззера. Давайте вначале рассмотрим код driverlib.

10.3.1 Обнаружение имен устройств

Использовать мощную встроенную библиотеку Python отладчика Immunity Debugger, чтобы найти имена устройств внутри драйвера достаточно просто. Взгляните на Листинг 10-2, это код из driverlib, который отвечает за обнаружение имен устройства.

Листинг 10-2: Процедура обнаружения имени устройства из driverlib

def getDeviceNames( self ):

    string_list = self.imm.getReferencedStrings( self.module.getCodebase() )

    for entry in string_list:

        if "\\Device\\" in entry[2]:
            self.imm.log( "Possible match at address: 0x%08x" % entry[0], address = entry[0] )

            self.deviceNames.append( entry[2].split("\"")[1] )

    self.imm.log("Possible device names: %s" % self.deviceNames)

    return self.deviceNames

Этот код просто получает список всех строк из драйвера, а затем перебирает его, ища строку «\Device\», которая возможно является той строкой, которую драйвер будет использовать для регистрации символической ссылки, чтобы программа режима пользователя могла получить его дескриптор. Для проверки этого утверждения, попробуйте загрузить драйвер «C:\WINDOWS\System32\beep.sys» в Immunity Debugger. После того, как он будет загружен, используйте PyShell отладчика и введите следующий код:

*** Immunity Debugger Python Shell v0.1 ***
Immlib instanciated as 'imm' PyObject
READY.
>>> import driverlib
>>> driver = driverlib.Driver()
>>> driver.getDeviceNames()
['\\Device\\Beep']
>>>

Вы видите, что мы обнаружили действительное имя устройства «\Device\Beep», используя для этого всего три строки кода, без необходимости просмотра таблицы строк или прокручивания строки за строкой дизассемблерного листинга. Теперь давайте перейдем к обнаружению основной функции обрабатывающей IOCTL-коды и непосредственно к обнаружению самих IOCTL-кодов, которые поддерживает драйвер.

10.3.2 Поиск процедуры обработки IOCTL-кодов (IOCTL Dispatch Routine)

Любой драйвер, который реализует интерфейс IOCTL, должен иметь процедуру обработки IOCTL-кодов, которая обрабатывает различные IOCTL-запросы. Когда драйвер загружается, первой функцией, которая получает управление – является DriverEntry. Скелет, процедуры DriverEntry, для драйвера, который поддерживает обработку IOCTL-кодов, показан в Листинге 10-3:

Листинг 10-3: Исходный код простой процедуры DriverEntry

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
        UNICODE_STRING uDeviceName;
        UNICODE_STRING uDeviceSymlink;
        PDEVICE_OBJECT gDeviceObject;

        RtlInitUnicodeString( &uDeviceName, L"\\Device\\GrayHat" );
        RtlInitUnicodeString( &uDeviceSymlink, L"\\DosDevices\\GrayHat" );

        // Register the device
        IoCreateDevice( DriverObject, 0, &uDeviceName, FILE_DEVICE_NETWORK, 0, FALSE, &gDeviceObject );

        // We access the driver through its symlink
        IoCreateSymbolicLink(&uDeviceSymlink, &uDeviceName);

        // Setup function pointers
        DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IOCTLDispatch;

        DriverObject->DriverUnload = DriverUnloadCallback;

        DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverCreateCloseCallback;

        DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverCreateCloseCallback;


        return STATUS_SUCCESS;
}

Это очень простая процедура DriverEntry, но она дает вам понимание того, как производят инициализацию большинство устройств.

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IOCTLDispatch

Эта строка говорит драйверу, что функция «IOCTLDispatch» будет обрабатывать все IOCTL-запросы. Когда драйвер скомпилирован, эта строка C-кода будет преобразована в следующую псевдо-ассемблерную строку:

mov dword ptr [REG+70h], CONSTANT

Вы будите видеть очень специфический набор инструкций, где массив MajorFunction (REG в ассемблерном коде) будет ссылаться на смещение 0x70, а указатель на функцию (CONSTANT в ассемблерном коде) будет храниться рядом. Используя эти инструкции, мы можем определить, где размещена процедура обработки IOCTL-кодов, и это именно то место, где мы можем начать поиски различных IOCTL-кодов. Поиск этой функции обработки осуществляется библиотекой driverlib с использованием кода из Листинга 10-4.

Листинг 10-4: Функция поиска процедуры обработки IOCTL-кодов (если имеется)

def getIOCTLDispatch( self ):
    search_pattern = "MOV DWORD PTR [R32+70],CONST"

    dispatch_address = self.imm.searchCommandsOnModule( self.module.getCodebase(), search_pattern )

    # We have to weed out some possible bad matches
    for address in dispatch_address:

        instruction = self.imm.disasm( address[0] )

        if "MOV DWORD PTR" in instruction.getResult():
            if "+70" in instruction.getResult():
                self.IOCTLDispatchFunctionAddress = instruction.getImmConst()
                self.IOCTLDispatchFunction = self.imm.getFunction( self.IOCTLDispatchFunctionAddress )
                break

    # return a Function object if successful
    return self.IOCTLDispatchFunction

Этот код опирается на мощное API поиска отладчика Immunity Debugger, чтобы найти все возможные совпадения, соответствующие нашим критериям поиска. После того, как найдено совпадение, мы возвращаем объект функции, который предоставляет процедуру обработки IOCTL-кодов, нашей ищейке, которая начнет поиск допустимых IOCTL-кодов.

Теперь давайте непосредственно посмотрим на процедуру обработки IOCTL-кодов и рассмотрим некоторые простые эвристики, чтобы попытаться найти все IOCTL-коды, которые поддерживает устройство.

10.3.3 Определение поддерживаемых IOCTL-кодов

Процедура обработки IOCTL, обычно выполняет различные действия, в зависимости от значения кода, которое в нее было предано. Мы хотим иметь возможность определить все возможные IOCTL-коды, поэтому пойдем на все трудности, чтобы найти их значения. Давайте сначала рассмотрим, как будет выглядеть исходный код на языке С, для скелета процедуры обработки IOCTL-кодов. После этого мы будем видеть, как декодировать блок, чтобы получить значения IOCTL-кодов. Листинг 10-5 показывает типичную процедуру обработки IOCTL-кодов.

Листинг 10-5: Упрощенная процедура обработки IOCTL, поддерживающая три IOCTL-кода (0x1337, 0x1338, 0x1339)

NTSTATUS IOCTLDispatch( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp )
{
        ULONG FunctionCode;
        PIO_STACK_LOCATION IrpSp;

        // Setup code to get the request initialized
        IrpSp = IoGetCurrentIrpStackLocation(Irp);
         (#1): FunctionCode = IrpSp->Parameters.DeviceIoControl.IoControlCode;

        // Once the IOCTL code has been determined, perform a
        // specific action

         (#2): switch(FunctionCode)
        {
                case 0x1337:
                        // ... Perform action A
                case 0x1338:
                        // ... Perform action B
                case 0x1339:
                        // ... Perform action C
        }

        Irp->IoStatus.Status = STATUS_SUCCESS;
        IoCompleteRequest( Irp, IO_NO_INCREMENT );

        
        return STATUS_SUCCESS;
}

Как только IOCTL-код был получен (#1), то довольно часто можно видеть оператор «switch {}» на месте (#2), чтобы определить какое действие должен выполнить драйвер, основываясь на полученном значение IOCTL-кода. Существует несколько различных способов преобразования этого кода в ассемблерный блок; взгляните на Листинг 10-6 в качестве примера.

Листинг 10-6: Пара дизассемблерных интерпретаций оператора switch{}

// Series of CMP statements against a constant
CMP DWORD PTR SS:[EBP-48], 1339    # Test for 0x1339
JE 0xSOMEADDRESS                   # Jump to 0x1339 action
CMP DWORD PTR SS:[EBP-48], 1338    # Test for 0x1338
JE 0xSOMEADDRESS
CMP DWORD PTR SS:[EBP-48], 1337    # Test for 0x1337
JE 0xSOMEADDRESS

// Series of SUB instructions decrementing the IOCTL code
MOV ESI, DWORD PTR DS:[ESI + C]    # Store the IOCTL code in ESI
SUB ESI, 1337                      # Test for 0x1337
JE 0xSOMEADDRESS                   # Jump to 0x1337 action
SUB ESI, 1                         # Test for 0x1338
JE 0xSOMEADDRESS                   # Jump to 0x1338 action
SUB ESI, 1                         # Test for 0x1339
JE 0xSOMEADDRESS                   # Jump to 0x1339 action

Может быть много способов, которыми оператор «switch{}», может быть преобразован в ассемблерный код, но существует два наиболее распространенных, которые я встречал. В первом случае, где мы видим ряд инструкций CMP, просто ищем константу, которая сравнивается с передаваемым IOCTL-кодом. Эта константа должна быть действительным IOCTL-кодом, который поддерживает драйвер. Во втором случае, мы ищем серию инструкций SUB, сопровождаемых некоторым типом условных инструкций JMP. Ключевым в этом случая является нахождение начальной оригинальной константы.

SUB ESI, 1337

Эта строка говорит нам, что самым наименьшим поддерживаемым IOCTL-кодом является 0x1337. Исходя из этого, значение, эквивалентное сумме каждой последующей инструкции SUB, которое мы видим, мы добавляем к нашей основной константе, тем самым получая остальные IOCTL-коды. Взгляните на хорошо прокомментированный код функции getIOCTLCodes() внутри “Libs\driverlib.py” вашей директории установки Immunity Debugger. Она автоматически проходит через процедуру обработки IOCTL-кодов и определяет, какие IOCTL-коды, поддерживает тестируемый драйвер; вы можете видеть некоторые из этих эвристик в действии!

Теперь, когда мы знаем, как driverlib делает часть нашей грязной работы, давайте воспользуемся ее услугами! Мы будем использовать driverlib, чтобы вытянуть имена устройств и поддерживаемые IOCTL-коды из драйвера и сохраним полученные результаты с помощью модуля Python pickle [6]. Затем напишем IOCTL-фаззер, который будет использовать результаты из pickle-файла, для фаззинга различных IOCTL-кодов, которые поддерживает драйвер. Это не только увеличит покрытие исследуемого драйвера, но и позволит фаззеру работать в течении неопределенного времени. Помимо этого, для проведения фаззинга, нам не нужно взаимодействовать с программой режима пользователя. Давайте приступим к фаззингу.

10.4 Создание драйвер-фаззера

Первый шаг заключается в создании нашего IOCTL-дампера, который будет выполнен в виде PyCommand, для запуска его в Immunity Debugger. Откройте новый Python файл, назовите его и ioctl_dump.py и введите следующий код:

ioctl_dump.py

import pickle
import driverlib
from immlib import *

def main( args ):
    ioctl_list = []
    device_list = []

    imm = Debugger()
    driver = driverlib.Driver()

    # Grab the list of IOCTL codes and device names
     (#1): ioctl_list = driver.getIOCTLCodes()
    if not len(ioctl_list):
        return "[*] ERROR! Couldn't find any IOCTL codes."

     (#2): device_list = driver.getDeviceNames()
    if not len(device_list):
        return "[*] ERROR! Couldn't find any device names."

    # Now create a keyed dictionary and pickle it to a file
     (#3): master_list = {}
    master_list["ioctl_list"] = ioctl_list
    master_list["device_list"] = device_list

    filename = "%s.fuzz" % imm.getDebuggedName()
    fd = open( filename, "wb" )

     (#4): pickle.dump( master_list, fd )
    fd.close()

    return "[*] SUCCESS! Saved IOCTL codes and device names to %s" % filename

Эта PyCommand довольно проста: она получает список IOCTL-кодов (#1), список имен устройств (#2), помещает обоих в словарь (#3) и сохраняет словарь в файл (#4). Просто загрузите тестируемый драйвер в Immunity Debugger и запустите PyCommand следующим образом:

!ioctl_dump

Pickle-файл будет сохранен в директории Immunity Debugger.

Теперь, когда у нас есть список имен устройств и набор IOCTL-кодов, давайте напишем простой фаззер использующий их! Важно знать, что этот фаззер ищет только багги связанные с повреждением и переполнением памяти, но он может быть легко расширен, чтобы иметь более широкое покрытие других классов ошибок.

Откройте новый Python файл, назовите его my_ioctl_fuzzer.py и введите следующий код.

my_ioctl_fuzzer.py

import pickle
import sys
import random

from ctypes import *

kernel32 = windll.kernel32

# Defines for Win32 API Calls
GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000
OPEN_EXISTING = 0x3

(#1): # Open the pickle and retrieve the dictionary
fd = open(sys.argv[1], "rb")
master_list = pickle.load(fd)
ioctl_list = master_list["ioctl_list"]
device_list = master_list["device_list"]
fd.close()

# Now test that we can retrieve valid handles to all
# device names, any that don't pass we remove from our test cases
valid_devices = []

(#2): for device_name in device_list:

    # Make sure the device is accessed properly
    device_file = u"\\\\.\\%s" % device_name.split("\\")[::-1][0]

    print "[*] Testing for device: %s" % device_file

    driver_handle = kernel32.CreateFileW(device_file,GENERIC_READ|GENERIC_WRITE,0,None,OPEN_EXISTING,0,None)

    if driver_handle:

        print "[*] Success! %s is a valid device!"

        if device_file not in valid_devices:
            valid_devices.append( device_file )

            kernel32.CloseHandle( driver_handle )
        else:
            print "[*] Failed! %s NOT a valid device."

if not len(valid_devices):
    print "[*] No valid devices found. Exiting..."
    sys.exit(0)

# Now let's begin feeding the driver test cases until we can't bear
# it anymore! CTRL-C to exit the loop and stop fuzzing
while 1:

    # Open the log file first
    fd = open("my_ioctl_fuzzer.log","a")

    # Pick a random device name
     (#3): current_device = valid_devices[random.randint(0, len(valid_devices)-1 )]
    fd.write("[*] Fuzzing: %s\n" % current_device)

    # Pick a random IOCTL code
     (#4): current_ioctl = ioctl_list[random.randint(0, len(ioctl_list)-1)]
    fd.write("[*] With IOCTL: 0x%08x\n" % current_ioctl)

    # Choose a random length
     (#5): current_length = random.randint(0, 10000)
    fd.write("[*] Buffer length: %d\n" % current_length)

    # Let's test with a buffer of repeating As
    # Feel free to create your own test cases here
    in_buffer = "A" * current_length

    # Give the IOCTL run an out_buffer
    out_buf = (c_char * current_length)()
    bytes_returned = c_ulong(current_length)

    # Obtain a handle
    driver_handle = kernel32.CreateFileW(device_file, GENERIC_READ|GENERIC_WRITE,0,None,OPEN_EXISTING,0,None)

    fd.write("!!FUZZ!!\n")
    # Run the test case
    kernel32.DeviceIoControl( driver_handle, current_ioctl, in_buffer,
                                                current_length, byref(out_buf),
                                                current_length, byref(bytes_returned),
                                                None )

    fd.write( "[*] Test case finished. %d bytes returned.\n\n" % bytes_returned.value )

    # Close the handle and carry on!
    kernel32.CloseHandle( driver_handle )
    fd.close()

Мы начинаем с распаковки словаря IOCTL-кодов и имен устройств из pickle-файла (#1). Затем проверяем, что мы можем получить дескрипторы ко всем устройствам в списке (#2). Если мы не можем получить дескриптор конкретного устройства, то удаляем его из списка. Затем просто выбираем случайное устройство (#3), случайный IOCTL-код (#4) и создаем буфер случайно длины (#5). После чего отправляем IOCTL-код драйверу и переходим к следующему тест-кейсу.

Чтобы использовать фаззер, просто передайте ему путь к файлу с тест-кейсами и дайте ему поработать! Примером может быть следующая команда:

C:\>python.exe my_ioctl_fuzzer.py i2omgmt.sys.fuzz

Если ваш фаззер действительно обрушит машину, то будет довольно очевидно, какой IOCTL-код вызвал сбой, потому что ваш log-файл покажет вам последний выполненный IOCTL-код. Листинг 10-7 показывает некоторый вывод, в качестве успешного примера фаззинга против неназванного драйвера.

Листинг 10-7: Результаты успешного фаззинга отображенные в логах

(*) Fuzzing: \\.\unnamed
(*) With IOCTL: 0x84002019
(*) Buffer length: 3277
!!FUZZ!!
(*) Test case finished. 3277 bytes returned.

(*) Fuzzing: \\.\unnamed
(*) With IOCTL: 0x84002020
(*) Buffer length: 2137
!!FUZZ!!
(*) Test case finished. 1 bytes returned.

(*) Fuzzing: \\.\unnamed
(*) With IOCTL: 0x84002016
(*) Buffer length: 1097
!!FUZZ!!
(*) Test case finished. 1097 bytes returned.

(*) Fuzzing: \\.\unnamed
(*) With IOCTL: 0x8400201c
(*) Buffer length: 9366
!!FUZZ!!

Очевидно, что последний IOCTL-код (0x8400201c) вызвал сбой, потому что мы не видим никаких дальнейших записей в log-файл. Я надеюсь, что вы имели такую же большую удачу, во время фаззинга драйвера, что и я! Это очень простой фаззер; не стесняйтесь расширять тест-кейсы – всегда, когда считаете это нужным. Возможным улучшением, могла бы быть передача буфера случайного размера, но с установленным параметром InBufferLength или OutBufferLength какой-нибудь другой длины, отличающегося от действительного размера буфера который вы передаете. Пойдите и уничтожьте все драйверы на своем пути!

© Translated by Prosper-H from r0 Crew

[1] See Kostya Kortchinsky, “Exploiting Kernel Pool Overflows” (2008):
http://immunityinc.com/downloads/KernelPool.odp

[2] See Justin Seitz, “I2OMGMT Driver Impersonation Attack” (2008):
http://immunityinc.com/downloads/DriverImpersonationAttack_i2omgmt.pdf

[3] See the MSDN CreateFile Function:
http://msdn.microsoft.com/en-us/library/aa363858.aspx

[4] See MSDN DeviceIoControl Function:
http://msdn.microsoft.com/en-us/library/aa363216(VS.85).aspx

[5] To download Wireshark go to:
http://www.wireshark.org/

[6] For more information on Python pickles, see:
http://www.python.org/doc/2.1/lib/module-pickle.html