Введение

Иногда возникает надобность в перехвате
вызовов методов стандартных виндовых
встроенных СОМ объектов. Например, чтобы
перехватить в системе момент создания
ярлыка гораздо удобнее было бы перехватить
вызов IShellLink.IPersistFile.Save, чем перехватывать
апи функции работы с файлами и парсить их
параметры, согласитесь. Вообще, если
порассуждать на эту тему, то можно выяснить,
что с точки зрения того, кто перехватывает,
метод – это не более чем абстракция,
скрывающая под собой простое определение
подпрограммы, а поэтому и перехватывать мы
будем просто подпрограмму. В чем основная
проблема? А проблема в том, что при
перехвате апишек из 3го кольца мы можем
получить адрес функции по ее имени, а тут мы
имеем дело с неким абстрактным понятием «интрефейс»,
элементы которого не именуются. Тем не
менее, понятие интерфейс – это жесткое
понятие, то есть если интерфейс определен
Microsoft, то и изменяться он не имеет никакого
права. Можно сделать производный интерфейс
с новыми и переопределенными методами, но
нельзя изменить специфицированный. Этим-то
и воспользуемся 😉 

Я старался писать эту статью доступным
языком, тем не менее придется напрячься и
почитать листинги для полного понимания.
СОМ штука непростая, даже если свести все к
выполнению поставленной мной задачи.
Листинги написаны на трех (!!!) различных
языках, так что с этим проблем быть не
должно. Да, совсем забыл :), примеры будут для
уже сделанных дядей Биллом встроенных СОМ
серверов, свои серверы писать не будем.

Немного теории

Во-первых, надо определиться с тем, что
такое интерфейс и как это выглядит в памяти
компьютера. Интерфейс – это некое подобие
абстрактного класса в котором нельзя
объявлять переменные и инкапсулировать (определять
области видимости) методы. 

«Базовый класс, объекты которого никогда
не будут реализованы, называется
абстрактным классом. Такой класс может
существовать с единственной целью – быть
родительским по отношению к производным
классам, объекты которых будут реализованы.»
Р.Лафоре «Объектно-ореинтированное
программирование в С++»

Фактически – это структура, содержащая
набор указателей на функции :). Пример?
Пожалуйста, вот вам дельфовое определение
класса IUnknown:

type
IInterface = interface
[‘{00000000-0000-0000-C000-000000000046}’]
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
IUnknown = IInterface;

А вот то же самое определение, но на низком
уровне (пример взят из статьи Ernest’a Murphy «Accessing
COM Objects From Assembly», ссылка в конце статьи):

IUnknown_QueryInterfaceProto typedef PROTO :DWORD,
:DWORD, :DWORD
IUnknown_AddRefProto typedef PROTO :DWORD
IUnknown_ReleaseProto typedef PROTO :DWORD

IUnknown_QueryInterface typedef ptr IUnknown_QueryInterfaceProto
IUnknown_AddRef typedef ptr IUnknown_AddRefProto
IUnknown_Release typedef ptr IUnknown_ReleaseProto

IUnknown STRUCT DWORD
QueryInterface IUnknown_QueryInterface ?
AddRef IUnknown_AddRef ?
Release IUnknown_Release ?
IUnknown ENDS

Как видите – просто структура с
указателями на функции, ничего более. Как
получить указатель на подобную структуру (а
если она существует в памяти, то это не
интерфейс, а объект! Но это не суть важно для
нас)? CoCreateInstance из ole32.dll как раз этим и
занимается ;). Предварительно надо
инициализировать СОМ с помощью CoInitialize,
внизу даны прототипы этих функций.

HRESULT CoInitialize( LPVOID pvReserved); 
// подробнее здесь: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/com/htm/cmf_a2c_36qt.asp

STDAPI CoCreateInstance(REFCLSID rclsid,LPUNKNOWN pUnkOuter,DWORD
dwClsContext,REFIID riid,LPVOID *ppv);
// Подробнее здесь: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/com/htm/cmf_a2c_1nad.asp

ОК, отлично. У нас есть таблица, по
определенному смещению в которой лежит
адрес нужного метода. А это значит, что
можно реализовать перехват 😉

Изменение таблиц

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

#include <windows.h>
#include <shlobj.h>

// Как получили эти
константы смотри после кода

#define CQUERYINTERFACE 0
#define CSETPATH 80

/* Процедура перехвата
СОМ метода изменением таблицы

Параметры:
PDWORD pIntf — Указатель на COM объект;
DWORD COMTableThunk — Смещение метода в таблице (См.
пояснения после кода);
void* pNewFunc — Указатель на обработчик;
PDWORD pOldMethodAddr — Куда записать старый адрес
метода, может быть NULL.

*/
void HookCOMTable(PDWORD pIntf, DWORD COMTableThunk, void* pNewFunc, PDWORD
pOldMethodAddr)
{
//
Получаем указатель
на адрес нужного метода

PDWORD pTableThunk = (PDWORD)(*(PDWORD)(*pIntf)+COMTableThunk);
//
Сохраняем старый
адрес метода – поможет возвратить всего на
круги своя 🙂

if(pOldMethodAddr!=NULL) *pOldMethodAddr = *pTableThunk;
DWORD op;
//
Это все принадлежит
адресному пространству ole32,
предположительно 

//
секции кода :),
поэтому надо позаботиться о доступе к
страницам памяти

VirtualProtect(pTableThunk,4,PAGE_READWRITE,&op);
//
Собственно перехват
*pTableThunk = DWORD(pNewFunc);
}

// Just example
void NewFunc()
{
MessageBox(0, “We made it!” , “GOTCHA!!!”, MB_OK);
}

int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)

//
Инициализируем СОМ
CoInitialize(NULL);
IShellLink* psl; 
//
Получаем доступ к
объекту ShellLink. Это будет локально (на нашей
машине) 

//
и принадлежать код
будет нашему процессу (CLSCTX_INPROC_SERVER)

CoCreateInstance(CLSID_ShellLink, NULL, 
CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID *) &psl); 
IPersistFile* ppf; 
//
Запрашиваем доступ к
объекту «подинтерфейса» из ShellLink, 
// под
названием PersistFile

psl->QueryInterface(IID_IPersistFile, 
(LPVOID*)&ppf); 
DWORD old;
//
Перехватываем вызов
метода изменения пути к проге, 

//
к которой будем
создавать ярлык

HookCOMTable((PDWORD)&psl, CSETPATH, &NewFunc, &old);
//
Отменяем перехват (восстанавливаем
старую процедуру) 🙂

HookCOMTable((PDWORD)&psl, CSETPATH, PWORD(old), 0);
//
Устанавливаем путь к
проге, к которой будем создавать ярлык

psl->SetPath("c:\\1.exe");
//
Берем первую
попавшуюся иконку из сорсного фаила

psl->SetIconLocation("c:\\1.exe",0);

WCHAR wsz[MAX_PATH]; 
//
Переводим в UNICODE, как
того требует Save-метод

MultiByteToWideChar(CP_ACP, 0, "1.lnk", -1, 
wsz, MAX_PATH); 
//
Создаем ярлык
ppf->Save(wsz, TRUE); 
//
Освобождаем объекты
ppf->Release(); 
psl->Release(); 
return 0;
}

Этот код создает ярлычок к проге с путем c:\1.exe
и значком этой же проги. Перехват
происходит при вызове функции HookCOMTable.
Теперь, как и обещано, пояснения на тему
откуда взялись константы типа CSETPATH? ОК, look
here: представим, что объект – это одномерный
массив указателей на методы, тогда наши
константы – это индексы элементов этого
массива. Как их находить? Смотрим
определение интерфейса IShellLink на С (пример-то
на С) из shlobj.h:

DECLARE_INTERFACE_(IShellLinkA, IUnknown) // sl
{
// *** IUnknown methods ***
STDMETHOD(QueryInterface) (THIS_ REFIID riid, LPVOID * ppvObj) PURE;
STDMETHOD_(ULONG,AddRef) (THIS) PURE;
STDMETHOD_(ULONG,Release) (THIS) PURE;

// *** IShellLink methods ***
STDMETHOD(GetPath)(THIS_ LPSTR pszFile, int cchMaxPath, WIN32_FIND_DATAA *pfd,
DWORD fFlags) PURE;

STDMETHOD(GetIDList)(THIS_ LPITEMIDLIST * ppidl) PURE;
STDMETHOD(SetIDList)(THIS_ LPCITEMIDLIST pidl) PURE;

STDMETHOD(GetDescription)(THIS_ LPSTR pszName, int cchMaxName) PURE;
STDMETHOD(SetDescription)(THIS_ LPCSTR pszName) PURE;

STDMETHOD(GetWorkingDirectory)(THIS_ LPSTR pszDir, int cchMaxPath) PURE;
STDMETHOD(SetWorkingDirectory)(THIS_ LPCSTR pszDir) PURE;

STDMETHOD(GetArguments)(THIS_ LPSTR pszArgs, int cchMaxPath) PURE;
STDMETHOD(SetArguments)(THIS_ LPCSTR pszArgs) PURE;

STDMETHOD(GetHotkey)(THIS_ WORD *pwHotkey) PURE;
STDMETHOD(SetHotkey)(THIS_ WORD wHotkey) PURE;

STDMETHOD(GetShowCmd)(THIS_ int *piShowCmd) PURE;
STDMETHOD(SetShowCmd)(THIS_ int iShowCmd) PURE;

STDMETHOD(GetIconLocation)(THIS_ LPSTR pszIconPath, int cchIconPath, int *piIcon)
PURE;
STDMETHOD(SetIconLocation)(THIS_ LPCSTR pszIconPath, int iIcon) PURE;

STDMETHOD(SetRelativePath)(THIS_ LPCSTR pszPathRel, DWORD dwReserved) PURE;

STDMETHOD(Resolve)(THIS_ HWND hwnd, DWORD fFlags) PURE;

STDMETHOD(SetPath)(THIS_ LPCSTR pszFile) PURE;
};

Видим определение метода SetPath. Считаем
какой по счету этот метод в интерфейсе (20) и
умножаем полученное число на 4 (sizeof(DWORD), а
указатель на функцию, как и любой другой –
это DWORD (в 32битных процах, коню понятно :))) –
получаем 80, что и требовалось доказать ;).
Оговорочка для Delphi: так как в этой
продвинутой среде повсеместно
используется наследование, в том числе и в
объявлениях интерфейсов, придется походить
по ссылочкам к предкам, считая и их методы.
Определения родительских методов
находиться в памяти перед определениями
родных. Для ассемблера же все и проще и
сложнее одновременно: вы должны сами
определить интерфейсы, как структуры, но
получить смещение адреса метода в таблице
можно так: <ИМЯ_СТРУКТУРЫ_ИНТЕРФЕЙСА>.<ИМЯ_МЕТОДА>.

Можно еще смотреть в отладчике, для дельфи
это даже более удобно. Вот как это можно
сделать для Delphi (руководство для дурака :)):

Пишем код, обращающийся к нужному методу.
Ставим на этом месте точку останова.
Когда она сработала открываем окно
отладчика(Ctrl+Alt+C) и видим примерно такую
картину:

много push’ей, которые просто передают
аргументы и:

mov eax, <адрес_интерфейса>
push eax
mov eax, [eax]
call dword ptr [eax+OUR_CONST]

Где OUR_CONST и есть нужная нам константа.
Проверено на Delphi 7 Enterprise.

Ок, вот функции перехвата для асма (masm) и Delphi:

Delphi:

procedure HookCOMTable(pIntf: Pointer;
COMTableThunk: DWORD; pNewFunc: Pointer; pOldMethodAddr: PDWORD);
var
pTableThunk : Pointer;
op : DWORD;
begin
pTableThunk:=Pointer(PDWORD(PDWORD(pIntf)^)^+COMTableThunk);
if pOldMethodAddr<> nil then pOldMethodAddr^:=PDWORD(pTableThunk)^;
VirtualProtect(pTableThunk,4, PAGE_READWRITE,@op);
PDWORD(pTableThunk)^:=DWORD(pNewFunc);
end;

Assembler:

; Устанавливаем
атрибуты страниц

CAP proc USES ecx eax lpPages: DWORD, dwAtr: DWORD, dwSize: DWORD
local lastpr : DWORD
lea eax, lastpr
push eax
push dwAtr
push dwSize
push lpPages
call VirtualProtect
ret
CAP endp 

TableCOMHook proc USES eax ebx edx edi pIntf:
DWORD, COMTableThunk: DWORD, pNewFunc: DWORD, pOldMethodAddr: DWORD
mov eax, pIntf 
mov eax, dword ptr [eax]
add eax, COMTableThunk 
mov ebx, pNewFunc 
invoke CAP, eax, PAGE_READWRITE, 4
cmp pOldMethodAddr, 0
jz __next
mov edx, dword ptr [eax]
mov edi, [pOldMethodAddr]
mov [edi], edx
__next:
mov dword ptr [eax], ebx
ret
TableCOMHook endp 

Хотелось бы привести код на Delphi по созданию
ярлыка, ибо он немного отличается от
Сишного (напротив, код на ассемблере почти
не отличается от кода на С)

var
ShellLink : IShellLinkA;
PersistFile : IPersistFile;
old : DWORD;
begin
ShellLink:=IShellLink(CreateComObject(CLSID_ShellLink));
HookCOMTable(@ShellLink,CSETPATH,@New,@old);
HookCOMTable(@ShellLink,CSETPATH,Pointer(old),nil);
ShellLink.QueryInterface(IPersistFile,PersistFile);
ShellLink.SetPath(PChar(‘c:\1.exe’));
ShellLink.SetIconLocation(‘c:\1.exe’,0);
PersistFile.Save(PWChar(WideString(‘1.lnk’)), FALSE);
end;

Вроде бы тоже самое, скажет невнимательный
читатель. Как бы не так! Во-первых, Delphi сам
следит за счетчиками обращений к объекту и
разрушает его (объект) автоматически, если
обращений больше не будет, а значит нет
надобности в вызове Release методов. Во-вторых,
вместо низкоуровневого СoCreateInstance мы видим
CreateComObject, который принимает всего 1 параметр.
Все это наводит на мысль о том, что Delphi не
стоит насиловать и использовать для
перехвата вызовов методов в чужом
приложении… Хотя, конечно, никто не мешает
нам вызвать CoCreateInstance и из дельфи… Только
противоестественно это как-то :)… К тому же
будет ли дельфя следить за счетчиками
использования COM объектов в этом случае,
неизвестно…

Продолжение следует.

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

Check Also

Десятка быстрых. Выбираем консольный софт для повседневных нужд

И вновь на страницах нашего журнала рубрика «Кто самый большой гик на планете». В этот раз…