Статья Пишем дизассемблер из библиотеки Dbgeng.dll

Одним из приоритетных направлений в разработке любого продукта является его отладка. Ведь мало написать удовлетворяющий требованиям исправно работающий код, так нужно ещё и протестить его в "токсических условиях", на предмет выявления всевозможных ошибок. Для этих целей, инженеры Microsoft включили в состав ОС полноценный механизм дебага в виде двух библиотек пользовательского режима: это Dbgeng.dll – основной движок отладки (Debug Engine), и Dbghelp.dll – вспомогательный процессор отладочных символов PDB (Program Database).

На моей 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'

Enum.png


Всё идёт прекрасно до тех пор, пока мы не сталкиваемся с вызовом функций из основного движка-отладки 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 в левом окне, то в правом получим отождествлённое с этим идентификатором, название интерфейса:

Reg_GUID.png


СОМ-сервер операционной системы имеет базовый интерфейс под названием "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. Поэтому и размеры библиотек у них разные, о чём свидетельствует скрин ниже:


DbgengFile.png


Посмотрим на рисунок ниже, где представлена обобщённая структура библиотеки Dbgeng.dll.
Чтобы воспользоваться услугами сервера-отладки, мы должны сначала активировать его функцией CoInitialize() из библиотеки подсистемы исполнения OLE32.dll. Теперь нужно создать "клиента отладки" функцией DebugCreate() из либы Dbgeng.dll, передав ей в виде аргумента GUID интерфейса "IDebugClient::". Это основной интерфейс клиента, где собраны часто используемые им (т.е. нашим приложением) методы.

Если зайти отладчиком OllyDbg в функцию DebugCreate() по [F7], то можно обнаружить, что она проделывает массу полезной работы – например копирует из тушки движка в пространство пользователя различные структуры, находит через GetProcAddress() и подключает вспомогательные функции отладки из библиотеки Ntdll.dll типа: DbgEvent(), DbgBreakPoint() и многое другое. Именно эта функция создаёт полный контекст отладки в памяти ОЗУ, и нам остаётся лишь вызывать методы из требуемых СОМ-интерфейсов:


Dbgeng.png


В качестве демонстрационного примера для вводной части, предлагаю код ниже, который в цикле будет запрашивать у базового интерфейса "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'   ;//<-------- подключаем свой инклуд!!!

Query.png


Посмотрим на результат работы программы..
Интерфейсы, у которых адресом является нуль, не реализованы в движке-отладки 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'

Dbgeng_Disassm.png


Здесь я добавил некоторую вспомогательную информацию в шапке, чтобы продемонстрировать расположение интерфейсов и их методов. Так, первые две строчки указывают на наше пользовательское пространство памяти, куда функция 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, с полным описанием входящих в их состав методов. Всем удачи, пока!
 

Вложения

  • DebugEngine.zip
    8,9 КБ · Просмотры: 383

ProMiNick

One Level
20.03.2020
4
2
BIT
0

Marylin

, я тебя иногда почитываю.
возникла идея все у тебя сплагиатить. то есть не сделать ничего нового, просто объединить оба твоих примера и переиначить их.
что (будет) делает прога: с ней идет инифайл с указанием целевой библиотеки (COM-сервера), с указанием процедуры загрузки интерфейсов из библиотеки, и текстовый файлик с расписанными интерфейсами.
теперь собственно что прога будет делать: загружать интерфейс по алгоритму из инифайла, затем осуществлять перебор по методам интерфейса, если будет встречен метод с кодом
Код:
mov eax, E_NOTIMPL
ret 4*кол-во параметров
такой метод метить как нереализованный,
а на все прочие методы натравливать вышеописанный дизассемблер (грубо - до первой встреченной инструкции ret ( т.е. инструкции в потоке инструкций начинающейся с байта $C2 или $C3)),
в результате программа будет показывать доступность интерфейсов, реализованность методов (при наследовании реализованность методов может как появляться так и исчезать, так и быть реализованной иначе), дизассемблинг методов, и это будет конфигурируемо - можно натравливать на любой COM-сервер.
У меня DBGENG.INC это один из исходников будущей программы, ее же синтаксис планируется для перебора интерфейсов и их методов, просчет числа параметров методов так же из этого файла.
в 2х словах - идея программы - ГУИ, 2 компонента - дерево и едитбокс, в дереве интерфейсы и их методы, в эдитбоксе информация синхронно дерева - адреса доступности интерфейсов, реализованность метода, дизасм метода. все остальное файлы рядом с программой 1 ini и 1 inc.
(На первом этапе наверно без 1 ini и 1 inc, а жестко закодировать перечень интерфейсов и их методов из dbgeng.dll)
 

Вложения

  • DBGENG.zip
    7,1 КБ · Просмотры: 239
  • Нравится
Реакции: Mikl___ и Marylin

morgot

Green Team
25.07.2018
74
35
BIT
2
Не могу найти, как получить длину инструкции; хотел заюзать эти СОМ для простого дизассемблера длин (чтобы не тащить сторонние). Но что-то не найду ничего среди методов - возвращают или строку или еще что.
А так - статья шикарная , как всегда! В мсдн не указано (или я не видел), что обязательно нужно WaitForEvent вызывать перед дизасмом, иначе возвращает ошибки.
В теории, есть метод GetNearInstruction, на практике он иногда возвращает 0, неясно что такое 0 - длина инструкции не может же быть нулевая; , кто лучше шарит в инглише, мб будут какие-то мысли? Написать функцию, на вход адрес , на выходе должна быть длина инструкции.
 
Последнее редактирование:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!