Введение во внутреннее устройство Windows

Функции для управления устройствами

Показывать лекцию целиком

Задание 1. Запустить приложение ReadFile.exe, установить точку останова на функции NtReadFile.

Указания к выполнению.

1. В виртуальной машине запустите приложение ReadFile.exe, разработанное в лабораторной работе 5 "Безопасность в Windows".

2. Когда сработает точка останова и управление перейдет к отладчику WinDbg, нужно установить точку останова на функции NtReadFile для процесса ReadFile.exe.

Для этого сначала следует определить адрес объекта EPROCESS для процесса ReadFile.exe при помощи команды:

!process 0 0 ReadFile.exe

В данном случае адрес равен 0x8222F630.

Установить точку останова для функции NtReadFile для процесса ReadFile.exe можно при помощи следующей команды:

bp /p 8222F630 nt!NtReadFile

Проверьте, что команда выполнена верно: в меню Edit отладчика выберите пункт Breakpoints…:

Замечание. После перезапуска процесса ReadFile.exe его адрес может поменяться. В этом случае придется удалить старую точку останова и повторить нахождение адреса объекта EPROCESS и установки новой точки останова.

3. Продолжите выполнение приложения ReadFile.exe (нажмите в отладчике F5). Должна сработать точка останова и открыться исходный код функции NtReadFile.

4. Удостоверьтесь, что поток, вызвавший функцию NtReadFile, действительно принадлежит процессу ReadFile.exe: используя клавишу F10 дойдите в отладчике до строки 112. В этом месте вызывается функция PsGetCurrentThread – получение указателя на текущий поток (посмотрите, как эта функция реализована). Чтобы произошел её вызов ещё раз нажмите F10.

После вызова функции PsGetCurrentThread в переменной CurrentThread содержится адрес объекта ETHREAD текущего потока. Этот адрес можно узнать либо наведя указатель мыши на переменную CurrentThread, либо посмотрев её значение в окне Locals (Alt+3):

В данном примере адрес структуры ETHREAD равен 0x81FB6A10.

Чтобы найти процесс, которому принадлежит текущий поток, найдите поле ThreadsProcess структуры ETHREAD, расположенной по адресу 0x81FB6A10. Это можно сделать либо введя команду:

dt ethread 81FB6A10

либо развернув переменную CurrentThread в окне Locals. На рисунке ниже показан первый способ:

Адрес структуры EPROCESS процесса-владельца текущего потока равен 0x8222F630.

Далее введите команду:

dt eprocess 8222F630

и в поле ImageFileName будет записано имя исполняемого файла:

Ту же информацию можно получить последовательно раскрывая поля структур в окне Locals.

5. Обратите внимание на функцию ObReferenceObjectByHandle в строке 121. Эта функция по заданному дескриптору (handle) возвращает указатель на объект. В данном случае функция ObReferenceObjectByHandle по известному дескриптору файла (параметр FileHandle) возвращает указатель на объект FILE_OBJECT (параметр fileObject).

Кроме того, данная функция проверяет права доступа потока на чтение файла. Если прав недостаточно, функция сообщает об этом, возвращая соответствующее значение (STATUS_ACCESS_DENIED) переменной status.

Задание 2. Исследовать параметры функции NtReadFile.

Указания к выполнению.

1. Функция NtReadFile имеет следующие параметры:

  • FileHandle – дескриптор файла, информацию из которого требуется прочитать;
  • Event – дескриптор объекта "Событие", которое может быть установлено при завершении операции чтения (необязательный параметр);
  • ApcRoutine, ApcContext – зарезервированные неиспользуемые параметры;
  • IoStatusBlock – указатель на структуру IO_STATUS_BLOCK, в которой будет содержаться статус операции после завершения чтения, в частности, количество реально прочитанных байт (см. файл public\sdk\inc\ntioapi.h, строка 451);
  • Buffer – указатель на область памяти (буфер), в которую будут помещены прочитанные данные;
  • Length – размер буфера в байтах;
  • ByteOffset – определяет для операции чтения смещение в байтах относительно начала файла;
  • Key – необязательный параметр, используемый в случае блокировки файла.
  • Значения параметров можно посмотреть в окне Locals:

    Дескриптор файла равен 1C, адрес буфера равен 0x0012FF14, размер буфера – 80 байт (0x50), смещение в файле равно нулю.

    Посмотрим, что находится в данный момент в буфере:

    dd 12FF14
    

    Пока данные не прочитаны, буфер пуст.

    По дескриптору файла можно узнать, что это за файл. Воспользуемся следующей командой:

    !handle 1C f 8222F630
    

    где 1C – дескриптор файла;

    f – отображение полной информации об объекте;

    8222F630 – адрес объекта EPROCESS процесса ReadFile.exe.

    Как видно из рисунка, дескриптор 1C описывает файл input.txt.

    Задание 3. Исследовать структуру FILE_OBJECT.

    Указания к выполнению.

    1. Как уже отмечалось, в строке 121 вызывается функция ObReferenceObjectByHandle, которая в переменной fileObject возвращает указатель на объект FILE_OBJECT для файла input.txt.

    Вследствие оптимизации программного кода ядра значение переменной fileObject невозможно напрямую посмотреть в отладчике (например, окно Locals не показывает эту переменную, наведение указателя мыши также не дает результата). Можно определить адрес объекта FILE_OBJECT, исследуя дизассемблированный код (окно Disassembly отладчика), но мы поступим проще.

    В предыдущем задании при помощи команды

    !handle 1C f 8222F630
    

    нам удалось по дескриптору определить имя файла. В результате этой же команды выводится и адрес объекта FILE_OBJECT:

    Таким образом, адрес объекта FILE_OBJECT для файла input.txt равен 0x81FFF2F8.

    2. Изучим информацию, содержащуюся в объекте FILE_OBJECT.

    Для этого можно воспользоваться двумя способами.

    Способ 1. При помощи команды:

    dt FILE_OBJECT 81FFF2F8
    

    Поле Type равно 5 – согласно константам, определенным в файле base\ntos\inc\io.h (строка 35), это тип IO_TYPE_FILE.

    В структуре присутствует адрес объекта DEVICE_OBJECT, с которым связан данный файл (см. лекцию 15 "Управление устройствами", рис. 15.1 и рис. 15.2).

    Способ 2. При помощи команды:

    !fileobj 81FFF2F8
    

    Здесь присутствует та же информация, что и в первом случае.

    Задание 4. Исследовать структуру DEVICE_OBJECT.

    Указания к выполнению.

    1. В предыдущем задании в информации, выводимой об объекте FILE_OBJECT, присутствует адрес объекта DEVICE_OBJECT, на котором находится файл. Этот адрес равен 0x823ADE00.

    Чтобы посмотреть информацию об объекте DEVICE_OBJECT по этому адресу, можно опять воспользоваться либо общей командой отображения типов данных dt, либо специальной командой !devobj. Продемонстрируем результат второго способа:.

    !devobj 823ADE00
    

    Данная команда сообщает имя драйвера, который управляет устройством (\Driver\Ftdisk – диспетчер томов) и адрес объекта драйвера DRIVER_OBJECT (0x82373690). Кроме того, из рисунка видно, что данное устройство отвечает за том HarddiskVolume1 (диск C).

    Задание 5. Исследовать структуру DRIVER_OBJECT.

    Указания к выполнению.

    1. В предыдущем задании мы узнали адрес объекта драйвера, который отвечает за устройство HarddiskVolume1. Информацию об этом драйвере можно получить либо при помощи команды:

    dt DRIVER_OBJECT 82373690
    

    либо при помощи команды:

    !drvobj 82373690
    

    Воспользуемся вторым способом:

    На рисунке показаны имя драйвера и объекты-устройства, которыми данный драйвер управляет. В частности, кроме устройства HarddiskVolume1, драйвер управляет устройством, которое описывается объектом DEVICE_OBJECT, расположенным по адресу 0x82373278.

    2. Дополнительную информацию о драйверах можно получить, воспользовавшись утилитой Process Explorer. В ней следует выбрать процесс System и отобразить для него DLL (меню View – пункт Lower Pane View – DLL):

    Задание 6. Исследовать структуру IRP.

    Указания к выполнению.

    1. Продолжим трассировку функции NtReadFile (мы остановились на строке 121 – вызов функции ObReferenceObjectByHandle) – клавиша F10.

    Обратите внимание на переменную deviceObject (строка 135). Значение, которое в ней оказывается после вызова функции IoGetRelatedDeviceObject не совпадает с полученным нами в задании 3. Дело в том, что запрос к файлу проходит несколько драйверов на разных уровнях, и в переменную deviceObject помещается ссылка на драйвер верхнего уровня.

    Самостоятельно исследуйте функцию IoGetRelatedDeviceObject, чтобы понять, каким образом получается ссылка на драйвер.

    2. Найдите строку 517 – здесь происходит вызов функции IopAllocateIrp, которая выделяет память (но не заполняет) под структуру IRP. Поставьте в этой строке точку останова (нажмите F9):

    И продолжите выполнение кода функции – нажмите F5. Управление должно перейти в точку останова на строку 517.

    3. После выполнения функции IopAllocateIrp (нажмите клавишу F10) узнайте адрес переменной irp и просмотрите её содержимое либо при помощи команды:

    dt irp address
    

    либо при помощи команды:

    !irp address
    

    Предположим, адрес переменной irp равен 0x81efc008:

    Из рисунка видно, что структура IRP пустая, и в ней 9 блоков стека (структур типа IO_STACK_LOCATION), 10-й блок, не заполненный, является текущим. Количество блоков стека указывается в поле StackSize структуры DEVICE_OBJECT, а определяется это количество системой; причем различаются малые IRP с одним блоком стека и большие IRP, количество блоков стека которых варьируется (подробнее см. [5, стр. 595]). В нашем случае мы имеем дело с большим IRP.

    4. Далее по коду функции происходит заполнение структуры IRP (просмотрите заполнение полей самостоятельно).

    Обратите внимание на поля UserIosb (блок статуса, строка 542), majorFunction (номер основной функции; в случае чтения он равен константе IRP_MJ_READ = 3, строка 558), UserBuffer (буфер чтения, строка 697).

    5. В строке 725 происходит вызов функции IopSynchronousServiceTail, которая помещает сформированный IRP в очередь потока.

    Перед вызовом этой функции просмотрите структуру IRP:

    !irp 0x81efc008
    

    Из рисунка видно, что последний блок стека сейчас заполнен:

  • MajorFunction = 3 (константа IRP_MJ_READ);
  • FileObject = 81FFF2F8 (адрес объекта FILE_OBJECT для файла input.txt);
  • Args = 50 (размер буфера).
  • Чтобы посмотреть структуру IO_STACK_LOCATION для данного блока стека, нужно из адреса текущего блока стека (выделен на рисунке) вычесть 0x24 (размер блока стека):

    dt IO_STACK_LOCATION 0x81EFC1BC–0x24
    

    Задание 7. Исследовать результаты операции чтения.

    Указания к выполнению.

    1. Выполните вызов функции IopSynchronousServiceTail (нажмите один раз F10).

    Когда управление вернется к отладчику, операция чтения будет выполнена.

    2. Просмотрите содержимое буфера чтения.

    Адрес буфера равен 0x0012FF14 (переменная Buffer, см. Задание 2 данной лабораторной работы). Просмотреть его содержимое (в ASCII кодах) можно командой:

    da 0x0012FF14
    

    или (в байтах) командой:

    db 0x0012FF14
    

    3. Просмотрите содержимое блока статуса – переменную IoStatusBlock (тип IO_STATUS_BLOCK). Найти её адрес можно в окне Locals.

    Поле Status, равное нулю, говорит о том, что операция выполнена успешно. В поле Information содержится количество прочитанных байт (0x20 = 32 байта).

    Задания для самостоятельного выполнения

    Задание 1. Выполнить трассировку функции NtWriteFile и найдите отличия от функции NtReadFile.

    Указания к выполнению.

    1. Для исследования функции ядра NtWriteFile создайте проект на основе примера из MSDN для WinAPI функции WriteFile.

    Задание 2. Исследовать параметры безопасности объектов устройств.

    Указания к выполнению.

    1. В объекте DEVICE_OBJECT имеется поле SecurityDescriptor. Требуется по методике, изложенной в лабораторной работе 5 "Безопасность в Windows", исследовать дескриптор безопасности объекта устройства.

    Вернуться к учебному плану