Путешествие из Kernel32 в HAL

Дата публикации 25 фев 2017 | Редактировалось 15 апр 2017
Оригинал статьи http://www.debasish.in/2014/02/reversing-tiny-built-in-windows-kernel.html


Путешествие из Kernel32 в HAL

В этом посте я собираюсь изучить ядро Wiindows путем небольшого реверса встроенного в ядро модуля. Если вы когда-нибудь разрабатывали ядерный драйвер (модуль), это будет очень просто для понимания. Если вы не знакомы с тем, как работают драйвера, то на Codeproject.com есть немного хороших ресурсов, чтобы начать.

Но прежде чем мы сможем начать реверсинг основного компонента, посмотрите на диаграмму, показанную ниже.

[​IMG]

На картинке выше, зеленые элементы являются компонентами пользовательского режима (Ring3). На диаграмме показано, как на самом деле apps.exe (приложение пользовательского режима) вызывает драйвер режима ядра. Мы постараемся взять каждую из секций, указанных в приведенной выше диаграмме и попытаться отреверсить их, чтобы понять, как все работает.


Выбор целевого драйвера:

Всегда лучше начинать с простых вещей. В этой статье мы отреверсим драйвер Beep. Этот компонент также обеспечивает некоторую вспомогательную информацию в реестре. Это, вероятно, самый маленький встроенный модуль ядра в ОС Windows. Он имеет только 6 подпрограмм.


Создаем apps.exe

Мы начнем с очень простого кода на Cи.

Код (C):
  1. #include<windows.h>
  2. int main(){
  3. Beep( 50, 750 );
  4. return 0;
  5. }
Код очень простой и прямолинейный. Функция гудка находится в Kernel32.dll

BOOL WINAPI Beep(
_In_ DWORD dwFreq,
_In_ DWORD dwDuration
);


После компиляции получится файл, который должен генерировать звуковой сигнал.

+ То же самое на FASM :

Код (ASM):
  1. format PE
  2.  
  3. include ".\include\win32a.inc"
  4.  
  5.  
  6. push 500 ; duration
  7. push 5000  ;freq
  8.  
  9. call [beep]
  10.  
  11. ret
  12.  
  13.  
  14. section ".imp" import readable
  15.  
  16.  
  17. library kern,"kernel32.dll"
  18. import kern,beep,"Beep"

Нахождение Драйвера/Устройства:

Ниже на скриншоте можно увидеть, драйвер Звукового сигнала фактически создал устройство под названием "Звуковой сигнал". Также вы можете увидеть много другой информации, такой как основные функции, поддерживаемые этим драйвером, и многое другое.

программа DeviceTree 2.30 https://www.osronline.com/article.cfm?article=97


[​IMG]


Прослушка всех пакетов запроса ввода-вывода (IRP) к устройству Beep с помощью утилиты IRP Tracker:

IRP Tracker очень крутая и мощная утилита. С ее помощью можно прослушивать шлюз Ring3-Ring0 и видеть детали сообщений, передаваемых из пользовательского режима процесса к драйверу ядра. С помощью этого инструмента мы будем прослушивать все запросы, которые Beep.exe отправляет из Ring3 к Ring0 для получения звукового сигнала.

Для начала прослушки мы должны предоставить утилите имя драйвера, который мы хотим прослушать. Выбираем пункт “драйвер” в меню “File”, пишем имя драйвера. В данном случае нас интересует только драйвер "Beep". Теперь мы начали прослушку всех сообщений между режимом пользователя и ядром. Теперь пришло время запустить Beep.exe, что мы скомпилировали. При выполнении файла Beep.exe вы увидите несколько новых записей в окне IRP Tracker.


[​IMG]


Теперь, если вы посмотрите на колонку IRP Address Sequence Number, вы увидите что первая запись является NtCreateFile() и последняя запись — ntClose(). В промежутках между ними вы можете увидеть вызовы ntDeviceIoControlFile.

(прим. перев. — для этого нужно выбрать Display NTAPI Calls в меню Options, перед запуском Beep.exe)

Теперь, если вы посмотрите на колонку Major Function, вы найдете "DEVICE_CONTROL". Если посмотреть в MSDN, вы увидите, что этот API фактически используется для передачи IOCTL кодов из пользовательского режима в драйвер ядра.

NTSTATUS WINAPI NtDeviceIoControlFile(
_In_ HANDLE FileHandle,
_In_ HANDLE Event,
_In_ PIO_APC_ROUTINE ApcRoutine,
_In_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ ULONG IoControlCode,
_In_ PVOID InputBuffer,
_In_ ULONG InputBufferLength,
_Out_ PVOID OutputBuffer,
_In_ ULONG OutputBufferLength
);


( https://msdn.microsoft.com/ru-ru/library/windows/desktop/aa363216(v=vs.85).aspx )

Утилита IRP Tracker также может предоставить нам IOCTL код, который приложение пользовательского режима посылает драйверу режима ядра.


[​IMG]


Мы можем видеть, оно посылает IOCTL код 0x10000 (BEEP_SET) этому устройству. Имейте это ввиду.


Реверсим Beep() [Kernel32.dll]

Чтобы отреверсить процедуру Beep, загрузим Kernel32.dll в IDA Pro. После того, как оно загрузится (в процессе загрузки dll-ки в IDA, последняя предложит скачать символы с сайта MS, соглашайтесь. И лучше скопировать kernel32 куда-нибудь в другую папку, и уже оттуда дать его IDA — прим.перев.), прыгните в начало процедуры Beep. Вы должны увидеть что-то наподобие этого:

[​IMG]


Как мы видим, оно пытается установить связь с устройством Beep (\\Device\\Beep) с помощью вызова NtCreateFile. При коммуникациях с драйвером режима ядра, любое приложение пользовательского режима использует NtCreateFile. Если вызов прошел успешно, функция возвращает один хэндл для целевого устройства. Используя этот хендл, мы можем читать / писать в это устройство.

Если мы пройдем немного дальше внутрь функции Beep!Kernel32, то увидим как она пытается проверить несколько параметров, передаваемых ей, и после этого она вызывает NtDeviceIoControlFile().


[​IMG]



Надеюсь, вы сможете отследить ход выполнения по рисункам.

Если вспомнить, это та же последовательность вызова функции, что вы видели в утилите IRP tracker. Нам уже известно, что этот NtDeviceIoControlFile используется для отправки кодов IOCTL драйверу ядра.


Реверсим ntdll.dll [NtDeviceIoControlFile()]

Теперь придется посмотреть ближе на вызов NtDeviceIoControlFile. Мы видели в MSDN, что шестой параметр NtDeviceIoControlFile является кодом IOCTL.


NTSTATUS WINAPI NtDeviceIoControlFile(
_In_ HANDLE FileHandle,
_In_ HANDLE Event,
_In_ PIO_APC_ROUTINE ApcRoutine,
_In_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ ULONG IoControlCode,
_In_ PVOID InputBuffer,
_In_ ULONG InputBufferLength,
_Out_ PVOID OutputBuffer,
_In_ ULONG OutputBufferLength
);



Давайте проверим это в коде функции NtDeviceIoControlFile:


[​IMG]


На изображении выше видно, сначала происходит загрузка значения 10000h в EBX и затем передача его в NtDeviceControlFile(). Это тот же самый IOCTL код, что мы видели в утилите IRP Tracker.



Теперь давайте загрузим Beep.exe в Immunity Debugger и выставим точку останова на NtDeviceIoControlFile().

( В отладчике нажимаем Alt+E, в появившемся окне Executable Modules жмем ПКМ на ntdll.dll, выбираем View Names. В окне Names ищем/ набираем с клавиатуры начало названия экспортируемой функции ZwDeviceIoControlFile, ПКМ, устанавливаем точку останова, выбрав Toggle Breakpoint )

После установки точки останова, если мы продолжим выполнение, мы должны остановиться в этой точке, как показано на снимке экрана ниже.


[​IMG]


Если вы посмотрите на точку входа NtDeviceIoControlFile, вы увидите ниже инструкции,

MOV EAX, 42
MOV EDX, 7FFE0300
CALL DWORD PTR DS: [EDX]
RETN 28


На этой последовательности инструкций мы видим, что оно вероятно собирается совершить системный вызов.

Теперь, если мы пойдем дальше и проследуем в CALL DWORD PTR DS:[EDX], мы получим что-то вроде этого :


[​IMG]


LEA EDX,DWORD PTR SS:[ESP+8]
INT 2E


По INT 2E теперь абсолютно ясно, что оно собирается вызвать программное прерывание. При вызове которого EAX равен 0x42, так что мы можем сказать, что это номер системного вызова. Мы можем проверить это из любой утилиты для дампа SDDT. В этом случае для дампа System Service Descriptor Table я использовал инструмент ICESword.

( http://www.majorgeeks.com/files/details/icesword.html )


[​IMG]


Вы можете видеть, системный вызов 0x42 на самом деле указывает на ядерную версию NtDeviceIoControlFile() и она располагается в главном ядерном компоненте Windows, ntoskrnl.exe. После вызова прерывания, ОС переключится в режим ядра для выполнения системной службы. KiSytemServices собирается принять этот вызов.

( больше сведений: http://masters.donntu.org/2009/fvti/pohilets/library/syscalls_ru.htm )

Инструкция ‘int’ заставляет процессор выполнить программное прерывание, т.е. процессор обратится к индексу 2E в IDT (Таблицы Дескрипторов Прерываний), чтобы прочесть Дескиптор Шлюза Прерывания по этому расположению. Процессор автоматически переключается на стек режима ядра. Процессор автоматически сохраняет регистры программы пользовательского режима, SS, ESP, EFLAGS, CS и EIP в стеке режима ядра.


Ломаем драйвер Beep (beep.sys):

Итак, до этой точки мы видели, как приложение пользовательского режима посылает запрос к драйверу ядра. Теперь мы увидим, как именно ядерный драйвер обрабатывает запрос из ring3 и соответственно этому действует.
После загрузки драйвера Beep.sys в IDA, первым делом вы можете увидеть процедуру Driver Entry. DriverEntry это первая подпрограмма, вызываемая после загрузки драйвера. Так как она ответственна за инициализацию драйвера, в ней мы можем найти все функции-обработчики IOCTL. Первое, что вы увидите, происходит вызов IoCreateDevice().


[​IMG]

Процедура IoCreateDevice() создает Объект Драйвера для использования драйвером. Вы найдете вызов этой процедуры в DriverEntry любого драйвера.

NTSTATUS IoCreateDevice(
_In_ PDRIVER_OBJECT DriverObject,
_In_ ULONG DeviceExtensionSize,
_In_opt_ PUNICODE_STRING DeviceName,
_In_ DEVICE_TYPE DeviceType,
_In_ ULONG DeviceCharacteristics,
_In_ BOOLEAN Exclusive,
_Out_ PDEVICE_OBJECT *DeviceObject
);


Теперь, мы успешно создали наше устройство \Device\Beep. Продвигаясь дальше в DriverEntry, мы получим структуру наподобие такой :


[​IMG]


Эквивалентный код на Си будет чем-то наподобие:

DriverObject->DriverStartIo = sub_1051A;
DriverObject->DriverUnload = DriverUnload;
DriverObject->MajorFunction[0] = sub_1046A;
DriverObject->MajorFunction[2] = sub_104B8;
DriverObject->MajorFunction[14] = sub_10400;
DriverObject->MajorFunction[18] = sub_10354;


Больше практических идей по главным функциям IRP могут быть найдены здесь:

https://www.codeproject.com/articles/9504/driver-development-part-1-introduction-to-drivers


“Копая” дальше во все упомянутые выше обработчики IRP (sub_xxxx), было установлено, что именно sub_10354 отвечает за обработку всех IoControls. Таким образом, мы можем сделать вывод,

DriverObject→ MajorFunction [IRP_MJ_DEVICE_CONTROL] = Beephandler;

Теперь давайте прыгнем в sub_10354 в IDA и посмотрим что оно пытается сделать. Внутри sub_10354 вы увидите что-то наподобие:


[​IMG]

Вы можете обратить внимание на вызовы к следующим функциям:
KeRemoveDeviceQueue, KfRaiseIrql, IoAcquireCancelSpinLock, IoReleaseCancelSpinLock и т.д. и т.п.

На настоящий момент мы не слишком заинтересованы в любой из этих функций.Если вы заинтересованы, можете изучить MSDN. Вызов что нас интересует, это HalMakeBeep.


[​IMG]

Реверсим Hal.dll:

Слой Аппаратных Абстракий ОС Windows (HAL) реализован в Hal.dll. Аппаратные абстракции — набор подпрограмм в ПО, которые эмулируют некоторые специфические для данной платформы детали, давая программам прямой доступ к аппаратным ресурсам. Они часто позволяют программистам писать аппаратно-независимые приложения путем предоставления стандартных вызовов операционной системы (ОС) к оборудованию. Каждый тип процессора имеет определенный набор инструкций архитектуры, или ISA. Одна из основных функций компилятора — позволить программисту написать алгоритм на языке высокого уровня без необходимости заботиться о инструкциях конкретного процессора. Тогда это работа компилятора — генерировать исполняемый файл для конкретного CPU. Тот же самый тип абстракции выполнен в операционных системах, но API ОС здесь представляют примитивные операции машины, а не ISA (процессора). Это позволяет программисту использовать операции уровня операционной системы (т.е. создания/удаления задач) в своих программах, оставаясь при этом совместимым с множеством различных платформ.

— Wiki

( также, http://ru.wikipedia.org/wiki/Слой_аппаратных_абстракций )

Итак, чтобы посмотреть асм-код HalMakeBeep, нам нужно загрузить hal.dll в IDA. После загрузки, прыгнем на подпрограмму HalMakeBeep. Вы увидите много инлайн-ассемблера внутри этой функции.

[​IMG]
Каждый компьютер имеет встроенный динамик. Он может генерировать звуковые сигналы различных частот. Мы можем самостоятельно управлять динамиком, предоставляя частоту, которая определяет высоту звукового сигнала, и затем включив динамик с нужной продолжительностью для гудка.



Число, определяющее частоту, не что иное, как значение счетчика циклов ожидания. Наш компьютер использует его, чтобы определить, как долго ждать между отправкой импульсов на внутренний динамик. Более точно: меньшее значение числа/номера частоты (frequency number) вызовет более быструю отправку импульсов, и это приведет к более высокой частоте звукового сигнала.

Главным образом, мы можем взаимодействовать с контроллером динамика с помощью инструкций IN и OUT. Ниже я упомянул несколько шагов в создании звукового сигнала:

1. Во-первых, мы должны послать значение 182 в порт 43h. Тем самым мы “откроем” динамик для работы.

2. Следующее, посылаем номер частоты (frequency number) в порт 42h. Так как это 8-битный порт, мы должны использовать две команды OUT, чтобы сделать это. Отправить младший байт, а затем старший байт.

3. После этого, чтобы начать звуковой сигнал, биты 1 и 0 порта 61h должен быть установлен в 1. Так как остальные биты порта 61h имеют другие применения, они не должны быть изменены. Таким образом, вы должны сначала использовать команду IN, чтобы получить значение из порта, затем команду OR, чтобы установить два бита, а затем использовать команду OUT, чтобы отправить новое значение в порт.

4. Выждать паузу для нужной продолжительности гудка.

5. Отключить звуковой сигнал путем сброса битов 1 и 0 порта 61h в ноль. Следует помнить, что так как остальные биты этого порта не должны быть изменены, вы должны прочитать значение, только установить биты 1 и 0 в ноль, а затем вывести новое значение.

[​IMG]


Так что теперь, если мы посмотрим на подпрограмму HalMakeBeep, это должно иметь смысл. Вы можете видеть, что она делает то же самое, что только что описано выше.

Спасибо за прочтение.

Прим. перев.

Можно добавить, что Kernel32, на каждый вызов Beep через LoadLibrary грузит WINSTA.dll, в ней ищет процедуру и передает ей управление. И уже в WINSTA происходит работа с Device\Beep.

[​IMG]


Благодарности:

Mikl___ — за идею перевода

TheTrick — за помощь с брейкпоинтами


http://wasm.in — asm is wonderful

© 2017

2 6.019
_edge

_edge
Well-Known Member

Регистрация:
29 окт 2004
Публикаций:
1

Комментарии


      1. _edge 2 мар 2017
        :) Те 1.5 месяца, когда я менял себя ради труда над переводом (ибо слоупок, а тут еще и самостоятельная работа была проведена), не прошли зря. Получить лайки приятно, но самое приятное, это то, что Знание теперь со мной, это бесценно.
      2. yashechka 2 мар 2017
        Офигенская статья, пока не читал, но пробежался, пролистал её. Яша сейчас занят, готовится к празднику, поэтому все позже.