Одним из приоритетных направлений в разработке любого продукта является его отладка. Ведь мало написать удовлетворяющий требованиям исправно работающий код, так нужно ещё и протестить его в "токсических условиях", на предмет выявления всевозможных ошибок. Для этих целей, инженеры Microsoft включили в состав ОС полноценный механизм дебага в виде двух библиотек пользовательского режима: это Dbgeng.dll – основной движок отладки (Debug Engine), и Dbghelp.dll – вспомогательный процессор отладочных символов PDB (Program Database).
На моей Win-7, библиотека символов имеет размер 0.8 Мб и выдаёт на экспорт аж 205 стандартных API-функций, а вот вторая Dbgeng.dll в три раза тяжелее своего (со)брата с размером ~2.5 Мб, зато экспортирует с выхлопной трубы всего 3 функции. От сюда следует, что эта либа от нас явно что-то скрывает, поскольку жалкие три процедуры никак не могут весить более двух мегабайт. В данной статье мы попытаемся заглянуть внутрь отладочного движка Engine, и в качестве примера вытащим из него полноценный дизассемблер инструкций процессоров х86.
Оглавление:
1. Знакомство с механизмом отладки
Первые библиотеки отладки, так-же известные как файлы "Symbolic Debugger Engine", были созданы компанией Microsoft в 2001-году для операционной системы Windows-XP. Большая часть либы Dbghelp.dll содержит в себе функции с префиксом(Sym), что говорит об их принадлежности к символьному процессору. Они позволяют по указанному адресу вычислять имена функций, определять типы данных, а также номер строки и название файла, в котором эта строка находится. Поддерживаются и обратные операции, например поиск адреса функции по её имени. Это достаточно творческая единица, если знать как ею пользоваться (
В состав этой библиотеки входят и привычные нам функции, без каких-либо префиксов. Например потянув за всего одну EnumerateLoadedModules() можно получить список всех модулей DLL (вместе с виртуальной базой и размером), которые загружены в интересующее нас приложение. Поскольку вся черновая работа происходит в фоне, то в большинстве случаях это удобно. На входе, функция требует лишь дескриптор процесса (в примере ниже я передаю -1, т.е. текущий процесс), и адрес callback-процедуры, куда она в цикле будет сбрасывать информацию о модулях. Функция связана со-своей "обратной процедурой" невидимой нитью, так-что программный цикл выстраивать не нужно – обход на автомате прекращается, как только коллбэк возвращает родителю ошибку:
Всё идёт прекрасно до тех пор, пока мы не сталкиваемся с вызовом функций из основного движка-отладки Dbgeng.dll – здесь и начинается самое интересное. Эта библиотека построена по модели СОМ (Component-Object-Model), а значит и вызывать из неё функции нужно соответствующим образом. Но проблема в том, что в отличии от крестов С++ и прочих высокоуровневых языков, ни один из ассемблеров не поддерживает на данный момент технологию COM/ActiveX, и врядли уже будет поддерживать в будущем. Ассемблер – это язык низкого уровня, а прослойка СОМ находится в иерархии намного выше.
Как уже упоминалось, библиотека Dbgeng.dll выдаёт на экспорт всего 3-функции (см.в тотале по ctrl+q) – это DebugCreate(), DebugConnect() и DebugConnectWide(). Но под капотом у неё припрятаны ещё порядка 300 внутренних, неэкспортируемых обычным способом функций. Чтобы подобраться к ним, для начала нужно разобраться, что вообще такое COM-интерфейс и как его реализуют современные компиляторы – вот об этом и поговорим..
2. Component-Object-Model в ассемблере
COM – многокомпонентная, клиент-серверная модель объектов Microsoft, которая является продолжением OLE и фундаментальной основой многих других технологий, в том числе ActiveX и DCOM (Distributed COM, работа с сетью). Ключевым аспектом COM является то, что эта технология обеспечивает связь между клиентом (нашим приложением) и сервером (операционной системой) посредством "интерфейсов". Именно интерфейс предоставляет клиенту способ узнать у сервера, какие конкретно возможности он поддерживает на текущий момент.
В терминологии языка С++ интерфейс – это абстрактный базовый класс, все методы которого являются виртуальными. То-есть вызов этих методов осуществляется через специальную таблицу-указателей, известную как vTable. Например, вызов метода
Если-же посмотреть на СОМ глазами ассемблера, то интерфейс представляет собой ничто-иное, как обычную структуру в памяти. Чтобы придерживаться общих правил, мы будем называть её так-же, т.е. "vTable". В свою очередь методы – это лишь иное название уже привычных нам API-функций, а указатели на эти функции хранятся внутри интерфейса. Таким образом, интерфейс можно рассматривать как массив указателей на СОМ-функции. Влиться в эту тему поможет ветка на
В операционной системе Win имеется огромное количество СОМ-интерфейсов и это не удивительно, ведь Microsoft строит системы используя объектно-ориентированный подход программирования, а частью ООП является как-раз-таки OLE/ActiveX/СОМ. Чтобы из этого общего пула у нас была возможность выбрать и использовать в своих программах конкретный интерфейс, система назначает ему уникальный идентификатор GUID. Собрав в единую базу, Win хранит все эти идентификаторы в своём кусте реестра под названием "HKEY_CLASSES_ROOT\Interface". Если выбрать любой GUID в левом окне, то в правом получим отождествлённое с этим идентификатором, название интерфейса:
СОМ-сервер операционной системы имеет базовый интерфейс под названием "IUnknown". Он глобален и все остальные наследуются именно от него. GUID этого интерфейса имеет значение
Как видим, при помощи IUnknown и его метода QueryInterface() можно найти адрес любого СОМ-интерфейса в системе, но только при условии, что мы знаем GUID искомого (нужно будет передать его в качестве аргумента этому методу). Важно запомнить, что базовый интерфейс IUnknown входит в состав буквально всех СОМ-интерфейсов, занимая первые три указателя в нём. СОМ-сервер инкапсулирует его во-все интерфейсы, чтобы вести над ними учёт.
Например, когда мы получаем от сервера ссылку (указатель) на какой-нибудь интерфейс, его метод AddRef() на автомате увеличивает внутренний счётчик-обращений к данному интерфейсу. Если-же интерфейс нам больше не нужен, мы должны вызвать его метод Release(), который соответственно уменьшит этот счётчик на 1. Сервер периодически парсит счётчики активных интерфейсов и если обнаруживает в нём нуль, то из-за ненадобности сразу выгружает его из памяти. Так реализуется "время жизни" СОМ-интерфейсов, и это стандартная схема учёта системных структур, в памяти Win.
3. Структура СОМ-библиотеки Dbgeng.dll
Будем считать, что прошлись по макушкам СОМ-технологии, и теперь рассмотрим её реализацию внутри главного героя этой статьи – библиотеки Dbgeng.dll. В каком-то смысле, эта библиотека сама является полноценным СОМ-сервером, поскольку GUID'ы её интерфейсов не прописаны в системном реестре Win, хотя библиотека и является детищем самой Microsoft. Из этических соображений, все разработчики СОМ-интерфейсов обязаны сопровождать свой продукт полной документацией, чтобы армия прикладных программистов могла использовать незнакомые интерфейсы в своих программах. Связано это с тем, что не зная GUID мы просто не сможем найти ни один интерфейс в системе, и соответственно лишимся возможности вызывать из него методы.
Движок-отладки Dbgeng.dll отлично документирован в репозитории мягких – общие сведения о нём можно почерпнуть
Одной из примечательных особенностей СОМ-интерфейсов является их масштабируемость. Так, если мы захотим изменить уже существующий интерфейс, то достаточно написать недостающие методы, и добавить указатели на них в конец прежнего интерфейса. К примеру, каждый из представленных выше 13-ти фейсов имеет дополнительные экземпляры, к именам которых добавляется порядковый номер по типу: IDebugClient (основной интерфейс), и дальше IDebugClient2 (3,4,5,6,7). Каждый последующий экземпляр включает в себя какие-то свежие методы и ему назначается новый GUID, в результате чего интерфейс шагает в ногу со-временем.
Если учитывать все интерфейсы вместе с расширенными, то в библиотеке Dbgeng.dll операционной системы Win7 зарегистрировано всего 35 СОМ-интерфейсов, а общее число методов в них приближается к отметке 300. Забегая вперёд скажу, что не все они реализованы на должном уровне, в чём мы убедимся позже. Здесь нужно отметить, что движок-отладки включённый в состав ОС отличается от движка ядерного отладчика "WinDbg" – у системного версия [6.1.7601], а у того, что использует отладчик [6.12.2]. Системный файл плохо зарекомендовал себя тем, что в нём вырезана поддержка удалённой отладки, что является козырем отладочного ядра WinDbg. Поэтому и размеры библиотек у них разные, о чём свидетельствует скрин ниже:
Посмотрим на рисунок ниже, где представлена обобщённая структура библиотеки Dbgeng.dll.
Чтобы воспользоваться услугами сервера-отладки, мы должны сначала активировать его функцией CoInitialize() из библиотеки подсистемы исполнения OLE32.dll. Теперь нужно создать "клиента отладки" функцией DebugCreate() из либы Dbgeng.dll, передав ей в виде аргумента GUID интерфейса "IDebugClient::". Это основной интерфейс клиента, где собраны часто используемые им (т.е. нашим приложением) методы.
Если зайти отладчиком OllyDbg в функцию DebugCreate() по [F7], то можно обнаружить, что она проделывает массу полезной работы – например копирует из тушки движка в пространство пользователя различные структуры, находит через GetProcAddress() и подключает вспомогательные функции отладки из библиотеки Ntdll.dll типа: DbgEvent(), DbgBreakPoint() и многое другое. Именно эта функция создаёт полный контекст отладки в памяти ОЗУ, и нам остаётся лишь вызывать методы из требуемых СОМ-интерфейсов:
В качестве демонстрационного примера для вводной части, предлагаю код ниже, который в цикле будет запрашивать у базового интерфейса "IUnknown::" все имеющиеся в наличии интерфейсы отладочного движка. Как уже упоминалось, всего в библиотеке Dbgeng.dll их зарегистрировано 35-штук (вместе с расширенными), а GUID'ы этих интерфейсов я вынес во-внешний инклуд (см.скрепку в конце статьи). По сути здесь нет ничего особенного, однако следующий нюанс требует некоторого пояснения..
Значит передаём функции DebugCreate() GUID интерфейса "IDebugClient::", на что функция возвращает нам адрес этого интерфейса в памяти. Если вернуться к рис.выше, то можно обнаружить, что первые три метода в любом интерфейсе, есть копия базового интерфейса "IUnknown::", а первый метод – как-раз нужный нам QueryInterface(). Он ожидает на входе два аргумента – это GUID искомого интерфейса, и указатель на переменную, куда метод сохранит его адрес.
Особое внимание нужно обратить на способ вызова СОМ-методов в ассемблере. Дело в том, что помимо обозначенных прототипом аргументов, мы всегда должны добавлять ещё один лишний аргумент – в спецификации его назвали "This" и представляет он собой адрес интерфейса. Другими словами, перед вызовом любого метода из какого-либо интерфейса, мы должны явно указать серверу, из какого именно осуществляем вызов. Этот аргумент(This) всегда является первым аргументом метода – вот пример:
Посмотрим на результат работы программы..
Интерфейсы, у которых адресом является нуль, не реализованы в движке-отладки Dbgeng.dll и вызывать из них методы нельзя (получим исключение Access-Violation с кодом 0xC0000005, т.к. будет попытка чтения адреса нуль). Ну с интерфейсами Client::[6,7] и Control::[5,6,7] всё понятно – как видим, это обновы предыдущих и добавлены они только начиная с Win-8. Однако мне так и не удалось найти ответа, почему отсутствуют интерфейсы Breakpoint:: и Callbacks::. Ради эксперимента я даже пробовал подключать не системную библиотеку Dbgeng.dll, а переименовав подсовывал программе либу ядерного отладчика WinDbg, и всё-равно получал аналогичную картину. После нескольких попыток было решено оставить этот вопрос открытым, до лучших времён.
Из остальных интерфейсов можно смело вызывать их методы. Например, лист методов интерфейса IDebugClient::[2,3,4] выглядит так.. а остальные – перечислены в созданном мной инклуде Dbgeng.inc (см.скрепку). Обратите внимание, как добавляются расширенные интерфейсы к предыдущим. Каждый из них включает в себя полный список всех/своих предков, и только в конце добавляются новые.
4. Практика – пишем дизассемблер
Теперь, на финишной прямой, собрав воедино всё/вышеизложенное напишем дизассемблер, одноимённый метод которого лежит в интерфейсе IDebugControl::. Чтобы на поверхность всплыла исключительно полезная составляющая кода, я ограничился дизаcсемблированием лишь текущей программы. В идеале, нужно было дать возможность юзеру выбирать исполняемый файл, но в этом случае "пайлоад" утонул-бы в массе дополнительных функций. Здесь главное понять суть, а окружение – это уже второстепенная задача и дело вкуса. Значит алго будет такой:
Ну и собственно вот реализация этого алгоритма на ассемблере FASM.
Все строки кода прокомментированы, а если что-то непонятно, то всегда можно задать вопрос в комментариях статьи:
Здесь я добавил некоторую вспомогательную информацию в шапке, чтобы продемонстрировать расположение интерфейсов и их методов. Так, первые две строчки указывают на наше пользовательское пространство памяти, куда функция DebugCreate() любезно сбросила указатели на интерфейсы. А вот сами методы находятся уже внутри библиотеки Dbgeng.dll, о чём свидетельствует их адреса с базой
5. Заключение.
Программирование СОМ-интерфейсов открывает перед нами огромные возможности, поскольку в своих/больших штанинах они прячут достаточно интересные методы, подобраться к которым можно только через указатель на интерфейс, аля GUID. По модели СОМ построена добрая половина системных библиотек – объектная модель позволяет нам работать с такими механизмами как WMI (инструментарий Windows), технологией DirectX, с библиотекой Shell32.dll и многое другое. Как упоминалось выше, любой СОМ-интерфейс обязан быть документированным, поэтому проблем не возникает – главное уловить логическую нить, а дальше уже дело техники.
По уже отработанной схеме, в скрепку ложу два исполняемых файла, а так-же инклуд с описанием GUID'ов всех интерфейсов движка Dbgeng.dll, с полным описанием входящих в их состав методов. Всем удачи, пока!
На моей Win-7, библиотека символов имеет размер 0.8 Мб и выдаёт на экспорт аж 205 стандартных API-функций, а вот вторая Dbgeng.dll в три раза тяжелее своего (со)брата с размером ~2.5 Мб, зато экспортирует с выхлопной трубы всего 3 функции. От сюда следует, что эта либа от нас явно что-то скрывает, поскольку жалкие три процедуры никак не могут весить более двух мегабайт. В данной статье мы попытаемся заглянуть внутрь отладочного движка Engine, и в качестве примера вытащим из него полноценный дизассемблер инструкций процессоров х86.
Оглавление:
1. Знакомство с системным механизмом отладки;
2. Component-Object-Model в ассемблере;
3. Структура СОМ-библиотеки Dbgeng.dll;
4. Практика – пишем дизассемблер;
5. Заключение.
---------------------------------------------------------------1. Знакомство с механизмом отладки
Первые библиотеки отладки, так-же известные как файлы "Symbolic Debugger Engine", были созданы компанией Microsoft в 2001-году для операционной системы Windows-XP. Большая часть либы Dbghelp.dll содержит в себе функции с префиксом(Sym), что говорит об их принадлежности к символьному процессору. Они позволяют по указанному адресу вычислять имена функций, определять типы данных, а также номер строки и название файла, в котором эта строка находится. Поддерживаются и обратные операции, например поиск адреса функции по её имени. Это достаточно творческая единица, если знать как ею пользоваться (
Ссылка скрыта от гостей
на сайте мелкософт).В состав этой библиотеки входят и привычные нам функции, без каких-либо префиксов. Например потянув за всего одну EnumerateLoadedModules() можно получить список всех модулей DLL (вместе с виртуальной базой и размером), которые загружены в интересующее нас приложение. Поскольку вся черновая работа происходит в фоне, то в большинстве случаях это удобно. На входе, функция требует лишь дескриптор процесса (в примере ниже я передаю -1, т.е. текущий процесс), и адрес callback-процедуры, куда она в цикле будет сбрасывать информацию о модулях. Функция связана со-своей "обратной процедурой" невидимой нитью, так-что программный цикл выстраивать не нужно – обход на автомате прекращается, как только коллбэк возвращает родителю ошибку:
C-подобный:
format pe console
entry start
include 'win32ax.inc'
;//----------
.code
start: cinvoke printf,<10,' Base Size Name',\
10,' ----------|-----------|-----------------',0>
invoke EnumerateLoadedModules,-1,Modules,0
cinvoke getch
cinvoke exit,0
;//----------
proc Modules mName,mBase,mSize,mUser ;//<-------- Callback-процедура
cinvoke printf,<10,' 0x%08X 0x%08X %s',0>,[mBase],[mSize],[mName]
ret
endp
;//----- ИМПОРТ ----------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',dbghelp,'dbghelp.dll'
import msvcrt, printf,'printf',getch,'_getch',exit,'exit'
import dbghelp, EnumerateLoadedModules,'EnumerateLoadedModules'
Всё идёт прекрасно до тех пор, пока мы не сталкиваемся с вызовом функций из основного движка-отладки Dbgeng.dll – здесь и начинается самое интересное. Эта библиотека построена по модели СОМ (Component-Object-Model), а значит и вызывать из неё функции нужно соответствующим образом. Но проблема в том, что в отличии от крестов С++ и прочих высокоуровневых языков, ни один из ассемблеров не поддерживает на данный момент технологию COM/ActiveX, и врядли уже будет поддерживать в будущем. Ассемблер – это язык низкого уровня, а прослойка СОМ находится в иерархии намного выше.
Как уже упоминалось, библиотека Dbgeng.dll выдаёт на экспорт всего 3-функции (см.в тотале по ctrl+q) – это DebugCreate(), DebugConnect() и DebugConnectWide(). Но под капотом у неё припрятаны ещё порядка 300 внутренних, неэкспортируемых обычным способом функций. Чтобы подобраться к ним, для начала нужно разобраться, что вообще такое COM-интерфейс и как его реализуют современные компиляторы – вот об этом и поговорим..
2. Component-Object-Model в ассемблере
COM – многокомпонентная, клиент-серверная модель объектов Microsoft, которая является продолжением OLE и фундаментальной основой многих других технологий, в том числе ActiveX и DCOM (Distributed COM, работа с сетью). Ключевым аспектом COM является то, что эта технология обеспечивает связь между клиентом (нашим приложением) и сервером (операционной системой) посредством "интерфейсов". Именно интерфейс предоставляет клиенту способ узнать у сервера, какие конкретно возможности он поддерживает на текущий момент.
В терминологии языка С++ интерфейс – это абстрактный базовый класс, все методы которого являются виртуальными. То-есть вызов этих методов осуществляется через специальную таблицу-указателей, известную как vTable. Например, вызов метода
QueryInterface
из интерфейса IUnknown
будет выглядеть так: IUnknown::QueryInterface()
. На сайте rsdn имеется
Ссылка скрыта от гостей
, выделенный специально под описание всех нюансов СОМ-технологии.Если-же посмотреть на СОМ глазами ассемблера, то интерфейс представляет собой ничто-иное, как обычную структуру в памяти. Чтобы придерживаться общих правил, мы будем называть её так-же, т.е. "vTable". В свою очередь методы – это лишь иное название уже привычных нам API-функций, а указатели на эти функции хранятся внутри интерфейса. Таким образом, интерфейс можно рассматривать как массив указателей на СОМ-функции. Влиться в эту тему поможет ветка на
Ссылка скрыта от гостей
, где представлены материалы по СОМ с реальными примерами на ассемблере – "допризывникам" настоятельно рекомендуется к прочтению.В операционной системе Win имеется огромное количество СОМ-интерфейсов и это не удивительно, ведь Microsoft строит системы используя объектно-ориентированный подход программирования, а частью ООП является как-раз-таки OLE/ActiveX/СОМ. Чтобы из этого общего пула у нас была возможность выбрать и использовать в своих программах конкретный интерфейс, система назначает ему уникальный идентификатор GUID. Собрав в единую базу, Win хранит все эти идентификаторы в своём кусте реестра под названием "HKEY_CLASSES_ROOT\Interface". Если выбрать любой GUID в левом окне, то в правом получим отождествлённое с этим идентификатором, название интерфейса:
СОМ-сервер операционной системы имеет базовый интерфейс под названием "IUnknown". Он глобален и все остальные наследуются именно от него. GUID этого интерфейса имеет значение
{00000000-0000-0000-C000-000000000046}
. В своей тушке интерфейс хранит указатели на три метода (функции) и на ассемблере будет выглядеть так:
C-подобный:
struct IUnknown
QueryInterface dd 0 ;// метод позволяет найти адрес интерфейса в памяти, по его GUID.
AddRef dd 0 ;// метод счётчика-ссылок на интерфейс
Release dd 0 ;// метод уменьшает счётчик (при достижении нуля интерфейс освобождается)
ends
Как видим, при помощи IUnknown и его метода QueryInterface() можно найти адрес любого СОМ-интерфейса в системе, но только при условии, что мы знаем GUID искомого (нужно будет передать его в качестве аргумента этому методу). Важно запомнить, что базовый интерфейс IUnknown входит в состав буквально всех СОМ-интерфейсов, занимая первые три указателя в нём. СОМ-сервер инкапсулирует его во-все интерфейсы, чтобы вести над ними учёт.
Например, когда мы получаем от сервера ссылку (указатель) на какой-нибудь интерфейс, его метод AddRef() на автомате увеличивает внутренний счётчик-обращений к данному интерфейсу. Если-же интерфейс нам больше не нужен, мы должны вызвать его метод Release(), который соответственно уменьшит этот счётчик на 1. Сервер периодически парсит счётчики активных интерфейсов и если обнаруживает в нём нуль, то из-за ненадобности сразу выгружает его из памяти. Так реализуется "время жизни" СОМ-интерфейсов, и это стандартная схема учёта системных структур, в памяти Win.
3. Структура СОМ-библиотеки Dbgeng.dll
Будем считать, что прошлись по макушкам СОМ-технологии, и теперь рассмотрим её реализацию внутри главного героя этой статьи – библиотеки Dbgeng.dll. В каком-то смысле, эта библиотека сама является полноценным СОМ-сервером, поскольку GUID'ы её интерфейсов не прописаны в системном реестре Win, хотя библиотека и является детищем самой Microsoft. Из этических соображений, все разработчики СОМ-интерфейсов обязаны сопровождать свой продукт полной документацией, чтобы армия прикладных программистов могла использовать незнакомые интерфейсы в своих программах. Связано это с тем, что не зная GUID мы просто не сможем найти ни один интерфейс в системе, и соответственно лишимся возможности вызывать из него методы.
Движок-отладки Dbgeng.dll отлично документирован в репозитории мягких – общие сведения о нём можно почерпнуть
Ссылка скрыта от гостей
. Что касается описания непосредственно имеющихся в наличии методов и GUID всех интерфейсов, то они находятся в заголовочном файле Dbgeng.h, электронная версия которого
Ссылка скрыта от гостей
. Судя по этому хидеру, в данную библиотеку включён не один, а целая дюжина связанных с отладкой различных интерфейсов, и в каждом из них имеются свои функции (методы). Исторически, 16-байтные GUID интерфейсов принято обозначать как IID, что подразумевает "Interface-Identifier".
C-подобный:
;// Названия и GUID'ы интерфейсов Dbgeng.dll
;//****************************************************
IID_IUnknown dd 0x00000000, 0x00000000, 0x000000c0, 0x46000000
IID_IDebugAdvanced dd 0xf2df5f53, 0x47bd071f, 0x3457e69d, 0x89d6fec3
IID_IDebugBreakpoint dd 0x5bd9d474, 0x423a5975, 0xa8648bb8, 0x650e11e7
IID_IDebugClient dd 0x27fe5639, 0x4f478407, 0x11ee6483, 0xc88ab08f
IID_IDebugControl dd 0x5182e668, 0x416e105e, 0xef2492ad, 0xba240480
IID_IDebugDataSpaces dd 0x88f7dfab, 0x4c3a3ea7, 0xe8c4fbae, 0xaa736110
IID_IDebugEventCallbacks dd 0x337be28b, 0x4d725036, 0x5fc4bfb6, 0xaa2e9fbb
IID_IDebugInputCallbacks dd 0x9f50e42c, 0x499ef136, 0x0373979a, 0x2ded946c
IID_IDebugOutputCallbacks dd 0x4bf58045, 0x4c40d654, 0x3068afb0, 0xdc56f390
IID_IDebugRegisters dd 0xce289126, 0x45a79e84, 0xbb677e93, 0x93146918
IID_IDebugSymbolGroup dd 0xf2528316, 0x44310f1a, 0xd011edae, 0xabe2e196
IID_IDebugSymbols dd 0x8c31e98c, 0x48a5983a, 0xe56f1690, 0x50a967d6
IID_IDebugSystemObjects dd 0x6b86fe2c, 0x4f0c2c4f, 0x4317a29d, 0x27c3ac11
Одной из примечательных особенностей СОМ-интерфейсов является их масштабируемость. Так, если мы захотим изменить уже существующий интерфейс, то достаточно написать недостающие методы, и добавить указатели на них в конец прежнего интерфейса. К примеру, каждый из представленных выше 13-ти фейсов имеет дополнительные экземпляры, к именам которых добавляется порядковый номер по типу: IDebugClient (основной интерфейс), и дальше IDebugClient2 (3,4,5,6,7). Каждый последующий экземпляр включает в себя какие-то свежие методы и ему назначается новый GUID, в результате чего интерфейс шагает в ногу со-временем.
Если учитывать все интерфейсы вместе с расширенными, то в библиотеке Dbgeng.dll операционной системы Win7 зарегистрировано всего 35 СОМ-интерфейсов, а общее число методов в них приближается к отметке 300. Забегая вперёд скажу, что не все они реализованы на должном уровне, в чём мы убедимся позже. Здесь нужно отметить, что движок-отладки включённый в состав ОС отличается от движка ядерного отладчика "WinDbg" – у системного версия [6.1.7601], а у того, что использует отладчик [6.12.2]. Системный файл плохо зарекомендовал себя тем, что в нём вырезана поддержка удалённой отладки, что является козырем отладочного ядра WinDbg. Поэтому и размеры библиотек у них разные, о чём свидетельствует скрин ниже:
Посмотрим на рисунок ниже, где представлена обобщённая структура библиотеки Dbgeng.dll.
Чтобы воспользоваться услугами сервера-отладки, мы должны сначала активировать его функцией CoInitialize() из библиотеки подсистемы исполнения OLE32.dll. Теперь нужно создать "клиента отладки" функцией DebugCreate() из либы Dbgeng.dll, передав ей в виде аргумента GUID интерфейса "IDebugClient::". Это основной интерфейс клиента, где собраны часто используемые им (т.е. нашим приложением) методы.
Если зайти отладчиком OllyDbg в функцию DebugCreate() по [F7], то можно обнаружить, что она проделывает массу полезной работы – например копирует из тушки движка в пространство пользователя различные структуры, находит через GetProcAddress() и подключает вспомогательные функции отладки из библиотеки Ntdll.dll типа: DbgEvent(), DbgBreakPoint() и многое другое. Именно эта функция создаёт полный контекст отладки в памяти ОЗУ, и нам остаётся лишь вызывать методы из требуемых СОМ-интерфейсов:
В качестве демонстрационного примера для вводной части, предлагаю код ниже, который в цикле будет запрашивать у базового интерфейса "IUnknown::" все имеющиеся в наличии интерфейсы отладочного движка. Как уже упоминалось, всего в библиотеке Dbgeng.dll их зарегистрировано 35-штук (вместе с расширенными), а GUID'ы этих интерфейсов я вынес во-внешний инклуд (см.скрепку в конце статьи). По сути здесь нет ничего особенного, однако следующий нюанс требует некоторого пояснения..
Значит передаём функции DebugCreate() GUID интерфейса "IDebugClient::", на что функция возвращает нам адрес этого интерфейса в памяти. Если вернуться к рис.выше, то можно обнаружить, что первые три метода в любом интерфейсе, есть копия базового интерфейса "IUnknown::", а первый метод – как-раз нужный нам QueryInterface(). Он ожидает на входе два аргумента – это GUID искомого интерфейса, и указатель на переменную, куда метод сохранит его адрес.
Особое внимание нужно обратить на способ вызова СОМ-методов в ассемблере. Дело в том, что помимо обозначенных прототипом аргументов, мы всегда должны добавлять ещё один лишний аргумент – в спецификации его назвали "This" и представляет он собой адрес интерфейса. Другими словами, перед вызовом любого метода из какого-либо интерфейса, мы должны явно указать серверу, из какого именно осуществляем вызов. Этот аргумент(This) всегда является первым аргументом метода – вот пример:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//----------
.data
InterfaceName:
dd i01,i02,i03,i04,i05,i06,i07,i08,i09,i10 ;// таблица указателей на имена интерфейсов
dd i11,i12,i13,i14,i15,i16,i17,i18,i19,i20
dd i21,i22,i23,i24,i25,i26,i27,i28,i29,i30
dd i31,i32,i33,i34,i35
i01 db 10,' IUnknown..............: ',0
i02 db 10,10,' IDebugAdvanced........: ',0
i03 db 10,' IDebugAdvanced2.......: ',0
i04 db 10,' IDebugAdvanced3.......: ',0
i05 db 10,10,' IDebugBreakpoint......: ',0
i06 db 10,' IDebugBreakpoint2.....: ',0
i07 db 10,' IDebugBreakpoint3.....: ',0
i08 db 10,10,' IDebugClient..........: ',0
i09 db 10,' IDebugClient2.........: ',0
i10 db 10,' IDebugClient3.........: ',0
i11 db 10,' IDebugClient4.........: ',0
i12 db 10,' IDebugClient5.........: ',0
i13 db 10,' IDebugClient6.........: ',0
i14 db 10,' IDebugClient7.........: ',0
i15 db 10,10,' IDebugControl.........: ',0
i16 db 10,' IDebugControl2........: ',0
i17 db 10,' IDebugControl3........: ',0
i18 db 10,' IDebugControl4........: ',0
i19 db 10,' IDebugControl5........: ',0
i20 db 10,' IDebugControl6........: ',0
i21 db 10,' IDebugControl7........: ',0
i22 db 10,10,' IDebugDataSpaces......: ',0
i23 db 10,' IDebugDataSpaces2.....: ',0
i24 db 10,' IDebugDataSpaces3.....: ',0
i25 db 10,10,' IDebugEventCallbacks..: ',0
i26 db 10,' IDebugInputCallbacks..: ',0
i27 db 10,' IDebugOutputCallbacks.: ',0
i28 db 10,' IDebugOutputCallbacks2: ',0
i29 db 10,10,' IDebugRegisters.......: ',0
i30 db 10,' IDebugSymbolGroup.....: ',0
i31 db 10,' IDebugSymbols.........: ',0
i32 db 10,' IDebugSymbols2........: ',0
i33 db 10,10,' IDebugSystemObjects...: ',0
i34 db 10,' IDebugSystemObjects2..: ',0
i35 db 10,' IDebugSystemObjects3..: ',0
Client dd 0 ;// переменная под адрес интерфейса "IDebugClient"
iOffset dd 0 ;// переменная под адрес остальных интерфейсов (обновляется в цикле)
buff db 0
;//----------
.code
start: invoke SetConsoleTitle,<'*** Debug Engine QueryInterface v0.1 ***',0>
invoke CoInitialize,0 ;// активируем СОМ-сервер
;// Создаём клиента отладки (в переменную Client получим указатель на интерфейс)
invoke DebugCreate,IID_IDebugClient,Client
xchg ebx,eax
cinvoke printf,<10,' Client Interface......: %08X',0>,[Client]
cmp ebx,S_OK
jnz @error ;// если ошибка..
;// Перебрать имеющиеся в движке интерфейсы
mov ecx,35 ;// всего зарегистрировано (длина цикла для LOOP)
mov esi,GuidTable ;// адрес таблицы-гуидов в инклуде "Dbgeng.inc"
mov ebx,InterfaceName ;// адрес таблицы с именами интерфейсов
@@: push ecx esi ebx ebx ;// запомнить для организации цикла!
mov eax,[Client] ;// адрес интерфейса "IDebugClient"
mov edx,[eax] ;// берём из него сразу-же первый указатель на метод QueryInterface()
push iOffset ;// куда сохранять указатель на интерфейс
push esi ;// GUID очередного интерфейса
push [Client] ;// аргумент "This" (от куда вызываем метод)
call dword[edx] ;// QueryInterface()!
pop ebx ;//
mov edx,[ebx] ;// указатель на имя из таблицы
cinvoke printf,<'%s%08X',0>,edx,[iOffset] ;// вывести на консоль имя и адрес интерфейса
pop ebx esi ecx ;// восстановить данные цикла
add esi,16 ;// следующий GUID в таблице
add ebx,4 ;// следующий указатель на имя
loop @b ;// промотать цикл ECX-раз..
@exit: invoke CoUninitialize ;// освобождаем СОМ-сервер
cinvoke getch ;//
cinvoke exit,0 ;// GAME OVER!
;//===== ОБРАБОТКА ОШИБКИ ======================
@error: cinvoke printf,<10,' Operation ERROR!!!',0>
jmp @exit
;//----- ИМПОРТ ----------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',dbgeng,'dbgeng.dll',\
ole32, 'ole32.dll', kernel32,'kernel32.dll'
import msvcrt, printf,'printf',getch,'_getch',exit,'exit'
import dbgeng, DebugCreate, 'DebugCreate'
import ole32, CoInitialize,'CoInitialize',CoUninitialize,'CoUninitialize'
include 'api\kernel32.inc'
include 'equates\dbgeng.inc' ;//<-------- подключаем свой инклуд!!!
Посмотрим на результат работы программы..
Интерфейсы, у которых адресом является нуль, не реализованы в движке-отладки Dbgeng.dll и вызывать из них методы нельзя (получим исключение Access-Violation с кодом 0xC0000005, т.к. будет попытка чтения адреса нуль). Ну с интерфейсами Client::[6,7] и Control::[5,6,7] всё понятно – как видим, это обновы предыдущих и добавлены они только начиная с Win-8. Однако мне так и не удалось найти ответа, почему отсутствуют интерфейсы Breakpoint:: и Callbacks::. Ради эксперимента я даже пробовал подключать не системную библиотеку Dbgeng.dll, а переименовав подсовывал программе либу ядерного отладчика WinDbg, и всё-равно получал аналогичную картину. После нескольких попыток было решено оставить этот вопрос открытым, до лучших времён.
Из остальных интерфейсов можно смело вызывать их методы. Например, лист методов интерфейса IDebugClient::[2,3,4] выглядит так.. а остальные – перечислены в созданном мной инклуде Dbgeng.inc (см.скрепку). Обратите внимание, как добавляются расширенные интерфейсы к предыдущим. Каждый из них включает в себя полный список всех/своих предков, и только в конце добавляются новые.
C-подобный:
;// Методы интерфейсов (в комментах указаны аргументы)
;//***********************************************************
struct IUnknown
QueryInterface dd 0 ;// InterfaceId, pInterface
AddRef dd 0 ;//
Release dd 0 ;//
ends
struct IDebugClient
Header IUnknown ;//<----------- всякий интерфейс начинается с IUnknown
AttachKrnl dd 0 ;// Flags, ConnectOptions
GetKrnlConnectionOptions dd 0 ;// Buffer, BufferSize, OptionsSize
SetKrnlConnectionOptions dd 0 ;// Options
StartProcessServer dd 0 ;// Flags, Options, Reserved
ConnectProcessServer dd 0 ;// RemoteOptions, Server64
DisconnectProcessServer dd 0 ;// Server64
GetRunProcessSysIds dd 0 ;// Server64, Ids, Count, ActualCount
GetRunProcessSysIdExName dd 0 ;// Server, ExeName, Flags, Id
GetRunProcessDescription dd 0 ;// 9 argumets
AttachProcess dd 0 ;// Server64, PId, AttachFlags
CreateProcess dd 0 ;// Server64, CommandLine, CreateFlags
CreateProcessAndAttach dd 0 ;// Server, CommandLine, CreateFlags, PId, AttachFlags
GetProcessOptions dd 0 ;// Options
AddProcessOptions dd 0 ;// Options
RemoveProcessOptions dd 0 ;// Options
SetProcessOptions dd 0 ;// Options
OpenDumpFile dd 0 ;// DumpFile
WriteDumpFile dd 0 ;// DumpFile, Qualifier
ConnectSession dd 0 ;// Flags, HistoryLimit
StartServer dd 0 ;// Options
OutputServers dd 0 ;// OutputControl, Machine, Flags
TerminateProcesses dd 0 ;//
DetachProcesses dd 0 ;//
EndSession dd 0 ;// Flags
GetExitCode dd 0 ;// Code
DispatchCallbacks dd 0 ;// Timeout
ExitDispatch dd 0 ;// Client
CreateClient dd 0 ;// Client
GetInputCallbacks dd 0 ;// Callbacks
SetInputCallbacks dd 0 ;// Callbacks
GetOutputCallbacks dd 0 ;// Callbacks
SetOutputCallbacks dd 0 ;// Callbacks
GetOutputMask dd 0 ;// Mask
SetOutputMask dd 0 ;// Mask
GetOtherOutputMask dd 0 ;// Client, Mask
SetOtherOutputMask dd 0 ;// Client, Mask
GetOutputWidth dd 0 ;// Columns
SetOutputWidth dd 0 ;// Columns
GetOutputLinePrefix dd 0 ;// Buffer, BufferSize, PrefixSize
SetOutputLinePrefix dd 0 ;// Prefix
GetIdentity dd 0 ;// Buffer, BufferSize, IdentitySize
OutputIdentity dd 0 ;// OutputControl, Flags, Format
GetEventCallbacks dd 0 ;// Callbacks
SetEventCallbacks dd 0 ;// Callbacks
FlushCallbacks dd 0 ;//
ends
struct IDebugClient2
Previous IDebugClient ;//<------ IDebugClient2 включает в себя весь предыдущий интерфейс!
WriteDumpFile2 dd 0 ;// DumpFile,Qualifier,FormatFlags,Comment
AddDumpInformationFile dd 0 ;// InfoFile,Type
EndProcessServer dd 0 ;// Server
WaitForProcessServerEnd dd 0 ;// Timeout
IsKrnlDebuggerEnabled dd 0 ;//
TerminateCurrentProcess dd 0 ;//
DetachCurrentProcess dd 0 ;//
AbandonCurrentProcess dd 0 ;//
ends
struct IDebugClient3
Previous IDebugClient2
GetRunProcessSysIdExNameWide dd 0 ;// Server,PCWSTR ExeName,Flags,PId
GetRunProcessDescriptionWide dd 0 ;// 9 arguments (see Dbgeng.h)
CreateProcessWide dd 0 ;// Server,CommandLine,CreateFlags
CreateProcessAndAttachWide dd 0 ;// Server,CmdLine,CreateFlags,ProcessId,AttachFlags
ends
struct IDebugClient4
Previous IDebugClient3
OpenDumpFileWide dd 0 ;// FileName,FileHandle
WriteDumpFileWide dd 0 ;// FileName,FileHandle,Qualifier,FormatFlags,Comment
AddDumpInfoFileWide dd 0 ;// FileName,FileHandle,Type
GetNumberDumpFiles dd 0 ;// PNumber
GetDumpFile dd 0 ;// Index,Buffer,BufferSize,PNameSize,Handle,PType
GetDumpFileWide dd 0 ;// Index,Buffer,BufferSize,PNameSize,Handle,PType
ends
4. Практика – пишем дизассемблер
Теперь, на финишной прямой, собрав воедино всё/вышеизложенное напишем дизассемблер, одноимённый метод которого лежит в интерфейсе IDebugControl::. Чтобы на поверхность всплыла исключительно полезная составляющая кода, я ограничился дизаcсемблированием лишь текущей программы. В идеале, нужно было дать возможность юзеру выбирать исполняемый файл, но в этом случае "пайлоад" утонул-бы в массе дополнительных функций. Здесь главное понять суть, а окружение – это уже второстепенная задача и дело вкуса. Значит алго будет такой:
1. При помощи метода QueryInterface() найти адреса интерфейсов IDebugClient:: и IDebugControl::;
2. Внутри интерфейса IDebugClient:: найти указатель на метод AttachProcess(), чтобы прицепить отладчик к текущему (или любому другому) процессу;
3. Внутри интерфейса IDebugControl:: найти указатели на методы WaitForEvent() и Disassemble() – первый ожидает события отладки, а второй генерит событие Disasm;
4. Вызвать все эти методы, обязательно в указанном выше порядке;
5. Последний метод Disassemble() сбросит в буфер дизассемблированную строку – вывести её на консоль!
6. Прокрутить цикл[5] столько раз, сколько хотим "переварить" инструкций.
7. Выход из программы.
Ну и собственно вот реализация этого алгоритма на ассемблере FASM.
Все строки кода прокомментированы, а если что-то непонятно, то всегда можно задать вопрос в комментариях статьи:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//----------
.data
title db '*** Disassembler v0.1 ***',0
clientFace db 10,' Client Interface............: %08X',0
ctrlFace db 10,' Control Interface...........: %08X',0
Client dd 0 ;// адреса СОМ-интерфейсов
Control dd 0 ;// ...^^^
Attach dd 0 ;// указатели на методы из интерфейсов
WaitEvent dd 0 ;// ...^^^
Disasm dd 0 ;// ...^^^
pNextOffs dd 0,0
DisasmSize dd 0
buff db 0
;//----------
.code
start: invoke SetConsoleTitle,title
invoke CoInitialize,0
;// Запрашиваем интерфейс клиента
invoke DebugCreate,IID_IDebugClient,Client
xchg ebx,eax
cinvoke printf,clientFace,[Client]
cmp ebx,S_OK
jnz @error
;// Проверить наличие интерфейса 'IDebugControl'
mov eax,[Client] ;// таблица клиента
mov edx,[eax] ;// адрес метода 'QueryInterface' в ней
push Control ;// сюда получим указатель
push IID_IDebugControl ;// GUID запрашиваемого интерфейса
push [Client] ;// This = в каком интерфейсе искать
call dword[edx] ;// QueryInterface!!!
;// Проверить на ошибку
xchg ebx,eax
cinvoke printf,ctrlFace,[Control]
cmp ebx,S_OK
jnz @error
;// Получаем адреса нужных нам методов из интерфейсов ==================
;// IDebugClient::AttachProcess
mov esi,[Client]
mov esi,[esi]
mov eax,[esi+IDebugClient.AttachProcess]
mov [Attach],eax
cinvoke printf,<10,' Client AttachProcess method: %08X',0>,eax
;// IDebugControl::WaitForEvent
mov esi,[Control]
mov esi,[esi]
mov eax,[esi+IDebugControl.WaitForEvent]
mov [WaitEvent],eax
cinvoke printf,<10,' Control WaitForEvent method: %08X',0>,eax
;// IDebugControl::Disassemble
mov esi,[Control]
mov esi,[esi]
mov eax,[esi+IDebugControl.Disassemble]
mov [Disasm],eax
cinvoke printf,<10,' Control Disassemble method: %08X',0>,eax
;//***********************************************************
@process:
;// Подцепить отладчик к текущему процессу ()
invoke GetCurrentProcessId
mov ebx,DEBUG_ATTACH_NONINVASIVE + DEBUG_ATTACH_NONINVASIVE_NO_SUSPEND
push ebx ;// флаги
push eax ;// Pid процесса
push 0 0 ;// Ulong64 Server
push [Client] ;// This
call dword[Attach]
cinvoke printf,<10,' Attach ReturnCode: %08x',0>,eax
;// Ожидать событие отладки..
push -1 ;// INFINITY (ждать бесконечно)
push 0 ;// DEBUG_WAIT_DEFAULT
push [Control] ;// This
call dword[WaitEvent]
cinvoke printf,<10,' DbgWait ReturnCode: %08x',10,10,0>,eax
;//===== Вызвать дизассемблер!!! ============================
mov ebx,start ;// адрес первой инструкции
mov ecx,25 ;// всего дизассемблировать инструкций
@@: push ecx ebx ;// запомнить для цикла..
push pNextOffs ;// получим адрес сл.инструкции
push DisasmSize ;// получим размер данных в буфере
push 256 ;// размер буфера
push buff ;// адрес приёмного буфа
push DEBUG_DISASM_EFFECTIVE_ADDRESS ;// флаг дизасма
push 0 ;// см.ниже vvv
push ebx ;// qword-адрес в памяти для дизасма
push [Control] ;// This
call dword[Disasm]
cinvoke printf,<' %s',0>,buff ;// распечатать дизасм-листинг!
pop ebx ecx ;// восстановить данные цикла
mov ebx,[pNextOffs] ;// адрес сл.инструкции из переменной
loop @b ;// промотать ECX-раз..
@exit: cinvoke getch ;// GAME OVER!
invoke CoUninitialize ;// освободить СОМ-сервер
cinvoke exit,0 ;//
;//===== ПРОЦЕДУРЫ ===============================
@error: cinvoke printf,<10,' Operation ERROR!!!',0>
jmp @exit
;//----- ИМПОРТ ----------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',dbgeng,'dbgeng.dll',\
ole32, 'ole32.dll', kernel32,'kernel32.dll'
import msvcrt, printf,'printf',getch,'_getch',exit,'exit'
import dbgeng, DebugCreate,'DebugCreate'
import ole32, CoInitialize,'CoInitialize',CoUninitialize,'CoUninitialize'
include 'api\kernel32.inc'
include 'equates\dbgeng.inc'
;//----- РЕСУРСЫ ---------
section '.rsrc' resource data readable
directory RT_VERSION,ver
resource ver, 1, LANG_NEUTRAL, vInfo
versioninfo vInfo,\
VOS__WINDOWS32, VFT_APP, VFT2_UNKNOWN,\
LANG_ENGLISH + SUBLANG_DEFAULT, 1252,\
'CompanyName' , 'https://codeby.net',\
'LegalCopyright' , 'Copyright 2020-2021 (c)Marylin',\
'ProductName' , 'Windows 7',\
'ProductVersion' , '6.1.7601.3821',\
'FileDescription' , 'DbgEngine Disassembler',\
'FileVersion' , '0.0.1',\
'OriginalFilename', 'DbgDisasm.exe'
Здесь я добавил некоторую вспомогательную информацию в шапке, чтобы продемонстрировать расположение интерфейсов и их методов. Так, первые две строчки указывают на наше пользовательское пространство памяти, куда функция DebugCreate() любезно сбросила указатели на интерфейсы. А вот сами методы находятся уже внутри библиотеки Dbgeng.dll, о чём свидетельствует их адреса с базой
0x5D0D0000
. Если вызов метода возвращает в EAX=0
, значит он прошёл успешно (константа S_OK), иначе в EAX
получим следующие коды ошибок:
C-подобный:
;// Перечень ошибок - WinError.h.
;//---------------------------------------------
S_OK = 0 ;// операция выполнена успешно!
S_FALSE = 1 ;// без ошибок, но получена только часть результата (см.буфер)
E_NOINTERFACE = 80004002h ;// интерфейс не найден
E_POINTER = 80004003h ;// неверный указатель
E_ABORT = 80004004h ;// операция отвергнута
E_FAIL = 80004005h ;// операция не может быть выполнена
E_ACCESSDENIED = 80070005h ;// доступ запрещён (отладчик находится в безопасном режиме)
E_HANDLE = 80070006h ;// проблема с дескриптором
E_OUTOFMEMORY = 8007000Eh ;// ошибка выделения памяти
E_INVALIDARG = 80070057h ;// неверный аргумент метода
E_UNEXPECTED = 8000FFFFh ;// отладчик в неправильном состоянии (см.WaitForEvent)
5. Заключение.
Программирование СОМ-интерфейсов открывает перед нами огромные возможности, поскольку в своих/больших штанинах они прячут достаточно интересные методы, подобраться к которым можно только через указатель на интерфейс, аля GUID. По модели СОМ построена добрая половина системных библиотек – объектная модель позволяет нам работать с такими механизмами как WMI (инструментарий Windows), технологией DirectX, с библиотекой Shell32.dll и многое другое. Как упоминалось выше, любой СОМ-интерфейс обязан быть документированным, поэтому проблем не возникает – главное уловить логическую нить, а дальше уже дело техники.
По уже отработанной схеме, в скрепку ложу два исполняемых файла, а так-же инклуд с описанием GUID'ов всех интерфейсов движка Dbgeng.dll, с полным описанием входящих в их состав методов. Всем удачи, пока!