Windbg – это мощный отладчик как для юзермодных приложений, так и драйверов. Множество плагинов, команд, скриптовый язык… Все это может смутить и опытного в отладке человека. Особенно актуальна эта тема для реверсеров, которым пришлось полностью или частично перейти на 64-битные платформы. Ведь, по сути, ни один имеющийся x64-отладчик не способен сравниться с WinDbg по функционалу, поэтому речь в основном пойдет про 64-битные версии Windows, но и обладатели 32-битных смогут обнаружить для себя интересную информацию.

 

Патчим ndiskd

WinDbg славится обилием различных расширений. Загрузка расширения делается командой .load имя_расширения, а выгрузка – командой .unload.

К сожалению, довольно удобный плагин для отладки ndis-драйверов, ndiskd, отказался работать в моем Windbg 6.11.1.404 (ndiskd.dll можно найти в WDK или в директории \Debugging Tools for Windows (x64)\winxp). Например, при попытке выполнить команду

!ndiskd.interfaces

ответ был неизменным: «Can’t get offset of Link in NDIS_IF_BLOCK!». Аналогичный результат давало выполнение команд opens, protocols и остальных. Перезагрузка символов результата не давала, все структуры были в порядке. Отказываться от такого удобного расширения у меня не было желания, поэтому я решил прибегнуть к патчу, если это возможно (были бы исходники – можно было бы переписать, например). Загружаю плагин в Ida. Начнем исследование с команды interfaces. Все команды, имеющиеся в расширении – это экспортируемые функции (что очень упрощает мою задачу). То есть достаточно найти функцию interfaces в экспорте и проанализировать ее.

Что ж, приступим:

.text:0000000180012920 public interfaces
.text:0000000180012920 interfaces proc near

.text:0000000180012920 mov [rsp+arg_18], r9d
.text:0000000180012925 mov [rsp+arg_10], r8
.text:000000018001292A mov [rsp+arg_8], rdx
.text:000000018001292F mov [rsp+arg_0], rcx
.text:0000000180012934 push rbx
....
.text:00000001800129AF lea r8, [rsp+0C8h+var_2C]
.text:00000001800129B7 lea rdx, aLink ; "Link"
.text:00000001800129BE mov rcx, cs:NDIS_IF_BLOCK_NAME
; получаем смещение поля Link в структуре NDIS_IF_BLOCK
.text:00000001800129C5 call GetFieldOffset
.text:00000001800129CA test eax, eax
.text:00000001800129CC jz short loc_1800129E0
.text:00000001800129CE lea rcx, aCanTGetOffs_22
; "Can't get offset of Link in NDIS_IF_BLO"...
.text:00000001800129D5 call cs:ExtensionApis.lpOutputRoutine+4
.text:00000001800129DB jmp loc_180012BF1

Видим вывод сообщения о том, что невозможно получить смещение поля Link в структуре NDIS_IF_BLOCK_NAME. Посмотрим, что за строка NDIS_IF_BLOCK_NAME.

.text:0000000180001260 aNdisNdis_if_bl db 'ndis!NDIS_IF_BLOCK',0

Выполнение команды:

dt ndis!NDIS_IF_BLOCK

Сразу дает ответ, почему плагин рушится: «Symbol ndis!NDIS_IF_BLOCK not found». Тогда как dt ndis!_NDIS_IF_BLOCK нормально выводит структуру.

Очевидно, надо изменить строки и ссылки на них, чтобы все работало нормально. Для нашей задачи воспользуемся каким-нибудь адекватным hex-редактором.

Мне нравится 010 Editor. Исправим строки. В основном выравнивание позволяет нам безболезненно добавлять 1 символ в строку, не сдвигая остальные, но для пары строк это не так. Поэтому нужно найти ссылки на эти строки и исправить их. На каждую строку в данном случае по одной ссылке, находящейся в секции данных, на которую ссылаются инструкции, поэтому достаточно исправить ее. Например, на строку «ndis!LIST_ENTRY» ссылка выглядит так:

.data:0000000180018470 LIST_ENTRY_NAME dq offset aNdis_list_entr ; DATA XREF: pktpools+5Dr

Смотрим, что в хексе это соответствует последовательности «68 14 00 80 01 00 00 00». Теперь нужно найти ее в хекс-редакторе и исправить первые байты.

После этого небольшого патча ndiskd выводит всю нужную информацию.

 

Отлавливаем загрузку драйвера

А как остановиться на DriverEntry отлаживаемого драйвера? Можно, конечно, поставить int 3 в начале функции, но не всегда хочется уродовать код брейками. Так может быть есть альтернативное решение? Оказывается, что есть. Как я уже говорил, WinDbg – мощный отладчик с огромным количеством возможностей, поэтому грех не воспользоваться ими снова. Но для начала немного теории. Непосредственный переход на точку входа драйвера осуществляет неэкспортируемая функция IopLoadDriver. Да, ты правильно понял, – неэкспортируемая – это значит, надо иметь отладочные символы (и вообще, без них отладка ядра может превратиться в настоящий ад). В моей Vista используется такой код (его можно найти или однократным трейсом или идой, дизассемблируя ядерную IopLoadDriver):

PAGE:00000001403AC40A loc_1403AC40A:
PAGE:00000001403AC40A ; IopLoadDriver+98Bj
PAGE:00000001403AC40A mov rdx, rsi
PAGE:00000001403AC40D mov rcx, rbx
PAGE:00000001403AC410 call qword ptr [rbx+58h] ; DRIVER_OBJECT.DriverInit

Собственно, убедиться, что по смещению 0x58 в DRIVER_OBJECT находится именно DriverInit, можно в kd командой dt _DRIVER_OBJECT.
Что соответствует последовательности байт:

48 8B D6 48 8B CB FF 53 58

Тогда, чтобы найти искомый адрес, нужно ввести в командной строке WinDbg:

s nt!IopLoadDriver L2000 48 8B D6 48 8B CB FF 53 58

Где s – это команда поиска последовательности, nt!IopLoadDriver – адрес начала поиска, L2000 – количество байт, ограничивающее поиск, «48 8B D6 48 8B CB FF 53 58» – байты, которые мы ищем. Отладчик ответит что-то вроде:

fffff800`01c0940a 48 8b d6 48 8b cb ff 53-58 4c 8b 15 5e 20 dd ff H..H...SXL..^ ..

Вот и все. Теперь можно ставить точку останова на полученный адрес. В нашем случае – fffff800`01c0940a. Отныне мы будем получать управление как раз на переходе на точку входа. Этот метод довольно неплох, а что использовать – int 3 или его – решать тебе.

 

Скриптинг

Написание скриптов для WinDbg – это отдельная, очень большая тема. В рамках данной статьи я сделаю только введение. Для примера напишем простой скрипт для расстановки бряков на всех Major-функциях в DRIVER_OBJECT’е, в качестве аргумента принимает указатель на driver object выбранного драйвера.

.block
{
.catch
{
r $t0 = $arg1
.printf "Driver object at 0x%I64X\n",@$t0
r? $t1 = (nt!_DRIVER_OBJECT*)@$t0
r $t0 = @@c++(@$t1->Type)
.if(@$t0==4)
{
r $t0 = @@c++(@$t1->MajorFunction)
.for(r $t1=0;@$t1<1c;r $t1=@$t1+1)
{
r $t2 = @$t0+@$t1*8
r? $t3 = *(void**)@$t2
.printf " Function at 0x%I64X\n",@$t3
.if(@$t3!=0)
{
bp @$t3
}
}
}.else
{
.printf "Not a driver object!\n"
}
}
}

$arg1 – это первый аргумент (и так до $argN), t0-t19 – внутренние псевдо-регистры. Заметь, синтаксис очень похож на с++ (циклы for, while…). Сначала в скрипте проверяется поле DRIVER_OBJECT.Type, если оно равно четырем, то это действительно DRIVER_OBJECT (не забывай перед использованием сего скрипта подгружать символы!).

Потом получаем указатель на таблицу DRIVER_OBJECT.MajorFunction. Затем мотаем цикл по всем Major-функциям (всего их 27 — 0x1b). И как только мы находим подходящий указатель (не нулевой), ставим на него бряк (bp @$t3). Отладочные сообщения выводим функцией .printf. Скрипт написан с учетом размера 64-битного указателя (8 байт), что видно из строки «r $t2 = @$t0+@$t1*8». Чтобы скрипт заработал на 32-битной системе, нужно просто изменить размер указателя на 4. Обрати внимание – перед внутренним регистром во время операции присваивания ставится r, когда тип числовой, и r? – когда нечисловой.

Для тестирования скрипта нужно сначала получить указатель на объект-драйвер какого-нибудь драйвера с помощью команды !drvobj или иным способом. В примере ниже показано получение адреса driver object tdx.sys.

kd> !drvobj tdx
Driver object (fffffa8001ea1330) is for:
\Driver\tdx
Driver Extension List: (id , addr)

Device Object list:
fffffa8001ec72f0 fffffa8001ec52f0 fffffa8001ec32f0 fffffa8001ec12f0
fffffa8001ebf2f0 fffffa8001ebd2f0 fffffa8001eb5300

Скрипты бывают многострочные и однострочные. Многострочные запускаются из командной строки $$><файл_скрипта, а однострочные – $$<текст_скрипта (или $<).
Если мы хотим передать скрипту аргументы, то надо писать $$>a<файл_скрипта (как раз наш случай).

Теперь модифицируем скрипт выше, чтобы он ставил брейк на конкретный обработчик device object’a, а не на все сразу.

$$ $arg1 - device object
$$ $arg2 - function number
.block
{
.catch
{
r $t0 = $arg1
.printf "Driver object at 0x%I64X\n",@$t0
r? $t1 = (nt!_DRIVER_OBJECT*)@$t0
r $t0 = @@c++(@$t1->Type)
.if(@$t0==4)
{
r $t0 = @@c++(@$t1->MajorFunction)
r $t1 = $arg2
$$checking second argument
.if(@$t1<1c)
{
r $t2 = @$t0+@$t1*8
r? $t3 = *(void**)@$t2
.printf " Function at 0x%I64X\n",@$t3
.if(@$t3!=0)
{
bp @$t3
u @$t3
}
.else
{
.printf "Invalid function address\n"
}
}
.else
{
.printf "Invalid function number: must be 0-1B\n"
}
}.else
{
.printf "Not a driver object!\n"
}
}
}

Комментарии начинаются с символов $$. Проверяем теперь два аргумента. Первый без изменений – поле DeviceObject.Type=4, а второй – это принадлежность к диапазону 0-1B, то есть допустимые обработчики от IRP_MJ_CREATE до IRP_MJ_PNP. Обрати внимание на строку u @$t3 – здесь осуществляется переход по адресу интересующего обработчика. Результат работы скрипта выглядит следующим образом (для разных вариантов входных аргументов):

kd> $$>a<c:\do2.wds fffffa8001ea1330 2
Driver object at 0xFFFFFA8001EA1330
Function at 0xFFFFFA600A60D830
tdx!TdxTdiDispatchClose:
fffffa60`0a60d830 fff3 push rbx
fffffa60`0a60d832 4883ec20 sub rsp,20h
fffffa60`0a60d836 483b0d130e0100 cmp rcx,qword ptr [tdx!TdxDeviceObject (fffffa60`0a61e650)]
fffffa60`0a60d83d 488b82b8000000 mov rax,qword ptr [rdx+0B8h]
fffffa60`0a60d844 488bda mov rbx,rdx
fffffa60`0a60d847 7458 je tdx!TdxTdiDispatchClose+0x71 (fffffa60`0a60d8a1)
fffffa60`0a60d849 488b4830 mov rcx,qword ptr [rax+30h]
fffffa60`0a60d84d 4883792002 cmp qword ptr [rcx+20h],2

kd> $$>a<c:\do2.wds fffffa8001ea1330 24
Driver object at 0xFFFFFA8001EA1330
Invalid function number: must be 0-1B

kd> $$>a<c:\do2.wds fffffa8001ea1350 7
Driver object at 0xFFFFFA8001EA1350
Not a driver object!

Вообще, это довольно простые задачи. Иногда требуются более сложные узкоспециализированные скрипты, рассмотрение которых выходит за рамки настоящей статьи.
Кстати, сейчас появилось расширение, которое позволяет использовать питон в WinDbg – pykd (что очень хорошо, ведь не у всех есть время и желание осваивать встроенный скриптовый язык), правда он в стадии тестирования. Плагин требует предустановленный Python.

 

Заключение

Конечно, рассмотренный здесь материал – это капля в море возможностей WinDbg. Я не коснулся, например, такой обширной темы, как написание плагинов. Чтобы освоить этот гигантский функционал, нужно время, терпение и опыт. Свои вопросы, как всегда, шли мне на e-mail.

 

WWW

Оставить мнение

Check Also

LUKS container vs Border Patrol Agent. Как уберечь свои данные, пересекая границу

Не секрет, что если ты собрался посетить такие страны как США или Великобританию то, прежд…